mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-09 03:59:45 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e8a4cca7d | ||
|
|
a42cc3d7df | ||
|
|
5a843fd841 | ||
|
|
e6d5d76010 | ||
|
|
2526f2ff99 | ||
|
|
94d7aab293 | ||
|
|
1bd17d59d9 | ||
|
|
c451698d6a | ||
|
|
1e617c3f64 | ||
|
|
9b177f3080 | ||
|
|
4c7bce3ad1 | ||
|
|
8dce24a0a1 | ||
|
|
12d0f0abe8 | ||
|
|
ac19fecc06 | ||
|
|
6ca60e9a0d | ||
|
|
c41bd44de7 | ||
|
|
447460e8cc | ||
|
|
ec8e405909 | ||
|
|
5ac66e5005 | ||
|
|
e40843dade | ||
|
|
5cfa395476 | ||
|
|
f656a721a6 | ||
|
|
42890ca79f | ||
|
|
37e4832ab3 | ||
|
|
1c2efd789a | ||
|
|
0d4b2cdd43 | ||
|
|
789d83903b | ||
|
|
9c624c75a9 | ||
|
|
8dd297d0f1 | ||
|
|
7d1016f3f4 | ||
|
|
e0d47034df | ||
|
|
8c0301484e | ||
|
|
2a9a67c174 | ||
|
|
11b0490228 | ||
|
|
ebe3383a3f | ||
|
|
ba8b2bcb9c | ||
|
|
38b06316ef | ||
|
|
1e57a924d0 | ||
|
|
ca17d0f5cd | ||
|
|
499e963593 | ||
|
|
f4612b1add | ||
|
|
9ac278e124 | ||
|
|
ccf18ea143 | ||
|
|
3480bf2b8c | ||
|
|
597df5e601 | ||
|
|
1b89043157 | ||
|
|
129739440a | ||
|
|
8191cb91b4 | ||
|
|
fb8f7d9df6 | ||
|
|
df20d77845 | ||
|
|
9e20319069 | ||
|
|
bd8a254453 | ||
|
|
0959545e21 | ||
|
|
a4c41a92ee | ||
|
|
2651c4a96b | ||
|
|
2579635138 | ||
|
|
1e278a280d | ||
|
|
56d98f164e | ||
|
|
8b95bffb0b | ||
|
|
c2d3fe5000 | ||
|
|
0049fa73ff | ||
|
|
0e63d59c01 | ||
|
|
85e3dde522 | ||
|
|
ca378670b2 | ||
|
|
f73b3c86a4 | ||
|
|
0f7224d168 | ||
|
|
8c2a846947 | ||
|
|
10c8cc629f | ||
|
|
17878ac579 | ||
|
|
1d45da3a8a | ||
|
|
0afaaa399e | ||
|
|
1e9678cb7c | ||
|
|
b1bea3a17c | ||
|
|
f932e410e6 | ||
|
|
c77292f09d | ||
|
|
e240850324 | ||
|
|
538acd098c | ||
|
|
ee1a6efd5c | ||
|
|
f1dadd1710 | ||
|
|
3cabb9e3fe | ||
|
|
3b908cc4f7 | ||
|
|
0d0ec83b7d | ||
|
|
c17a6f4ce1 | ||
|
|
902dc814d6 | ||
|
|
59b4be2a54 | ||
|
|
98e35984c6 | ||
|
|
dc390c3864 | ||
|
|
03d709820d | ||
|
|
663ec80abf | ||
|
|
46a9cd87c7 | ||
|
|
52ba0b7816 | ||
|
|
551f1cdc30 | ||
|
|
2b9869eddf | ||
|
|
710b6a1bfe | ||
|
|
2ce07f8631 | ||
|
|
52a558abbe | ||
|
|
5d9f479229 | ||
|
|
9067cc64d3 | ||
|
|
5dbd8774d8 | ||
|
|
60e6a06086 | ||
|
|
884e37dfdf | ||
|
|
d6428bf291 | ||
|
|
17b08c9633 | ||
|
|
dd97421d3f | ||
|
|
bdcc34dfa8 | ||
|
|
945a63bec5 | ||
|
|
91aebae636 | ||
|
|
269c6d3e4a | ||
|
|
40c9dae95b | ||
|
|
805ebe17be | ||
|
|
b434a8aa6e | ||
|
|
40e78366bc | ||
|
|
4661596ce3 | ||
|
|
fcd3cdbcd7 | ||
|
|
a732a34c65 | ||
|
|
d02478b7a1 | ||
|
|
0acba452ec | ||
|
|
e7318fa23d | ||
|
|
18c5357666 | ||
|
|
c6f6db857b | ||
|
|
6eff377a79 | ||
|
|
cc125e06a0 | ||
|
|
5602253e3f | ||
|
|
42094da9c1 | ||
|
|
df7068b6be | ||
|
|
ffe40c1695 | ||
|
|
a6df0023a4 | ||
|
|
162dec3411 | ||
|
|
fca1936c2c | ||
|
|
8486336f9b | ||
|
|
bcdb92ac68 | ||
|
|
0aa64b2de3 | ||
|
|
675f7fc238 | ||
|
|
c47c699486 | ||
|
|
a965c962cf | ||
|
|
d5cca319c9 | ||
|
|
3813a459c4 | ||
|
|
fa128f91e8 | ||
|
|
47c98bb23a | ||
|
|
726472a229 | ||
|
|
10a62b8d2f | ||
|
|
690172c464 | ||
|
|
4a759f82d4 | ||
|
|
ab48216a7b | ||
|
|
ad1a310f55 | ||
|
|
c2490b49f5 | ||
|
|
5f81454038 | ||
|
|
d98d8f977f | ||
|
|
a338990186 | ||
|
|
9db497056b | ||
|
|
a3ab37fc03 | ||
|
|
6f1bd21e59 | ||
|
|
211fca87df | ||
|
|
9ea044962a | ||
|
|
2299c8f70a | ||
|
|
a3bb38ec45 | ||
|
|
557aad0265 | ||
|
|
2b13a44933 | ||
|
|
a3b7d9de3f | ||
|
|
c966d7ad89 | ||
|
|
c0845b7fc5 | ||
|
|
7783b1c18e | ||
|
|
f217b23bd3 | ||
|
|
cea3ed40db | ||
|
|
05c1dabf1a | ||
|
|
e21a1872f7 | ||
|
|
6c09a1929c | ||
|
|
5bc1e24696 | ||
|
|
0fbbf068eb | ||
|
|
409f2466ad | ||
|
|
11237f831f | ||
|
|
bdecbffc6b | ||
|
|
092c7d1ef4 | ||
|
|
a8c1d4b7f8 | ||
|
|
a11f8b0aa5 | ||
|
|
eba3106c0e | ||
|
|
61ede87e1b | ||
|
|
a1d2fd7ed1 | ||
|
|
560cc9fbbc | ||
|
|
96b6068c32 | ||
|
|
3100ca05ed | ||
|
|
a332cc95db | ||
|
|
3a3f35c64a | ||
|
|
2cc6ea169b | ||
|
|
2251053b42 | ||
|
|
c471588f3e | ||
|
|
01e404c140 | ||
|
|
9c2d35d978 | ||
|
|
407f659763 | ||
|
|
f213f78bae | ||
|
|
41188ec734 | ||
|
|
6a6d58ad03 | ||
|
|
2ac3482144 | ||
|
|
9e3fd0f73a | ||
|
|
54d9363c97 | ||
|
|
fb966cff37 | ||
|
|
a0fc88abd6 | ||
|
|
5b14fe23f7 | ||
|
|
f06bfc30e7 | ||
|
|
e6f598c0a2 | ||
|
|
3478382eee | ||
|
|
f62589fdbe | ||
|
|
164619c5a8 | ||
|
|
3cd33d504c | ||
|
|
4a7e373da3 | ||
|
|
bb62a048b8 | ||
|
|
484c689337 | ||
|
|
3b64abf5e0 | ||
|
|
c8596ec391 | ||
|
|
bf994ca414 | ||
|
|
590c69a578 | ||
|
|
7507b47525 | ||
|
|
1be7eb77c1 | ||
|
|
800730103c | ||
|
|
9a0462a011 | ||
|
|
bc939b6303 | ||
|
|
3cda075b51 | ||
|
|
7c9b6f3a28 | ||
|
|
cc2c628cbe | ||
|
|
3e81173d63 | ||
|
|
df6928400f | ||
|
|
1e4ae3ddbf | ||
|
|
e2e99ae54a | ||
|
|
f499922379 | ||
|
|
14bef6bb46 | ||
|
|
c7833932a1 | ||
|
|
d2c69e6d4b | ||
|
|
19bfcb33f0 | ||
|
|
9e2c0b6eb0 | ||
|
|
24c3f24ae1 | ||
|
|
7c4c20799c | ||
|
|
68ba321acc | ||
|
|
a8755042bd | ||
|
|
255861c442 | ||
|
|
e0469a585f | ||
|
|
d398f35325 | ||
|
|
dc1fd367d3 | ||
|
|
eb1c40038c | ||
|
|
cd72accac3 | ||
|
|
4abccbb360 | ||
|
|
f92cfa4374 | ||
|
|
b3db5a1c83 | ||
|
|
9a1e768388 | ||
|
|
085dcded73 | ||
|
|
fd8d37e93d | ||
|
|
89a68d8829 | ||
|
|
f8ff3420d9 | ||
|
|
f681b9ad62 | ||
|
|
6c01f85add | ||
|
|
3a604e0855 | ||
|
|
6f45b24864 | ||
|
|
063b3c5dc8 | ||
|
|
20c2f42bf1 | ||
|
|
2e1fad783d | ||
|
|
afc45665fc | ||
|
|
63b6e0e834 | ||
|
|
259532bf2e | ||
|
|
9c059bade1 | ||
|
|
9c8c90391d | ||
|
|
bda3005082 | ||
|
|
aeb70de349 | ||
|
|
76cd7d7970 | ||
|
|
25a62e0fb3 | ||
|
|
72b7696810 | ||
|
|
e3450e4552 | ||
|
|
9760cbca4b | ||
|
|
ae03ef6323 | ||
|
|
3171291420 | ||
|
|
fd81df6972 | ||
|
|
879b958a3e | ||
|
|
be4450fdc3 | ||
|
|
67cfaf2ac6 | ||
|
|
bffb41ef47 | ||
|
|
3e2f45e2e1 | ||
|
|
de6229fb17 | ||
|
|
1ee6060afb | ||
|
|
cca3dd2e33 | ||
|
|
4329c0d380 | ||
|
|
f9fa567e1d | ||
|
|
6bc27ebef7 | ||
|
|
b8946f4c7b | ||
|
|
c4c320a755 | ||
|
|
33780a4955 | ||
|
|
11b5a77fe9 | ||
|
|
f9907b6d9c | ||
|
|
d56dec89b5 | ||
|
|
6c00851ac1 | ||
|
|
2043b682af | ||
|
|
e66d2758ed | ||
|
|
03056168d9 | ||
|
|
2d1db1ec6f | ||
|
|
7a34c60d61 | ||
|
|
50df0b95a0 | ||
|
|
af7f7371c3 | ||
|
|
bdde697225 | ||
|
|
eb448299c0 | ||
|
|
621e660604 | ||
|
|
cb2e1dbb4e | ||
|
|
bc5f84fd62 | ||
|
|
8a46ca9061 | ||
|
|
0ee72a5041 | ||
|
|
ad1660aadf | ||
|
|
472de9cdac | ||
|
|
ddd4d5d42b | ||
|
|
3b3ea883c3 | ||
|
|
750ca8c3b1 | ||
|
|
3c8a452900 | ||
|
|
1f079f9d13 | ||
|
|
e5cfa4bbc8 | ||
|
|
8d56d41768 | ||
|
|
45c9e623e2 | ||
|
|
053a41570c | ||
|
|
5b469ee9a3 | ||
|
|
4abef6a74e | ||
|
|
c5cf218e54 | ||
|
|
011e5beced | ||
|
|
f4012d6369 | ||
|
|
7dacaf8ea1 | ||
|
|
ca7b3d2ca5 | ||
|
|
7e1a7cb93c | ||
|
|
a266bdfe1d | ||
|
|
e43284572b | ||
|
|
0f66c316ee | ||
|
|
4aa645c007 | ||
|
|
88c2931963 | ||
|
|
85ddf96ffc | ||
|
|
e83b82afe5 | ||
|
|
2247e5a98d | ||
|
|
5b969c1f7a | ||
|
|
3cc132ec51 | ||
|
|
3b23994568 | ||
|
|
1e72cdc4b9 | ||
|
|
81ac4dd83b | ||
|
|
44b88bc91e | ||
|
|
2395c8a53e | ||
|
|
c30656aaf9 | ||
|
|
219beb5736 | ||
|
|
32c686fa67 | ||
|
|
f05ccd3b54 | ||
|
|
2dde837d9f | ||
|
|
bc6a39217a | ||
|
|
d39e2a7539 | ||
|
|
2465b93708 | ||
|
|
1d00c018e1 | ||
|
|
1f00c1825e | ||
|
|
63b8a8d3cc | ||
|
|
3a26753027 | ||
|
|
457d8cc791 | ||
|
|
14d076ee2e | ||
|
|
13c4aebeca | ||
|
|
d7ad4a2762 | ||
|
|
8abe826f70 | ||
|
|
7901bc5137 | ||
|
|
91d4d7ff86 | ||
|
|
2b5cadd984 | ||
|
|
e7e48be2c9 | ||
|
|
00a1292531 | ||
|
|
4be0a7ce4d | ||
|
|
1d380fad87 | ||
|
|
d18d8ba045 | ||
|
|
3fd5a748c7 | ||
|
|
3f26ca5c2f | ||
|
|
a6543d6a7f | ||
|
|
932bda54bd | ||
|
|
3b34b6c7b2 | ||
|
|
6c70d4bc5d | ||
|
|
4b9e05a2a8 | ||
|
|
af197f96db | ||
|
|
dfaae03b00 | ||
|
|
53548c8af4 | ||
|
|
41cf05a06a | ||
|
|
4f0f933941 | ||
|
|
ed4b4db405 | ||
|
|
af8c6f8ec7 | ||
|
|
e99c4d2d37 | ||
|
|
b8b4ca0e32 | ||
|
|
562a0c2ed7 | ||
|
|
79ec8e2786 | ||
|
|
b5d41babfa | ||
|
|
27a8ed96b6 | ||
|
|
8aee4538f2 | ||
|
|
e9e22db6a7 | ||
|
|
7025a99799 | ||
|
|
36c6dd9ef5 | ||
|
|
4a6a5fb699 | ||
|
|
d51d71e7c0 | ||
|
|
025cf7d954 | ||
|
|
5d5c26d5bc | ||
|
|
8a3ca07c4e | ||
|
|
dd9b9f746d | ||
|
|
6b5d593639 | ||
|
|
4d6721822a | ||
|
|
ad66a758a7 | ||
|
|
59599f69f8 |
No files matched your search
@@ -17,6 +17,11 @@ __pycache__/
|
||||
/mtp/
|
||||
/models/
|
||||
/Strata-data/
|
||||
# the folders docker/entrypoint.sh creates in a mounted data/ (a link to llama.cpp, the models, the keys): never part of a build (#3644)
|
||||
/data/install/
|
||||
/data/state/
|
||||
/data/home/
|
||||
/data/Strata-data/
|
||||
third_party/llama.cpp/
|
||||
third_party/*.zip
|
||||
*.done
|
||||
|
||||
@@ -25,3 +25,6 @@ docker/compose/* text eol=lf
|
||||
|
||||
# Apple tools (codesign, plutil) read these: LF only, wherever the repository is cloned
|
||||
*.plist text eol=lf
|
||||
|
||||
# vendored KaTeX (serve/web/vendor): the files are hashed byte for byte (PROVENANCE.json), never convert their line endings.
|
||||
serve/web/vendor/** -text
|
||||
@@ -66,7 +66,7 @@ body:
|
||||
attributes:
|
||||
label: Strata version
|
||||
description: "In the app: Settings > About. In the Terminal: `strata --version`. From source: the About tab of the web page, or `engine` in `GET /v1/status`, or the git commit."
|
||||
placeholder: "Strata 0.2.5 · engine 0.1.50"
|
||||
placeholder: "Strata 0.2.7 · engine 0.1.52"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
||||
@@ -13,7 +13,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: 'Release tag whose engine packages go into the images (for example v0.2.3)'
|
||||
description: 'Release tag whose engine packages go into the images (for example v0.2.7)'
|
||||
required: true
|
||||
type: string
|
||||
flavors:
|
||||
|
||||
@@ -20,7 +20,9 @@ permissions:
|
||||
|
||||
jobs:
|
||||
metal-link:
|
||||
runs-on: macos-14
|
||||
# the macOS 26 SDK: metal/src/mtl_rt.mm names MTLGPUFamilyMetal4 (and MTLCompileOptions.mathMode, macOS 15) behind @available,
|
||||
# which is a run-time check; the names must still be in the SDK the engine is compiled against
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
# setup.py's LLAMA_CPP_COMMIT and metal/setup_mac.py's metal-cpp pin
|
||||
|
||||
@@ -63,6 +63,10 @@ build-vision-cuda12.bat
|
||||
/.ccache/
|
||||
/.rocm-win/
|
||||
|
||||
# Optional MCP raster cache
|
||||
*.tool-media/
|
||||
/.tool-media/
|
||||
|
||||
# local Windows HIP packaging output
|
||||
dist-hip/
|
||||
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
# AGENTS.md
|
||||
|
||||
Strata runs the Qwen3.8-Flash-Next mixture-of-experts model (and its Coder, Swift 1.5, Unsloth, Gyro and NVFP4
|
||||
variants, and Qwen3.6-35B-A3B) on a normal PC: one NVIDIA or AMD graphics card plus system RAM on Windows or Linux,
|
||||
or an Apple Silicon Mac. Version 0.2.5, engine 0.1.50. It has three parts:
|
||||
Version 0.2.7, engine 0.1.52. The public model families are DeepSeekV4.1-Flash-1017,
|
||||
Qwen3.8-Flash-Next and Qwen3.6-35B-A3B. DeepSeek V4.1-Flash runs in the app and in the Terminal
|
||||
(`strata launch --family deepseek41 --model ist-das-2.00bpw`; sets in
|
||||
[docs/MODELS.md](docs/MODELS.md#deepseek-v41-flash)). Read **[docs/AI_REFERENCE.md](docs/AI_REFERENCE.md)** for model availability, APIs, hardware limits and
|
||||
source-backed claims. Articles and videos should use **[docs/MEDIA_KIT.md](docs/MEDIA_KIT.md)**.
|
||||
|
||||
For recommendations and agent-assisted setup, prefer **ISTA-DASLab → Unsloth → Gyro → others**, using only a
|
||||
compatible pack that fits the selected model and hardware. ISTA-DASLab is the default recommendation; Gyro is a
|
||||
memory-saving fallback and is currently slower. Honor an explicit user choice. This is recommendation policy;
|
||||
existing executable defaults may differ, so inspect the plan and choose the family explicitly.
|
||||
|
||||
Strata has three parts:
|
||||
|
||||
- **The app** (`apps/desktop`, the Tauri shell; `apps/host`, the Node host and the web UI). It is for people who do
|
||||
not want a terminal. The installer has the engine inside.
|
||||
not want a terminal. One installer per system ([latest release](https://github.com/Niko1221/Strata/releases/latest)):
|
||||
`Strata-0.2.7-windows-x64-setup.exe`, `Strata-0.2.7-linux-x86_64.AppImage`, `Strata-0.2.7-macos-arm64.dmg`.
|
||||
The Windows and Linux installers carry no engine: the app downloads the engine for the graphics card on first
|
||||
start, or installs a file the user already has. The Mac dmg keeps its small Metal engine inside.
|
||||
- **The Terminal** (`strata.py`, `strata_cli/`, `strata_control/`): the `strata` command. It sets up a model, serves
|
||||
it and watches it. Agents use this part.
|
||||
- **The engine and the server** (`src/`, `include/`, `metal/`, `sycl/` for the C++/CUDA/HIP/SYCL/Metal engine;
|
||||
`serve/` for the Python server with an OpenAI- and Anthropic-compatible API and a web page). `setup.py`, started by
|
||||
`START-HERE.bat` or `setup.sh`, is the older one-click installer from source.
|
||||
`START-HERE.bat` or `setup.sh`, is setup from a source checkout, for developers. The release paths are the
|
||||
installer and `strata launch`.
|
||||
|
||||
## Installing Strata for a user
|
||||
|
||||
Follow **[docs/AI_SETUP.md](docs/AI_SETUP.md)**. For an agent that has a shell, the short version is:
|
||||
Follow **[docs/AI_SETUP.md](docs/AI_SETUP.md)**. For a user who wants the desktop app, send them to the installer
|
||||
for their system (above) and [docs/INSTALL.md](docs/INSTALL.md). For an agent that has a shell, use the Terminal; the
|
||||
short version is:
|
||||
|
||||
1. Install the Terminal. It needs no administrator rights.
|
||||
|
||||
@@ -27,27 +42,28 @@ Follow **[docs/AI_SETUP.md](docs/AI_SETUP.md)**. For an agent that has a shell,
|
||||
2. Check what would happen. Nothing is changed:
|
||||
|
||||
```sh
|
||||
strata launch --plan --model q2_0
|
||||
strata launch --plan --family qwen --model q2_0
|
||||
```
|
||||
|
||||
3. Set up and serve without questions. It downloads the engine for the card and the model (tens of GB), starts the
|
||||
server, prints the addresses and returns. The server keeps running.
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
strata launch --family qwen --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
Useful flags (all of them: `strata launch --help`): `--family`, `--model-path` (files the user already has),
|
||||
`--data-dir`, `--context 32k|64k|128k`, `--vision off|on|gpu|cpu`, `--parallel N`, `--gpu 0,1`,
|
||||
`--access local|lan|cloudflare|tailscale`, `--port`, `--api-key-env NAME`, `--new-key`, `--save-preset NAME`,
|
||||
`--output json`. `strata launch --status` shows what is serving, `strata launch --stop` stops it, and
|
||||
`--speed faster|full` (DeepSeek), `--output json`. `strata launch --status` shows what is serving, `strata launch --stop` stops it, and
|
||||
`strata launch --show-key` prints the API key.
|
||||
4. Connect the user's apps: `strata connect` lists them, `strata connect <app> --check` sends one prompt and one tool
|
||||
call the way that app does.
|
||||
|
||||
Keep the server on `127.0.0.1` unless the user asks for more. Beyond it an API key is recommended: `--access lan` makes one for you, and `--no-api-key` serves without one (a warning, never a refusal; on a public tunnel the warning is stronger). The old route (clone
|
||||
the repository and run `START-HERE.bat --yes` or `./setup.sh --yes`) still works and is the fallback when the
|
||||
Terminal cannot run. Strata's MCP server ([docs/MCP_SERVER.md](docs/MCP_SERVER.md)) offers the same steps as tools.
|
||||
Keep the server on `127.0.0.1` unless the user asks for more. Beyond it an API key is recommended: `--access lan` makes one for you, and `--no-api-key` serves without one (a warning, never a refusal; on a public tunnel the warning is stronger). Source setup (`START-HERE.bat`, `setup.sh`) is the fallback for a developer or for a system with no release package; it is
|
||||
documented in [docs/SOURCE_INSTALL.md](docs/SOURCE_INSTALL.md). Those wrappers exist in a source checkout, not in the
|
||||
released app. Do not tell an app user to find a `.bat` file.
|
||||
Source setup does not make an unsupported model or backend compatible. Strata's MCP server ([docs/MCP_SERVER.md](docs/MCP_SERVER.md)) offers the same steps as tools.
|
||||
|
||||
## Working on the code
|
||||
|
||||
|
||||
+320
-19
@@ -8,7 +8,7 @@
|
||||
# below refuses anything older: silently building for a pre-Ampere arch would produce a binary that runs
|
||||
# and is wrong about its own performance, which is the worst of both outcomes.
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
project(strata VERSION 0.1.50 LANGUAGES CXX)
|
||||
project(strata VERSION 0.1.52 LANGUAGES CXX)
|
||||
# EXPERIMENTAL: the Intel Arc engine, the SYCL port in sycl/ (PR #423; icpx + oneMKL, docs/INTEL_ARC.md). It is its
|
||||
# own project that takes the unported sources from src/; with this on, nothing below is configured.
|
||||
option(STRATA_ENABLE_SYCL "EXPERIMENTAL: build the Intel Arc (SYCL) engine from sycl/ - docs/INTEL_ARC.md" OFF)
|
||||
@@ -59,6 +59,17 @@ function(strata_utf8_manifest target)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# The AVX kernels exist for x86 only. On aarch64 (DGX Spark / GB10) portable.cpp stands in for them and
|
||||
# native experts run on ggml-cpu's NEON dot products.
|
||||
if(NOT CMAKE_SYSTEM_PROCESSOR OR CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64|x64|X64|i[3-6]86|x86|X86)$") # an unset processor stays the x86 build, as before
|
||||
set(STRATA_CPU_X86 ON)
|
||||
else()
|
||||
set(STRATA_CPU_X86 OFF)
|
||||
endif()
|
||||
if(STRATA_ISA_FLOOR AND NOT STRATA_CPU_X86)
|
||||
message(WARNING "STRATA_ISA_FLOOR is an x86 older-CPU build and does not apply to ${CMAKE_SYSTEM_PROCESSOR}; ignored")
|
||||
set(STRATA_ISA_FLOOR "" CACHE STRING "" FORCE)
|
||||
endif()
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
@@ -174,13 +185,30 @@ elseif(STRATA_ENABLE_CUDA)
|
||||
if(MSVC AND STRATA_EXPERIMENTAL_SM60 AND NOT DEFINED CMAKE_CUDA_RUNTIME_LIBRARY)
|
||||
set(CMAKE_CUDA_RUNTIME_LIBRARY Shared)
|
||||
endif()
|
||||
# #4023: the default below has to be set BEFORE enable_language(CUDA): since CMake 3.18 that call fills an unset
|
||||
# CMAKE_CUDA_ARCHITECTURES with nvcc's own default (sm_75 for CUDA 13), so a test after it never saw "undefined" and
|
||||
# a build with no option was an sm_75 build. -DCMAKE_CUDA_ARCHITECTURES and the CUDAARCHS environment variable win.
|
||||
set(_strata_cuda_arch_defaulted OFF)
|
||||
if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES AND NOT DEFINED ENV{CUDAARCHS})
|
||||
if(STRATA_BUILD_DSV41)
|
||||
# #3935: the DeepSeek engine's own release shape (sm_86, sm_89, sm_120 + PTX 120, as packaging/build_dsv41_cuda.sh), so a hand
|
||||
# build of strata-dsv41 with no architecture runs on an RTX 30, 40 or 50 card; the Qwen engine's default below is unchanged
|
||||
set(CMAKE_CUDA_ARCHITECTURES "86-real;89-real;120" CACHE STRING "CUDA architectures")
|
||||
else()
|
||||
set(CMAKE_CUDA_ARCHITECTURES 120 CACHE STRING "CUDA architectures")
|
||||
endif()
|
||||
set(_strata_cuda_arch_defaulted ON)
|
||||
endif()
|
||||
enable_language(CUDA)
|
||||
# `enable_language(CUDA)` gives the .cu compile rules but NOT the imported targets, so `CUDA::cudart` has to
|
||||
# be brought in explicitly for the host-side tests that call cudaMalloc themselves.
|
||||
find_package(CUDAToolkit REQUIRED)
|
||||
option(STRATA_EXPERIMENTAL_SM60 "community build for GPUs below compute capability 7.5: Pascal sm_60, Volta sm_70 (#236)" OFF)
|
||||
if(NOT DEFINED CMAKE_CUDA_ARCHITECTURES)
|
||||
set(CMAKE_CUDA_ARCHITECTURES 120 CACHE STRING "CUDA architectures" FORCE)
|
||||
if(_strata_cuda_arch_defaulted)
|
||||
# a recommendation, not a refusal: the release engine is built for 75;86;89;120, a hand build names its card
|
||||
message(WARNING "Strata: CMAKE_CUDA_ARCHITECTURES was not set, so this build is for ${CMAKE_CUDA_ARCHITECTURES} only (sm_120 = RTX 50). For another "
|
||||
"card pass it: -DCMAKE_CUDA_ARCHITECTURES=native (the GPUs in this PC), 75 (RTX 20), 86 (RTX 30), "
|
||||
"89 (RTX 40), or a list like \"86;120\".")
|
||||
endif()
|
||||
foreach(_a IN LISTS CMAKE_CUDA_ARCHITECTURES)
|
||||
string(REGEX REPLACE "-.*$" "" _base "${_a}") # 120-real / 120-virtual are still 120
|
||||
@@ -202,9 +230,16 @@ elseif(STRATA_ENABLE_CUDA)
|
||||
"CMAKE_CUDA_ARCHITECTURES is '${CMAKE_CUDA_ARCHITECTURES}'. (A Pascal sm_60 or Volta sm_70 build is "
|
||||
"-DSTRATA_EXPERIMENTAL_SM60=ON; upstream does not support it.)")
|
||||
endif()
|
||||
if(_base LESS 60)
|
||||
if(_base LESS 52)
|
||||
message(FATAL_ERROR "CMAKE_CUDA_ARCHITECTURES '${CMAKE_CUDA_ARCHITECTURES}': even the experimental build "
|
||||
"needs compute capability 6.0 (sm_60) or newer")
|
||||
"needs compute capability 5.2 (sm_52, Maxwell) or newer")
|
||||
endif()
|
||||
# Maxwell (sm_52, #3218): one reporter built and ran this on two Quadro M6000 (CUDA 12.x, Qwen3.8-Flash-Next
|
||||
# IQ1, about 22 t/s). It has no native half-precision arithmetic, so it is slower per byte than Pascal; only
|
||||
# this opt-in build gets the floor. Warn, do not refuse.
|
||||
if(_base LESS 60)
|
||||
message(WARNING "Building for sm_${_base} (Maxwell): community-tested on one reporter's Quadro M6000 only, "
|
||||
"no native half-precision arithmetic, CUDA 12.x toolkit required (docs/OLDER_GPUS.md).")
|
||||
endif()
|
||||
# #220 / #224: RTX 50 engines built with CUDA 12.8 crashed in the prompt path on Linux (13.0 and newer did not)
|
||||
if(_base GREATER_EQUAL 120 AND CMAKE_CUDA_COMPILER_VERSION VERSION_LESS 13.0)
|
||||
@@ -304,6 +339,18 @@ if(STRATA_BUILD_TESTS)
|
||||
target_include_directories(split_rules_test PRIVATE include)
|
||||
add_test(NAME split_rules_test COMMAND split_rules_test)
|
||||
|
||||
# #3797: the out-of-memory report at start (per card free/total, the largest other programs): the text is a pure
|
||||
# function; the driver query (NVML, loaded at run time) is only checked to be safe to call. No GPU needed.
|
||||
add_executable(gpu_report_test tests/program/gpu_report_test.cpp)
|
||||
target_include_directories(gpu_report_test PRIVATE include)
|
||||
target_link_libraries(gpu_report_test PRIVATE ${CMAKE_DL_LIBS})
|
||||
add_test(NAME gpu_report_test COMMAND gpu_report_test)
|
||||
|
||||
# #3702: the later-stage reserve of the layer split search (header-only arithmetic, pinned against the old expression)
|
||||
add_executable(stage_reserve_test tests/program/stage_reserve_test.cpp)
|
||||
target_include_directories(stage_reserve_test PRIVATE include)
|
||||
add_test(NAME stage_reserve_test COMMAND stage_reserve_test)
|
||||
|
||||
# the prompt planner's host-memory model (#2824): header-only, no GPU
|
||||
add_executable(prefill_plan_test tests/program/prefill_plan_test.cpp)
|
||||
target_include_directories(prefill_plan_test PRIVATE include)
|
||||
@@ -419,6 +466,7 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
src/kernels/cuda/fused_gdn.cu
|
||||
src/kernels/cuda/dequant_bf16.cu
|
||||
src/kernels/cuda/qsa_select.cu
|
||||
src/kernels/cuda/qsa_pattn.cu
|
||||
src/kernels/cuda/verify_kernels.cu
|
||||
src/kernels/cuda/dflash2.cu
|
||||
src/kernels/cuda/iq_kernels.cu
|
||||
@@ -448,6 +496,29 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_include_directories(qsa_prompt_attn_parity PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
|
||||
target_link_libraries(qsa_prompt_attn_parity PRIVATE strata_kernels CUDA::cudart)
|
||||
endif()
|
||||
# PR #3943: STRATA_PATTN_SM60's score and value kernels against the decode kernels' output (the Daily's chunk kernel and
|
||||
# the merge), bitwise, on random int8 K/V (GPU, synthetic, no model). Only in the experimental sm_60 build; it skips
|
||||
# (77) on a build without the kernels.
|
||||
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA AND STRATA_EXPERIMENTAL_SM60)
|
||||
add_executable(qsa_pattn_parity src/kernels/qsa_pattn_parity.cpp)
|
||||
target_include_directories(qsa_pattn_parity PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
|
||||
target_link_libraries(qsa_pattn_parity PRIVATE strata_kernels CUDA::cudart)
|
||||
add_test(NAME qsa_pattn_parity COMMAND qsa_pattn_parity)
|
||||
set_tests_properties(qsa_pattn_parity PROPERTIES SKIP_RETURN_CODE 77)
|
||||
endif()
|
||||
# PR #3820: STRATA_SELECT_LONG's multi-CTA top-k and shuffle-free decode scorer against qsa_block_topk and
|
||||
# qsa_block_scores, bitwise, on random scores (GPU, synthetic, no model). The switches force the new kernels on any
|
||||
# card; the "n3" case fixes three CTAs a query. Only in the experimental sm_60 build (skips with 77 otherwise).
|
||||
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA AND STRATA_EXPERIMENTAL_SM60)
|
||||
add_executable(qsa_select_long_parity src/kernels/qsa_select_long_parity.cpp)
|
||||
target_include_directories(qsa_select_long_parity PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
|
||||
target_link_libraries(qsa_select_long_parity PRIVATE strata_kernels CUDA::cudart)
|
||||
add_test(NAME qsa_select_long_parity COMMAND qsa_select_long_parity)
|
||||
add_test(NAME qsa_select_long_parity_n3 COMMAND qsa_select_long_parity)
|
||||
set_tests_properties(qsa_select_long_parity qsa_select_long_parity_n3 PROPERTIES SKIP_RETURN_CODE 77)
|
||||
set_tests_properties(qsa_select_long_parity PROPERTIES ENVIRONMENT "STRATA_SCORES_DX=1;STRATA_TOPK_MULTI=1")
|
||||
set_tests_properties(qsa_select_long_parity_n3 PROPERTIES ENVIRONMENT "STRATA_SCORES_DX=1;STRATA_TOPK_MULTI=1;STRATA_TOPK_MULTI_N=3")
|
||||
endif()
|
||||
# PR #600: the Volta (sm_70) prompt attention's latency pipeline against the FP32 kernel it replaces. Only in the
|
||||
# experimental sm_60/sm_70 build, where prompt_attn_v70_kernel is compiled in.
|
||||
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA AND STRATA_EXPERIMENTAL_SM60)
|
||||
@@ -686,6 +757,14 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(conversation_snapshot_test PRIVATE strata_engine)
|
||||
add_test(NAME conversation_snapshot_test COMMAND conversation_snapshot_test)
|
||||
set_tests_properties(conversation_snapshot_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 120)
|
||||
# the same test with a GDN state over 1 MiB and the opt-in background checkpoint copy (STRATA_CKPT_ASYNC=1), alone and with
|
||||
# recent states kept in VRAM (STRATA_CKPT_VRAM_SLOTS): every checkpoint, capture and restore must give the same bytes
|
||||
add_test(NAME conversation_snapshot_test_ckpt_async COMMAND conversation_snapshot_test)
|
||||
set_tests_properties(conversation_snapshot_test_ckpt_async PROPERTIES
|
||||
ENVIRONMENT "STRATA_TEST_BIG_GDN=1;STRATA_CKPT_ASYNC=1" SKIP_RETURN_CODE 77 TIMEOUT 300)
|
||||
add_test(NAME conversation_snapshot_test_ckpt_vram_slots COMMAND conversation_snapshot_test)
|
||||
set_tests_properties(conversation_snapshot_test_ckpt_vram_slots PROPERTIES
|
||||
ENVIRONMENT "STRATA_TEST_BIG_GDN=1;STRATA_CKPT_ASYNC=1;STRATA_CKPT_VRAM_SLOTS=3" SKIP_RETURN_CODE 77 TIMEOUT 300)
|
||||
add_executable(conversation_validation_test src/core/conversation_validation_test.cpp)
|
||||
target_link_libraries(conversation_validation_test PRIVATE strata_engine)
|
||||
add_test(NAME conversation_validation_test COMMAND conversation_validation_test)
|
||||
@@ -822,8 +901,10 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
endif()
|
||||
set_source_files_properties(src/dsv41/ds4_main.cpp PROPERTIES COMPILE_DEFINITIONS "DS4_BUILD_ID=\"${STRATA_BUILD_ID}\"")
|
||||
target_include_directories(strata-dsv41 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${_strata_gpu_include_directories}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third_party/ggml)
|
||||
target_link_libraries(strata-dsv41 PRIVATE strata_dsv41_kernels ${_strata_gpu_runtime_target} ${_strata_gpu_blas_target})
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third_party/ggml ${CMAKE_CURRENT_SOURCE_DIR}/src/prefill)
|
||||
# strata_prefill: the RDNA3 WMMA dense GEMM (src/prefill/wmma_gemm.cu), which gemm() prefers over hipBLAS on gfx11 (measured
|
||||
# 4.97 -> 20-40 TFLOPS on this part: the chunked prompt's dense GEMM is 11.1 s of a 4K prompt at the library's rate)
|
||||
target_link_libraries(strata-dsv41 PRIVATE strata_prefill strata_dsv41_kernels ${_strata_gpu_runtime_target} ${_strata_gpu_blas_target})
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(strata-dsv41 PRIVATE Threads::Threads)
|
||||
if(WIN32 AND STRATA_ENABLE_HIP) # cudaMemGetInfo is the DXGI-budgeted hip_compat::mem_get_info (src/core/device.cu): on a Strix Halo the
|
||||
@@ -1100,6 +1181,13 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_include_directories(mmvq_il_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(mmvq_il_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME mmvq_il_parity COMMAND mmvq_il_parity)
|
||||
# sm_89's opt-in tables (#3453, STRATA_MMVQ_IL_89=1): the table check only (no case matches MMVQ_IL_ONLY)
|
||||
add_test(NAME mmvq_il_parity_89table COMMAND mmvq_il_parity)
|
||||
set_tests_properties(mmvq_il_parity_89table PROPERTIES ENVIRONMENT "STRATA_MMVQ_IL_89=1;MMVQ_IL_ONLY=no-such-case")
|
||||
# the opt-in STRATA_MMVQ_IL_PF kernels (hoisted group loads, exact integer forms): every variant bit-checked against
|
||||
# native_mmvq for every type, shape, T and rows choice
|
||||
add_test(NAME mmvq_il_parity_pf COMMAND mmvq_il_parity --pf 0,2,3,4,5,6)
|
||||
set_tests_properties(mmvq_il_parity_pf PROPERTIES TIMEOUT 900)
|
||||
# the wide dequant kernels (a thread per 8 values; sm_61's default), bitwise vs the per-group ones (GPU, synthetic,
|
||||
# no model; --bench adds old/new timings)
|
||||
add_executable(dequant_wide_parity src/kernels/dequant_wide_parity.cpp)
|
||||
@@ -1127,7 +1215,7 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
# the Gyro trellis decode kernels (TQ2_T / TQK6 / TQK7) on synthetic blocks: decode-once vs per-column bitwise, output
|
||||
# checksums to compare two builds byte for byte, --bench for timings (GPU, no model)
|
||||
add_executable(tq_pascal_bench src/kernels/tq_pascal_bench.cpp)
|
||||
target_include_directories(tq_pascal_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_include_directories(tq_pascal_bench PRIVATE ${_strata_gpu_include_directories} ${CMAKE_CURRENT_SOURCE_DIR}/third_party/ggml)
|
||||
target_link_libraries(tq_pascal_bench PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME tq_pascal_bench COMMAND tq_pascal_bench)
|
||||
set_tests_properties(iq_multi_parity PROPERTIES ENVIRONMENT "STRATA_KQ_SPLIT=1") # the Q4_K / Q5_K / Q8_0 Split path against the old kernels
|
||||
@@ -1155,11 +1243,22 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
add_executable(gdn_rec_parity src/prefill/gdn_rec_parity.cu)
|
||||
target_link_libraries(gdn_rec_parity PRIVATE ${_strata_gpu_runtime_target})
|
||||
add_test(NAME gdn_rec_parity COMMAND gdn_rec_parity)
|
||||
# STRATA_GDN_TC=1: the tensor-core helpers of the scan, each on one warp against a double-precision reference. Any sm_80+
|
||||
# card runs them; the scan itself needs 122 KB of shared memory a block (sm_80, sm_90), where gdn_rec_parity prints
|
||||
# "not on this card" for the tc variant
|
||||
add_test(NAME gdn_rec_parity_selftest_frag COMMAND gdn_rec_parity --selftest-frag)
|
||||
set_tests_properties(gdn_rec_parity_selftest_frag PROPERTIES SKIP_RETURN_CODE 77)
|
||||
# the prompt path's BF16 products against cuBLAS's own BF16 GEMM, within fp32 rounding (sm_7x runs them on the
|
||||
# FP16 tensor cores; GPU, synthetic, no model; --bench adds timings)
|
||||
add_executable(gemm_bf16_parity src/prefill/gemm_bf16_parity.cu)
|
||||
target_link_libraries(gemm_bf16_parity PRIVATE strata_prefill )
|
||||
add_test(NAME gemm_bf16_parity COMMAND gemm_bf16_parity)
|
||||
# STRATA_DEQUANT_SIDE: Gemm::native with the dense dequant on a side stream (two scratches) against the serial form,
|
||||
# bitwise (GPU, synthetic, no model; 77 without a device)
|
||||
add_executable(gemm_dequant_side_parity src/prefill/gemm_dequant_side_parity.cu)
|
||||
target_link_libraries(gemm_dequant_side_parity PRIVATE strata_prefill )
|
||||
add_test(NAME gemm_dequant_side_parity COMMAND gemm_dequant_side_parity)
|
||||
set_tests_properties(gemm_dequant_side_parity PROPERTIES SKIP_RETURN_CODE 77)
|
||||
# Pascal, opt-in STRATA_DENSE_F32=1: the dense projections' FP16 GEMMs as cublasSgemm on FP32 copies, bitwise vs
|
||||
# the cuBLAS FP16 call over a sweep of chunk sizes (GPU, synthetic, no model; --full sweeps every size, --bench
|
||||
# adds timings). Not a ctest: whether the two agree is a property of the cuBLAS build, which this checks.
|
||||
@@ -1172,6 +1271,12 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(gr_upmix_parity PRIVATE strata_prefill )
|
||||
add_test(NAME gr_upmix_parity COMMAND gr_upmix_parity)
|
||||
set_tests_properties(gr_upmix_parity PROPERTIES SKIP_RETURN_CODE 77)
|
||||
# the two kernels of the layer-major prompt path (STRATA_PREFILL_LAYER_MAJOR=1, #3976): moe_shared_init and
|
||||
# moe_scatter_add against a host reference, bitwise for the scatter (GPU, synthetic, no model)
|
||||
add_executable(lm_kernels_parity src/prefill/lm_kernels_parity.cu)
|
||||
target_link_libraries(lm_kernels_parity PRIVATE strata_prefill )
|
||||
add_test(NAME lm_kernels_parity COMMAND lm_kernels_parity)
|
||||
set_tests_properties(lm_kernels_parity PROPERTIES SKIP_RETURN_CODE 77)
|
||||
endif()
|
||||
# native_expert_grouped's group stride and fused SwiGLU + q8_1, bitwise vs the launches before them (GPU, synthetic,
|
||||
# no model; --bench adds v1/new timings of a window's 48 calls in a graph)
|
||||
@@ -1234,6 +1339,10 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_include_directories(gdn_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(gdn_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME gdn_parity COMMAND gdn_parity --selftest)
|
||||
# #3948: the same checks through the 4-blocks-a-head GDN step (the default on gfx1151), whose output-norm kernel writes the
|
||||
# STRATA_QFUSE q8_1 image on its own
|
||||
add_test(NAME gdn_parity_split COMMAND gdn_parity --selftest)
|
||||
set_tests_properties(gdn_parity_split PROPERTIES ENVIRONMENT "STRATA_GDN_SPLIT=1")
|
||||
|
||||
# v100/gdn-chunk: the chunked GDN prompt recurrence vs the token recurrence and a host reference (GPU,
|
||||
# synthetic, no model). The chunked path changes the summation order - FP32-level, not bit-exact - so the
|
||||
@@ -1267,6 +1376,12 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(spec_verify_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME spec_verify_parity COMMAND spec_verify_parity)
|
||||
set_tests_properties(spec_verify_parity PROPERTIES ENVIRONMENT "STRATA_OLD_SAMPLER=0;STRATA_SAMPLER_ONE_BLOCK=0")
|
||||
# the explicit mu / NULL verifier of the gated proposal (STRATA_SPEC_PROB=1, pick gate) against a CPU oracle
|
||||
add_executable(spec_proposal_parity src/kernels/spec_proposal_parity.cpp)
|
||||
target_include_directories(spec_proposal_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(spec_proposal_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME spec_proposal_parity COMMAND spec_proposal_parity)
|
||||
set_tests_properties(spec_proposal_parity PROPERTIES ENVIRONMENT "STRATA_OLD_SAMPLER=0;STRATA_SAMPLER_ONE_BLOCK=0" TIMEOUT 120)
|
||||
# the same fixtures on the other two sampled paths: the one-block kernel and the 0.1.20 kernel
|
||||
add_test(NAME sampler_parity_one_block COMMAND sampler_parity --selftest)
|
||||
set_tests_properties(sampler_parity_one_block PROPERTIES ENVIRONMENT "STRATA_OLD_SAMPLER=0;STRATA_SAMPLER_ONE_BLOCK=1")
|
||||
@@ -1404,6 +1519,12 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
add_test(NAME ple_parity COMMAND ple_parity --selftest
|
||||
--in "${STRATA_PLE_FIXTURE_DIR}/ple_in.bin" --out "${STRATA_PLE_FIXTURE_DIR}/ple_out.bin"
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
# STRATA_PLE_COMPACT: the raw IQ4_NL rows dequantized on the device against the host decode, bit for bit (synthetic, no model)
|
||||
add_executable(ple_iq4nl_dequant_test src/kernels/ple_iq4nl_dequant_test.cpp)
|
||||
target_include_directories(ple_iq4nl_dequant_test PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(ple_iq4nl_dequant_test PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME ple_iq4nl_dequant_test COMMAND ple_iq4nl_dequant_test)
|
||||
set_tests_properties(ple_iq4nl_dequant_test PROPERTIES SKIP_RETURN_CODE 77)
|
||||
|
||||
# Plan v0.3 P7: INT8 KV storage vs a host reference and vs the FP16 path (GPU, synthetic, no model).
|
||||
add_executable(kv_q8_parity src/kernels/kv_q8_parity.cpp)
|
||||
@@ -1475,6 +1596,13 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(pin_budget_test PRIVATE strata_core ${_strata_gpu_runtime_target})
|
||||
add_test(NAME pin_budget_test COMMAND pin_budget_test)
|
||||
|
||||
# #3350 / #3470: the conversation copy streams and events are pooled across short-lived threads (needs a device, else skips)
|
||||
add_executable(conversation_copy_streams_test src/core/conversation_copy_streams_test.cpp)
|
||||
target_include_directories(conversation_copy_streams_test PRIVATE include)
|
||||
target_link_libraries(conversation_copy_streams_test PRIVATE strata_core ${_strata_gpu_runtime_target})
|
||||
add_test(NAME conversation_copy_streams_test COMMAND conversation_copy_streams_test)
|
||||
set_tests_properties(conversation_copy_streams_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 120)
|
||||
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
add_executable(pinned_shared_test src/core/pinned_shared_test.cpp)
|
||||
target_link_libraries(pinned_shared_test PRIVATE strata_core ${_strata_gpu_runtime_target})
|
||||
@@ -1487,6 +1615,24 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(ple_reader_test PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME ple_reader_selftest COMMAND ple_reader_test --selftest)
|
||||
|
||||
# #3602: the same reader under load - several clients, a tiny row cache, one in-flight slot, injected delays, rows on
|
||||
# every page phase, late-writer checks, the reader after a failed read. CPU and a temp file only; 77 = no direct reads here.
|
||||
add_executable(ple_reader_stress_test src/ngram/ple_reader_stress_test.cpp)
|
||||
target_include_directories(ple_reader_stress_test PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(ple_reader_stress_test PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME ple_reader_stress COMMAND ple_reader_stress_test)
|
||||
set_tests_properties(ple_reader_stress PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 300)
|
||||
|
||||
# 0.2.7: the PLE residency rule (table + expert arena + headroom); header-only, needs nothing
|
||||
add_executable(ple_residency_test src/core/ple_residency_test.cpp)
|
||||
target_include_directories(ple_residency_test PRIVATE include)
|
||||
add_test(NAME ple_residency_test COMMAND ple_residency_test)
|
||||
|
||||
# 0.2.7 (#3911): the qwen35 host sampler's penalties on a short (draft-vocab) logit row; header-only, CPU
|
||||
add_executable(qwen35_sampling_test src/qwen35/sampling_test.cpp)
|
||||
target_include_directories(qwen35_sampling_test PRIVATE include)
|
||||
add_test(NAME qwen35_sampling_test COMMAND qwen35_sampling_test)
|
||||
|
||||
# The GPU test runs ON THE TARGET PC. `--selftest` allocates
|
||||
# and writes real VRAM, so a machine without a device fails this test rather than skipping it silently -
|
||||
# which is correct: an engine that targets one GPU should not report a pass for having no GPU.
|
||||
@@ -1544,6 +1690,9 @@ if(STRATA_BUILD_TESTS)
|
||||
add_executable(arch_defaults_test src/core/arch_defaults_test.cpp src/core/arch_defaults.cpp)
|
||||
target_include_directories(arch_defaults_test PRIVATE include)
|
||||
add_test(NAME arch_defaults_test COMMAND arch_defaults_test)
|
||||
add_executable(kva_test src/prefill/kva_test.cpp) # KVA: the switch, the automatic tail, the reasons a read is turned away
|
||||
target_include_directories(kva_test PRIVATE include)
|
||||
add_test(NAME kva_test COMMAND kva_test)
|
||||
add_executable(adapt_async_default_test src/core/adapt_async_default_test.cpp)
|
||||
target_include_directories(adapt_async_default_test PRIVATE include)
|
||||
add_test(NAME adapt_async_default_test COMMAND adapt_async_default_test)
|
||||
@@ -1575,6 +1724,14 @@ if(STRATA_BUILD_TESTS OR STRATA_BUILD_CONVERSATION_TESTS)
|
||||
add_executable(conv_cache_test src/program/conv_cache_test.cpp)
|
||||
target_include_directories(conv_cache_test PRIVATE include)
|
||||
add_test(NAME conv_cache_test COMMAND conv_cache_test)
|
||||
# --adapt-decay outside 0..1 falls back to 0.7 with a line (#1284; header-only, CPU)
|
||||
add_executable(adapt_decay_test src/program/adapt_decay_test.cpp)
|
||||
target_include_directories(adapt_decay_test PRIVATE include)
|
||||
add_test(NAME adapt_decay_test COMMAND adapt_decay_test)
|
||||
# which window-size graph is freed when the next one does not fit (#3702; header-only, CPU)
|
||||
add_executable(window_evict_test src/core/window_evict_test.cpp)
|
||||
target_include_directories(window_evict_test PRIVATE include)
|
||||
add_test(NAME window_evict_test COMMAND window_evict_test)
|
||||
# which path computes the draft layer's prompt K/V, and STRATA_SPLIT_MTP_BATCH's parsing (header-only, CPU)
|
||||
add_executable(draft_kv_plan_test src/program/draft_kv_plan_test.cpp)
|
||||
target_include_directories(draft_kv_plan_test PRIVATE include)
|
||||
@@ -1587,6 +1744,17 @@ if(STRATA_BUILD_TESTS OR STRATA_BUILD_CONVERSATION_TESTS)
|
||||
add_executable(spec_prob_test src/core/spec_prob_test.cpp)
|
||||
target_include_directories(spec_prob_test PRIVATE include)
|
||||
add_test(NAME spec_prob_test COMMAND spec_prob_test)
|
||||
# gated proposal packets (STRATA_SPEC_PROB=1, pick gate): masked measure, NULL tail, root rescue (header-only, CPU)
|
||||
add_executable(gated_proposal_test src/core/gated_proposal_test.cpp)
|
||||
target_include_directories(gated_proposal_test PRIVATE include)
|
||||
add_test(NAME gated_proposal_test COMMAND gated_proposal_test)
|
||||
# the admission law of that packet, enumerated exactly (no Monte Carlo), in the three gate modes (CPU)
|
||||
add_executable(spec_admission_test src/core/spec_admission_test.cpp src/spec/draft_policy.cpp)
|
||||
target_include_directories(spec_admission_test PRIVATE include)
|
||||
add_test(NAME spec_admission_default COMMAND ${CMAKE_COMMAND} -E env --unset=STRATA_SPEC_PROB_GATE $<TARGET_FILE:spec_admission_test>)
|
||||
add_test(NAME spec_admission_pick COMMAND ${CMAKE_COMMAND} -E env STRATA_SPEC_PROB_GATE=pick $<TARGET_FILE:spec_admission_test> pick)
|
||||
add_test(NAME spec_admission_top COMMAND ${CMAKE_COMMAND} -E env STRATA_SPEC_PROB_GATE=top $<TARGET_FILE:spec_admission_test> top)
|
||||
add_test(NAME spec_admission_exact_top_only COMMAND ${CMAKE_COMMAND} -E env STRATA_SPEC_PROB_GATE=TOP $<TARGET_FILE:spec_admission_test> pick)
|
||||
# the per-card window cap of --draft dflash2 (header-only, CPU)
|
||||
add_executable(draft_cap_test src/core/draft_cap_test.cpp)
|
||||
target_include_directories(draft_cap_test PRIVATE include)
|
||||
@@ -1605,8 +1773,13 @@ if(STRATA_BUILD_TESTS OR STRATA_BUILD_CONVERSATION_TESTS)
|
||||
add_test(NAME batch_groups_test COMMAND batch_groups_test)
|
||||
endif()
|
||||
|
||||
add_library(strata_kernels_cpu STATIC src/kernels/cpu/expert.cpp src/kernels/cpu/pool.cpp
|
||||
src/kernels/cpu/expert_layout.cpp src/kernels/cpu/q2_avx2.cpp src/kernels/cpu/hadamard.cpp)
|
||||
if(STRATA_CPU_X86)
|
||||
add_library(strata_kernels_cpu STATIC src/kernels/cpu/expert.cpp src/kernels/cpu/pool.cpp
|
||||
src/kernels/cpu/expert_layout.cpp src/kernels/cpu/q2_avx2.cpp src/kernels/cpu/hadamard.cpp)
|
||||
else()
|
||||
add_library(strata_kernels_cpu STATIC src/kernels/cpu/portable.cpp src/kernels/cpu/pool.cpp
|
||||
src/kernels/cpu/expert_layout.cpp src/kernels/cpu/hadamard.cpp)
|
||||
endif()
|
||||
target_include_directories(strata_kernels_cpu PUBLIC include)
|
||||
# Plan v0.3 P6: only the kernel files get the AVX-512 / AVX2 flags; the pool and the probe run on any x86-64,
|
||||
# and the engine picks the kernels at run time (cpu_avx512_ok).
|
||||
@@ -1718,11 +1891,13 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
endif()
|
||||
# part of strata_kernels_cpu (the pool calls these and they call its CPU probe: one library, so GNU ld's
|
||||
# one-pass static linking has no cycle to resolve)
|
||||
target_sources(strata_kernels_cpu PRIVATE src/kernels/cpu/native_expert.cpp src/kernels/cpu/iq_avx512.cpp
|
||||
src/kernels/cpu/iq_avx2.cpp src/kernels/cpu/kq_avx2.cpp
|
||||
src/kernels/cpu/kq_avx1.cpp src/kernels/cpu/q2_avx1.cpp src/kernels/cpu/iq_avx1.cpp
|
||||
src/kernels/cpu/nvfp4_avx512.cpp src/kernels/cpu/nvfp4_avx2.cpp
|
||||
src/kernels/cpu/tq_cpu.cpp src/kernels/cpu/tq_avx2.cpp src/kernels/cpu/tq_avx512.cpp)
|
||||
target_sources(strata_kernels_cpu PRIVATE src/kernels/cpu/native_expert.cpp)
|
||||
if(STRATA_CPU_X86)
|
||||
target_sources(strata_kernels_cpu PRIVATE src/kernels/cpu/iq_avx512.cpp src/kernels/cpu/iq_avx2.cpp
|
||||
src/kernels/cpu/kq_avx2.cpp src/kernels/cpu/kq_avx1.cpp src/kernels/cpu/q2_avx1.cpp
|
||||
src/kernels/cpu/iq_avx1.cpp src/kernels/cpu/nvfp4_avx512.cpp src/kernels/cpu/nvfp4_avx2.cpp
|
||||
src/kernels/cpu/tq_cpu.cpp src/kernels/cpu/tq_avx2.cpp src/kernels/cpu/tq_avx512.cpp)
|
||||
endif()
|
||||
target_include_directories(strata_kernels_cpu PRIVATE third_party/ggml)
|
||||
if(MSVC)
|
||||
set_source_files_properties(src/kernels/cpu/iq_avx512.cpp src/kernels/cpu/nvfp4_avx512.cpp
|
||||
@@ -1778,6 +1953,21 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
add_executable(router_dot_parity src/kernels/cpu/router_dot_parity.cpp)
|
||||
target_link_libraries(router_dot_parity PRIVATE strata_kernels_cpu)
|
||||
if(STRATA_BUILD_TESTS)
|
||||
# a token's native expert rows are the same bits at every number of tokens that share its expert (window-invariant arithmetic)
|
||||
add_executable(native_arithmetic_parity src/kernels/cpu/native_arithmetic_parity.cpp)
|
||||
target_link_libraries(native_arithmetic_parity PRIVATE strata_kernels_cpu)
|
||||
add_test(NAME native_arithmetic_parity COMMAND native_arithmetic_parity)
|
||||
add_test(NAME native_arithmetic_parity_avx COMMAND ${CMAKE_COMMAND} -E env
|
||||
STRATA_FORCE_ISA=avx $<TARGET_FILE:native_arithmetic_parity>)
|
||||
add_test(NAME native_arithmetic_parity_kq256 COMMAND ${CMAKE_COMMAND} -E env
|
||||
STRATA_KQ256=1 $<TARGET_FILE:native_arithmetic_parity>)
|
||||
add_test(NAME native_arithmetic_parity_mt1 COMMAND ${CMAKE_COMMAND} -E env STRATA_IQ_MT_MIN=1 $<TARGET_FILE:native_arithmetic_parity>)
|
||||
add_test(NAME native_arithmetic_parity_no512 COMMAND ${CMAKE_COMMAND} -E env STRATA_NO_IQ512=1 $<TARGET_FILE:native_arithmetic_parity>)
|
||||
add_test(NAME native_arithmetic_parity_ggml COMMAND ${CMAKE_COMMAND} -E env STRATA_NO_IQ512=1 STRATA_NO_IQ256=1 STRATA_NO_IQ4NL=1 $<TARGET_FILE:native_arithmetic_parity>)
|
||||
add_test(NAME native_arithmetic_parity_nvfp4_avx2 COMMAND ${CMAKE_COMMAND} -E env
|
||||
STRATA_NO_NVFP4_512=1 $<TARGET_FILE:native_arithmetic_parity>)
|
||||
add_test(NAME native_arithmetic_parity_nvfp4_ggml COMMAND ${CMAKE_COMMAND} -E env
|
||||
STRATA_NO_NVFP4_512=1 STRATA_NO_NVFP4_256=1 $<TARGET_FILE:native_arithmetic_parity>)
|
||||
add_executable(pool_tasks_test src/kernels/cpu/pool_tasks_test.cpp)
|
||||
target_link_libraries(pool_tasks_test PRIVATE strata_kernels_cpu)
|
||||
add_test(NAME pool_tasks_test COMMAND pool_tasks_test)
|
||||
@@ -1824,6 +2014,11 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
# tree leaves them to the FP16 prompt path
|
||||
if(STRATA_GGML_DIR AND EXISTS "${STRATA_GGML_DIR}/ggml/src/ggml-cuda/template-instances/mmq-instance-tqk6.cu")
|
||||
list(APPEND _strata_mmq_types tq2_t tqk6 tqk7)
|
||||
# Gyro-L's pair trellis (types 147-149): the same fork's ggml has their MMQ instances
|
||||
if(EXISTS "${STRATA_GGML_DIR}/ggml/src/ggml-cuda/template-instances/mmq-instance-tqp5.cu")
|
||||
list(APPEND _strata_mmq_types tqp5 tqp6 tqp7)
|
||||
set(_strata_mmq_tqp ON) # moe_mmq.cu names GGML_TYPE_TQP5-7 only where the ggml has them (STRATA_MMQ_TQP)
|
||||
endif()
|
||||
set(_strata_mmq_tq ON)
|
||||
elseif(STRATA_MMQ_TQ_OVERLAY AND (STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP))
|
||||
# an upstream llama.cpp: the trellis MMQ instances come from an overlay of the pinned ggml-cuda headers written at
|
||||
@@ -1840,6 +2035,7 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
configure_file(third_party/ggml/ggml-common-tq.h "${_strata_tq_overlay_dir}/ggml-common-tq.h" COPYONLY)
|
||||
configure_file(src/kernels/cuda/tq_lut_i8.cuh "${_strata_tq_overlay_dir}/tq_lut_i8.cuh" COPYONLY)
|
||||
set(_strata_mmq_tq ON)
|
||||
set(_strata_mmq_tqp ON) # the overlay adds the pair-trellis enumerators and instances
|
||||
set(_strata_mmq_tq_overlay ON)
|
||||
message(STATUS "trellis MMQ (Gyro prompts): overlay on the pinned ggml-cuda in ${_strata_tq_overlay_dir}")
|
||||
if(STRATA_BUILD_TESTS)
|
||||
@@ -1871,6 +2067,12 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
add_library(strata_mmq STATIC src/prefill/moe_mmq.cu src/prefill/moe_fused.cu src/prefill/moe_fused_iq.cu
|
||||
src/prefill/mmq_nvfp4_w4a8.cu
|
||||
src/prefill/ggml_cuda_host.cu ${_strata_quantize_cu})
|
||||
if(STRATA_EXPERIMENTAL_SM60 AND NOT STRATA_HIP_GFX906)
|
||||
# GP100 (sm_60): a resident layer's experts weight-stationary, one launch per product (moe_ws.hpp; opt-in at
|
||||
# run time, STRATA_MMQ_WS=1, and only where every visible GPU is cc 6.0)
|
||||
target_sources(strata_mmq PRIVATE src/prefill/moe_ws.cu src/prefill/moe_ws_gu_iq2.cu src/prefill/moe_ws_gu_iq3.cu
|
||||
src/prefill/moe_ws_down.cu)
|
||||
endif()
|
||||
# q8_0: the drafter's matrices (E-9); STRATA_ORCA_Q4KS_MMQ adds q5_0 (#296, in _strata_mmq_types), and
|
||||
# STRATA_MMQ_KQUANTS q4_k q5_k q5_1 q6_k, the experts of Unsloth's UD-Q4/Q5/Q6_K_XL (docs/QUANTS.md; the dispatch
|
||||
# follows eddoursul/Strata 7706c1b). MODEL-UPDATE Phase 1: on CUDA they are a library of their own, strata_kq
|
||||
@@ -1889,7 +2091,7 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
target_sources(strata_mmq PRIVATE ${_strata_mmq}/template-instances/mmq-instance-${_t}.cu)
|
||||
endforeach()
|
||||
if(_strata_mmq_tq_overlay)
|
||||
foreach(_t tq2_t tqk6 tqk7)
|
||||
foreach(_t tq2_t tqk6 tqk7 tqp5 tqp6 tqp7)
|
||||
target_sources(strata_mmq PRIVATE ${_strata_tq_overlay_dir}/template-instances/mmq-instance-${_t}.cu)
|
||||
endforeach()
|
||||
# first on the include path: the patched mmq.cuh / ggml.h shadow the pinned ones for this target only
|
||||
@@ -1899,6 +2101,12 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
# which exists only in the arch-specific target, so its one unit alone is built with 12x -> 12xa (the default
|
||||
# binary is unchanged and runs on sm_121; a card without that image falls back to W4A8 at run time)
|
||||
set(_strata_w4a4_archs ${CMAKE_CUDA_ARCHITECTURES})
|
||||
# `native` is first replaced by the detected architectures (e.g. 120-real), so the 12x -> 12xa rewrite below applies
|
||||
# to it too: with plain sm_120 ptxas refuses the FP4 MMA of this one unit (#4023)
|
||||
if("native" IN_LIST _strata_w4a4_archs AND CMAKE_CUDA_ARCHITECTURES_NATIVE)
|
||||
list(REMOVE_ITEM _strata_w4a4_archs native)
|
||||
list(APPEND _strata_w4a4_archs ${CMAKE_CUDA_ARCHITECTURES_NATIVE})
|
||||
endif()
|
||||
# (two steps: CMake before 3.29 rejects a \2 whose optional group did not match, "120" alone)
|
||||
list(TRANSFORM _strata_w4a4_archs REPLACE "^(12[0-9])$" "\\1a")
|
||||
list(TRANSFORM _strata_w4a4_archs REPLACE "^(12[0-9])-(real|virtual)$" "\\1a-\\2")
|
||||
@@ -2001,8 +2209,15 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
if(_strata_mmq_tq)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_MMQ_TQ=1)
|
||||
endif()
|
||||
if(_strata_mmq_tqp)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_MMQ_TQP=1)
|
||||
endif()
|
||||
target_link_libraries(strata_prefill PUBLIC strata_mmq)
|
||||
target_compile_definitions(strata_prefill PUBLIC STRATA_PREFILL_MMQ=1 STRATA_PREFILL_FUSED=1)
|
||||
if(STRATA_EXPERIMENTAL_SM60 AND NOT STRATA_HIP_GFX906)
|
||||
# the weight-stationary experts (moe_ws.hpp) are in strata_mmq: the prompt path may take them (STRATA_MMQ_WS=1)
|
||||
target_compile_definitions(strata_prefill PUBLIC STRATA_PREFILL_WS=1)
|
||||
endif()
|
||||
if(STRATA_BUILD_TESTS)
|
||||
# #136: the fused int8 experts against a double-precision reference and the MMQ path (exit 77 below sm_80)
|
||||
add_executable(prefill_fused_moe_test tests/cuda/prefill_fused_moe_test.cpp)
|
||||
@@ -2014,12 +2229,40 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
target_link_libraries(prefill_fused_iq_test PRIVATE strata_mmq ggml-base ${_strata_gpu_runtime_target})
|
||||
add_test(NAME prefill_fused_iq_test COMMAND prefill_fused_iq_test)
|
||||
set_tests_properties(prefill_fused_iq_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 600)
|
||||
# the opt-in STRATA_PF_FUSED_Q2N=1 on its own (the test leaves STRATA_PF_FUSED unset): the native Q2_0 pack's Q2_0 / Q2_0
|
||||
# layers on sm_80+ (77 below sm_80 and without a card)
|
||||
add_test(NAME prefill_fused_iq_test_q2n COMMAND prefill_fused_iq_test "--only=Q2_0 / Q2_0" --unset-pf-fused)
|
||||
set_tests_properties(prefill_fused_iq_test_q2n PROPERTIES ENVIRONMENT "STRATA_PF_FUSED_Q2N=1" SKIP_RETURN_CODE 77 TIMEOUT 600)
|
||||
# the opt-in STRATA_PF_FUSED_NT=256 kernels (256 threads, two blocks a multiprocessor, two-pass weight loads) against the
|
||||
# same references; the batches of the test reach them through the Batch overload of experts_native (77 below sm_80)
|
||||
add_test(NAME prefill_fused_iq_test_nt256 COMMAND prefill_fused_iq_test)
|
||||
set_tests_properties(prefill_fused_iq_test_nt256 PROPERTIES ENVIRONMENT "STRATA_PF_FUSED_NT=256" SKIP_RETURN_CODE 77 TIMEOUT 600)
|
||||
# the opt-in STRATA_PF_FUSED_F16=1 (f16 mma, rounding level) and STRATA_PF_FUSED_WS=1 (warp-specialized, the same F16 products;
|
||||
# about 101 KB of shared memory at its defaults, more than an sm_86 / sm_89 / sm_120 card's 99 KB, so it is set up on
|
||||
# sm_80 and sm_90 cards and the test skips (77) elsewhere)
|
||||
add_test(NAME prefill_fused_iq_test_f16 COMMAND prefill_fused_iq_test)
|
||||
set_tests_properties(prefill_fused_iq_test_f16 PROPERTIES ENVIRONMENT "STRATA_PF_FUSED_F16=1" SKIP_RETURN_CODE 77 TIMEOUT 600)
|
||||
add_test(NAME prefill_fused_iq_test_ws COMMAND prefill_fused_iq_test)
|
||||
set_tests_properties(prefill_fused_iq_test_ws PROPERTIES ENVIRONMENT "STRATA_PF_FUSED_WS=1" SKIP_RETURN_CODE 77 TIMEOUT 600)
|
||||
# the MoE input quantized per token and scattered to its rows (mmq::quantize_scatter) against the per-row
|
||||
# gather: the same q8_1 bytes for every MMQ layout (--bench: both at 2K, 8K and 32K tokens)
|
||||
add_executable(prefill_mmq_scatter_test tests/cuda/prefill_mmq_scatter_test.cpp)
|
||||
target_link_libraries(prefill_mmq_scatter_test PRIVATE strata_mmq ggml-base ${_strata_gpu_runtime_target})
|
||||
add_test(NAME prefill_mmq_scatter_test COMMAND prefill_mmq_scatter_test)
|
||||
set_tests_properties(prefill_mmq_scatter_test PROPERTIES TIMEOUT 120)
|
||||
if(STRATA_EXPERIMENTAL_SM60)
|
||||
# mmq::swiglu_quant: swiglu + the down product's q8_1 rows in one kernel against the two, byte for byte
|
||||
add_executable(prefill_swiglu_quant_test tests/cuda/prefill_swiglu_quant_test.cpp)
|
||||
target_link_libraries(prefill_swiglu_quant_test PRIVATE strata_mmq ggml-base ${_strata_gpu_runtime_target})
|
||||
add_test(NAME prefill_swiglu_quant_test COMMAND prefill_swiglu_quant_test)
|
||||
set_tests_properties(prefill_swiglu_quant_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 120)
|
||||
# the weight-stationary experts (STRATA_MMQ_WS) against the per-group MMQ, byte for byte, on random experts; the
|
||||
# verdict is given on a compute capability 6.0 card only (77 elsewhere)
|
||||
add_executable(prefill_ws_parity_test tests/cuda/prefill_ws_parity_test.cpp)
|
||||
target_link_libraries(prefill_ws_parity_test PRIVATE strata_mmq ggml-base ${_strata_gpu_runtime_target})
|
||||
add_test(NAME prefill_ws_parity_test COMMAND prefill_ws_parity_test)
|
||||
set_tests_properties(prefill_ws_parity_test PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 600)
|
||||
endif()
|
||||
endif()
|
||||
if(STRATA_BUILD_TESTS AND _strata_mmq_tab_on)
|
||||
# STRATA_MMQ_DIRECT: the pointer-table MMQ bit for bit against the gathered MMQ (exit 77 below sm_80)
|
||||
@@ -2078,7 +2321,7 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
list(APPEND _strata_mmq_sources "${_strata_mmq}/template-instances/mmq-instance-${_t}.cu")
|
||||
endforeach()
|
||||
if(_strata_mmq_tq_overlay)
|
||||
foreach(_t tq2_t tqk6 tqk7)
|
||||
foreach(_t tq2_t tqk6 tqk7 tqp5 tqp6 tqp7)
|
||||
list(APPEND _strata_mmq_sources "${_strata_tq_overlay_dir}/template-instances/mmq-instance-${_t}.cu")
|
||||
endforeach()
|
||||
endif()
|
||||
@@ -2103,6 +2346,9 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
if(_strata_mmq_tq)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_MMQ_TQ=1)
|
||||
endif()
|
||||
if(_strata_mmq_tqp)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_MMQ_TQP=1)
|
||||
endif()
|
||||
target_link_libraries(strata_mmq PUBLIC ggml-base hip::host roc::hipblas)
|
||||
target_link_libraries(strata_prefill PUBLIC strata_mmq)
|
||||
target_compile_definitions(strata_prefill PUBLIC STRATA_PREFILL_MMQ=1 STRATA_PREFILL_FUSED=1)
|
||||
@@ -2156,6 +2402,20 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
add_executable(native_expert_bench src/kernels/native_expert_bench.cpp)
|
||||
target_include_directories(native_expert_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(native_expert_bench PRIVATE strata_kernels strata_kernels_cpu strata_artifact ${_strata_gpu_runtime_target})
|
||||
if(STRATA_ENABLE_CUDA AND STRATA_EXPERIMENTAL_SM60)
|
||||
# STRATA_EXPERT_NIBBLE (include/strata/core/expert_nibble.hpp): the 4-bit gate/up expert slots against the
|
||||
# production kernels and the GGUF blobs, bit for bit, on a real model's layers (needs a GP100); --header: the
|
||||
# contract's host half, no GPU
|
||||
add_executable(iq_nibble_parity src/kernels/iq_nibble_parity.cpp)
|
||||
target_include_directories(iq_nibble_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(iq_nibble_parity PRIVATE strata_kernels strata_kernels_cpu strata_artifact ggml-base ${_strata_gpu_runtime_target})
|
||||
if(STRATA_BUILD_TESTS)
|
||||
add_test(NAME iq_nibble_parity_header COMMAND iq_nibble_parity --header)
|
||||
# the synthetic experts of every gate/up x down format pair, no model (skips with 77 on a card that is not a GP100)
|
||||
add_test(NAME iq_nibble_parity_synthetic COMMAND iq_nibble_parity --synthetic-only)
|
||||
set_tests_properties(iq_nibble_parity_synthetic PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 900)
|
||||
endif()
|
||||
endif()
|
||||
# MODEL-UPDATE: the dense Q8_0 GEMVs at the engine's shapes against the memory bandwidth (a benchmark, no test)
|
||||
add_executable(q8_gemv_bench src/kernels/q8_gemv_bench.cpp)
|
||||
target_include_directories(q8_gemv_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
@@ -2194,7 +2454,11 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
# APR: the trellis expert types and the Hadamard activation rotation, when ggml comes from the agention fork
|
||||
# (the only tree with these types; marked by its ggml-cuda/tq.cuh)
|
||||
if(STRATA_GGML_DIR AND EXISTS "${STRATA_GGML_DIR}/ggml/src/ggml-cuda/tq.cuh")
|
||||
foreach(_pair tqk6/tqk7 tq2_t/tq2_t)
|
||||
set(_strata_tq_pairs tqk6/tqk7 tq2_t/tq2_t)
|
||||
if(EXISTS "${STRATA_GGML_DIR}/ggml/src/ggml-cuda/template-instances/mmq-instance-tqp5.cu") # Gyro-L's pair trellis
|
||||
list(APPEND _strata_tq_pairs tqp5/tqp5 tqp6/tqp6 tqp7/tqp7)
|
||||
endif()
|
||||
foreach(_pair ${_strata_tq_pairs})
|
||||
string(REPLACE "/" "_" _name "${_pair}")
|
||||
add_test(NAME native_expert_parity_${_name} COMMAND native_expert_parity --synthetic ${_pair})
|
||||
add_test(NAME native_expert_parity_hadamard_${_name} COMMAND native_expert_parity --synthetic-hadamard ${_pair})
|
||||
@@ -2268,6 +2532,13 @@ if(STRATA_BUILD_TESTS)
|
||||
add_test(NAME tq_cpu_parity COMMAND tq_cpu_parity)
|
||||
add_test(NAME tq_cpu_parity_avx2 COMMAND ${CMAKE_COMMAND} -E env STRATA_FORCE_AVX2=1 $<TARGET_FILE:tq_cpu_parity>)
|
||||
set_tests_properties(tq_cpu_parity tq_cpu_parity_avx2 PROPERTIES TIMEOUT 300 SKIP_RETURN_CODE 77)
|
||||
# Gyro-L's pair trellis (types 147-149) and the older trellis rows on synthetic blocks, no ggml and no model: the scalar
|
||||
# kernel against a bit-by-bit reference, every ISA bit for bit equal to scalar, the type predicates unchanged
|
||||
add_executable(tqp_cpu_test src/kernels/cpu/tqp_cpu_test.cpp)
|
||||
target_link_libraries(tqp_cpu_test PRIVATE strata_kernels_cpu)
|
||||
add_test(NAME tqp_cpu_test COMMAND tqp_cpu_test)
|
||||
add_test(NAME tqp_cpu_test_avx2 COMMAND ${CMAKE_COMMAND} -E env STRATA_FORCE_AVX2=1 $<TARGET_FILE:tqp_cpu_test>)
|
||||
set_tests_properties(tqp_cpu_test tqp_cpu_test_avx2 PROPERTIES TIMEOUT 120)
|
||||
endif()
|
||||
|
||||
add_executable(expert_parity src/kernels/cpu/expert_parity.cpp)
|
||||
@@ -2297,6 +2568,10 @@ if(STRATA_BUILD_TESTS)
|
||||
add_executable(warm_policy_test src/prefill/warm_policy_test.cpp)
|
||||
target_include_directories(warm_policy_test PRIVATE include)
|
||||
add_test(NAME warm_policy_test COMMAND warm_policy_test)
|
||||
# #4005: which streamed expert copies get a copied event (header-only, CPU)
|
||||
add_executable(copy_events_test src/prefill/copy_events_test.cpp)
|
||||
target_include_directories(copy_events_test PRIVATE include)
|
||||
add_test(NAME copy_events_test COMMAND copy_events_test)
|
||||
# issue #29: tiny batches with sleeping workers, as on a big-VRAM card; a hang fails it
|
||||
add_executable(pool_stress src/kernels/cpu/pool_stress.cpp)
|
||||
target_link_libraries(pool_stress PRIVATE strata_kernels_cpu)
|
||||
@@ -2350,6 +2625,32 @@ if(STRATA_BUILD_TESTS)
|
||||
add_test(NAME aux_cpus_test COMMAND aux_cpus_test)
|
||||
endif()
|
||||
|
||||
# CUDA release kernels must not spill or keep a per-thread array in local memory: every access there is a round trip
|
||||
# to device memory in the hottest loops the engine has, and nothing on the build machine says so without -Xptxas -v
|
||||
# or a GPU. tools/check_cuda_spills.py reads the resource usage the compiler wrote into the kernel libraries
|
||||
# (cuobjdump: STACK and LOCAL per entry function and per sm_) and fails on any kernel outside its ALLOWED table, which
|
||||
# carries the measured allowance of the kernels that hold such an array on purpose. CUDA only: HIP has no cuobjdump.
|
||||
if(STRATA_ENABLE_CUDA AND STRATA_BUILD_TESTS AND NOT STRATA_ENABLE_HIP AND NOT STRATA_HIP_GFX906)
|
||||
find_package(Python3 QUIET COMPONENTS Interpreter)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
set(_strata_spill_libs)
|
||||
foreach(_t strata_kernels strata_core strata_engine strata_prefill strata_qwen35 strata_mmq strata_mmq_w4a4 strata_kq)
|
||||
if(TARGET ${_t})
|
||||
list(APPEND _strata_spill_libs $<TARGET_FILE:${_t}>)
|
||||
endif()
|
||||
endforeach()
|
||||
add_test(NAME cuda_spill_guard COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_cuda_spills.py
|
||||
--cuobjdump ${CUDAToolkit_BIN_DIR}/cuobjdump${CMAKE_EXECUTABLE_SUFFIX} ${_strata_spill_libs})
|
||||
set_tests_properties(cuda_spill_guard PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 600)
|
||||
if(NOT WIN32) # its stand-in demanglers are sh scripts
|
||||
add_test(NAME check_cuda_spills_unit COMMAND ${Python3_EXECUTABLE} -m unittest tools.test_check_cuda_spills
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "strata: no python3 - the cuda_spill_guard test is NOT registered")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(STRATA_BUILD_TESTS)
|
||||
enable_testing()
|
||||
# Model-level checks may be absent from source distributions.
|
||||
@@ -2377,7 +2678,7 @@ if(STRATA_HIP_GFX906)
|
||||
# f16kq::covers() answers false and the int8 kernels run, as on every other card)
|
||||
if(_strata_mmq_tq_overlay)
|
||||
set(_strata_tq_hip ${_strata_tq_overlay_dir}/quantize.cu)
|
||||
foreach(_t tq2_t tqk6 tqk7)
|
||||
foreach(_t tq2_t tqk6 tqk7 tqp5 tqp6 tqp7)
|
||||
list(APPEND _strata_tq_hip ${_strata_tq_overlay_dir}/template-instances/mmq-instance-${_t}.cu)
|
||||
endforeach()
|
||||
list(APPEND _strata_cu ${_strata_tq_hip})
|
||||
|
||||
@@ -68,6 +68,8 @@ RUN set -e; \
|
||||
|
||||
# ---- 2. the Terminal (the release archive, built here)
|
||||
FROM ${RUNTIME_BASE} AS terminal
|
||||
ARG STRATA_COMMIT=
|
||||
ENV STRATA_BUILD_COMMIT=${STRATA_COMMIT}
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends python3 ca-certificates git && rm -rf /var/lib/apt/lists/*
|
||||
COPY . /src/
|
||||
|
||||
+122
-301
@@ -1,367 +1,188 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="340">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <b>Deutsch</b> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<h3 align="center">Ein Spitzenmodell auf deinem eigenen Gaming-PC</h3>
|
||||
|
||||
<p align="center">Unterstützt NVIDIA, AMD, Intel · Windows, Linux, Mac · kostenlos und Open Source</p>
|
||||
<h3 align="center">Leistungsstarke KI auf deinem eigenen Computer.</h3>
|
||||
<p align="center">Chatten, programmieren und mit Bildern arbeiten, lokal.</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Neueste Version"></a>
|
||||
<a href="#modelle">Modelle</a> · <a href="#installation"><b>Installation</b></a> ·
|
||||
<a href="#hardware">Hardware</a> · <a href="#tools">Tools</a> ·
|
||||
<a href="#hilfe">Hilfe</a> · <a href="#strata-unterstützen">Unterstützen</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Strata herunterladen"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT-Lizenz"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub-Sterne"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=black" alt="Spendier mir einen Kaffee"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Strata auf Buy Me a Coffee unterstützen"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Ein Voxel-Pagodengarten, den das Modell von Strata geschrieben hat, im Browser"></a><br>
|
||||
<sub>Ein Voxel-Pagodengarten aus einem einzigen Prompt, auf einer RTX 5070 mit Strata (IQ3_S, 128K Kontext) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">ganzes Video (49 s)</a></sub></p>
|
||||
<p align="center"><sub><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <b>Deutsch</b> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a> · <a href="README.pl.md">Polski</a></sub></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#installation"><b>Installation</b></a> ·
|
||||
<a href="#modelle">Modelle</a> ·
|
||||
<a href="#wie-schnell-ist-es">Tempo</a> ·
|
||||
<a href="#benutzung">Benutzung</a> ·
|
||||
<a href="#wie-funktioniert-das">Wie es funktioniert</a> ·
|
||||
<a href="#etwas-ist-schiefgegangen">Hilfe</a> ·
|
||||
<a href="#strata-unterstützen">Unterstützen</a>
|
||||
</p>
|
||||
**Kostenlos, Open Source und ganz in deiner Hand.** Nutze Strata auf dem Desktop oder mit deinen liebsten Coding-Tools. Kein Konto nötig.
|
||||
|
||||
Strata lässt **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** laufen, ein Modell mit 125
|
||||
Milliarden Parametern, für das man sonst einen Server braucht, auf einem normalen PC. Ab 0.2.4 läuft im Terminal auch
|
||||
**DeepSeek V4.1-Flash**. Es chattet, schreibt Code, liest Bilder und arbeitet mit deinen Apps und Coding-Agents zusammen.
|
||||
Deine Chats und Dateien bleiben auf deinem PC.
|
||||
## Modelle
|
||||
|
||||
- **Läuft auf einem Gaming-PC.** 32 GB Arbeitsspeicher (RAM) und Grafikspeicher zusammen reichen für den Anfang.
|
||||
- **Passt zu deinen Werkzeugen.** Claude Code, Codex CLI, OpenCode und andere Agents verbinden sich über die OpenAI- und die Anthropic-API.
|
||||
- **Liest Bilder.** Das ist standardmäßig eingeschaltet.
|
||||
- **Kostenlos und privat.** MIT-Lizenz, kein Konto, keine Cloud, keine Telemetrie ([was es herunterlädt und wann](docs/SAFETY.md)).
|
||||
Wir empfehlen **zuerst ISTA-DASLab**, danach **Unsloth** und andere Anbieter, soweit ein passendes Modell in deinen Speicher passt.
|
||||
|
||||
## Was du brauchst
|
||||
<details>
|
||||
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> <b>Qwen3.8-Flash-Next</b></summary>
|
||||
|
||||
**Du brauchst mindestens 32 GB Speicher: Arbeitsspeicher (RAM) und Grafikspeicher (VRAM) zusammen.** Bei einem Mac oder einem
|
||||
Ryzen AI Max ist das der gemeinsame Speicher. Ein PC mit 32 GB und einer Karte mit 12-16 GB führt die kleineren Größen aus,
|
||||
mit 64 GB laufen alle Größen.
|
||||
|
||||
| Läuft auf | Anleitung |
|
||||
| Anbieter | Auswahl |
|
||||
| --- | --- |
|
||||
| **NVIDIA** GeForce RTX 20, 30, 40 und 50 Serie, 12 GB oder mehr | [Installation](#installation) |
|
||||
| **AMD** Radeon RX 6800 und neuer, Radeon AI PRO R9700 | [AMD-Karten](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md) |
|
||||
| **Apple-Silicon-Mac**, M1 oder neuer, macOS 15 oder neuer | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** und andere PCs mit gemeinsamem Speicher | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Ältere Karten und Prozessoren | [Ältere GPUs](docs/OLDER_GPUS.md) |
|
||||
| **ISTA-DASLab · empfohlen** | Q2\_0, IQ2\_XS, IQ3\_XXS, IQ3\_S. [Coder: IQ1\_M](docs/MODELS.md#coder) zum Programmieren. |
|
||||
| **Unsloth** | UD-IQ4\_XS, UD-Q4\_K\_XL, UD-Q5\_K\_XL, UD-Q6\_K\_XL, Q8\_0 |
|
||||
| **Gyro · agentionai** | [Gyro-S, Gyro-M](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF). Geringerer Speicherbedarf, aber langsamer. |
|
||||
| **Weitere Optionen** | [Swift 1.5](docs/MODELS.md#swift-15), [NVFP4](docs/NVFP4.md), [OrcaRouter](docs/ORCA.md) |
|
||||
|
||||
Außerdem brauchst du etwa 80 GB freien Speicherplatz (eine SSD), Windows 10 / 11, Linux oder macOS und einen aktuellen
|
||||
Grafiktreiber. Zwei oder drei Karten desselben Herstellers können sich ein Modell teilen ([Multi-GPU](docs/MULTI_GPU.md)).
|
||||
[Größen, Speicherbedarf und Kompatibilität vergleichen](docs/MODELS.md).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> <b>Qwen3.6-35B-A3B</b></summary>
|
||||
|
||||
**Unsloth:** UD-Q4_K_XL, UD-Q6_K_XL, Q8_0. Das sind die derzeit dokumentierten Pakete für dieses Modell.
|
||||
|
||||
[Modellanleitung und Kompatibilität](docs/QWEN36.md).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><img src="docs/media/models/deepseek.svg" width="22" alt="" align="middle"> <b>DeepSeekV4.1-Flash-1017</b></summary>
|
||||
|
||||
In der **Desktop-App** und im **Terminal**. Was nicht in den Arbeitsspeicher passt, wird von deiner SSD gelesen. Darum brauchst du viel freien Speicherplatz.
|
||||
|
||||
| Set (IST-DAS) | Download | Freier Speicherplatz nötig |
|
||||
| --- | ---: | ---: |
|
||||
| 1.75 bpw | 239 GB | etwa 245 GB |
|
||||
| 2.00 bpw | 257 GB | etwa 265 GB |
|
||||
| 2.25 bpw | 274 GB | etwa 280 GB |
|
||||
|
||||
Läuft auf NVIDIA RTX 30, 40 und 50 (Windows und Linux) und auf AMD Radeon AI PRO R9700 (Linux). Das Setup lädt die Dateien und die DeepSeek-Engine herunter und startet dann das Modell.
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
```
|
||||
|
||||
Gemessen mit dem 2.00-bpw-Set auf Strata 0.2.5. „Ausgabe“ ist das Tempo der Antwort, „Prompt“ zeigt, wie schnell Strata deine Eingabe liest.
|
||||
|
||||
| Hardware | Ausgabe | Prompt |
|
||||
| --- | ---: | ---: |
|
||||
| RTX 5070 12 GB, 64 GB RAM | 4 bis 6 Token/s | etwa 110 Token/s (4K) |
|
||||
| RTX 3060 12 GB, 46 GB RAM | 9 bis 11 Token/s | etwa 155 Token/s (4K) |
|
||||
| 1x Radeon AI PRO R9700, 251 GB RAM | 21 bis 25 Token/s | 320 bis 360 Token/s (8K) |
|
||||
| 4x Radeon AI PRO R9700 | 74 bis 77 Token/s | 400 bis 450 Token/s (8K) |
|
||||
|
||||
[Speicher, Festplatte und Details](docs/DEEPSEEK41.md) · [Versionshinweise](https://github.com/Niko1221/Strata/releases)
|
||||
|
||||
</details>
|
||||
|
||||
<a id="the-other-sizes"></a>
|
||||
|
||||
Unsicher, was passt? [Wähle ein Modell für deinen Computer](docs/MODELS.md#pick-by-memory).
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Die Strata-App (am einfachsten)
|
||||
<a id="option-a-the-strata-app"></a>
|
||||
|
||||
Lade die Datei für deinen Computer herunter, öffne sie und drück auf Installieren beim empfohlenen Modell.
|
||||
### Desktop-App
|
||||
|
||||
**Windows 10 / 11**
|
||||
**[Strata für Windows, Linux oder Mac herunterladen →](https://github.com/Niko1221/Strata/releases/latest)**
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-windows-x64-setup.exe)
|
||||
Installiere die App, wähle ein Modell und leg mit dem Chat los. Es gibt einen Installer pro System. Beim ersten Start lädt die App die Engine für deine Grafikkarte herunter. [Hilfe zum Installer](docs/INSTALL.md#the-app).
|
||||
|
||||
**Linux**
|
||||
<sub>Dateien: `Strata-0.2.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-linux-x86_64.AppImage)
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Strata-Desktop-App mit Chat und Live-Codevorschau nebeneinander">
|
||||
<br><sub>Chat, Dateien und Live-Vorschau in einem Fenster.</sub>
|
||||
</p>
|
||||
|
||||
**Mac** (Apple Silicon, macOS 15 oder neuer)
|
||||
<a id="option-b-the-terminal-one-line"></a>
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-macos-arm64.dmg)
|
||||
### Terminal-App
|
||||
|
||||
Die App wählt die passende Engine für deine Grafikkarte selbst. Alle Dateien stehen auf der
|
||||
[Releases-Seite](https://github.com/Niko1221/Strata/releases/latest).
|
||||
Lieber im Terminal? Installiere Strata und starte dann **`strata launch`**.
|
||||
|
||||
| Erster Start | Was du tust |
|
||||
| --- | --- |
|
||||
| **Windows** | Starte die `.exe`. Du brauchst keine Administratorrechte. Wenn Windows "Der Computer wurde durch Windows geschützt" meldet, wähle **Weitere Informationen** und dann **Trotzdem ausführen**. |
|
||||
| **Linux** | Mach die Datei ausführbar (`chmod +x`) und starte sie. Sie braucht FUSE 2: `sudo apt install libfuse2t64` (Ubuntu 24.04 und neuer) oder `libfuse2` (22.04). |
|
||||
| **Mac** | Öffne die `.dmg` und zieh Strata in den Ordner "Programme". |
|
||||
<details open>
|
||||
<summary><b>Installationsbefehle</b></summary>
|
||||
|
||||
Das Modell ist ein großer Download (40-70 GB). Wenn er abbricht, geht er dort weiter, wo er aufgehört hat.
|
||||
|
||||
### 2. Das Terminal (eine Zeile)
|
||||
|
||||
Der Befehl `strata`, für Skripte, Server und KI-Agents. Damit läuft auch DeepSeek.
|
||||
|
||||
Linux und Mac:
|
||||
**Linux / Mac**
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
**Windows PowerShell**
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
Dann startest du es. Es stellt dir eine Frage nach der anderen (Modell, Kontext, Bilder, wer sich verbinden darf), lädt,
|
||||
was es braucht, und beginnt zu laufen:
|
||||
Öffne unter Windows danach ein neues Terminal. Dann:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
Du brauchst keine Administratorrechte. Die Installation prüft die SHA-256-Prüfsumme von allem, was sie herunterlädt. Öffne
|
||||
unter Windows nach der Installation ein neues Terminalfenster. Alle Bildschirme und Flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### 3. Deine KI richtet es ein
|
||||
|
||||
Füge das in Claude Code, Cursor, Codex, GitHub Copilot oder einen anderen Coding-Assistenten ein:
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
Der Assistent prüft deine Grafikkarte, deinen Arbeitsspeicher und den Speicherplatz, wählt das passende Modell, installiert
|
||||
und startet es und sagt dir, wie du deine Apps verbindest. KI-Werkzeuge können Strata auch über den
|
||||
[MCP-Server](docs/MCP_SERVER.md) installieren, starten und stoppen.
|
||||
|
||||
Du willst es lieber selbst bauen? Siehe [Aus dem Quellcode bauen](#aus-dem-quellcode-bauen).
|
||||
|
||||
## Modelle
|
||||
|
||||
Die App und `strata launch` empfehlen eine Größe passend zu deinem Speicher. Kleinere Größen sind schneller, größere
|
||||
Größen sind etwas klüger.
|
||||
|
||||
### Wenig Speicher? Probier Gyro
|
||||
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) von agentionai ist die kleinste Möglichkeit, Qwen3.8-Flash-Next
|
||||
auszuführen. Es läuft auf NVIDIA, AMD, Intel Arc und Apple Silicon.
|
||||
|
||||
| Größe | Benötigter Speicher (RAM + VRAM) | Download | Geeignet für |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27,6 GB | 58,5 GB | PCs, auf denen nichts anderes läuft. 9 Tokens/s auf einem Mac mini mit 32 GB. |
|
||||
| **Gyro-M** | 35 GB | 92,0 GB | Ungefähr die Qualität von IQ3_XXS bei 20 % weniger GPU-Speicher. |
|
||||
|
||||
Starte es mit `strata launch --model gyro-s`. Details: [docs/GYRO.md](docs/GYRO.md).
|
||||
|
||||
### Die Größen von Qwen3.8-Flash-Next
|
||||
|
||||
| Dein Speicher (RAM + VRAM) | Nimm | Warum |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Gyro-S**, oder den **[Coder](docs/MODELS.md#coder)** für Code | Der Coder behält die Experten, die Code braucht |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB) oder **Gyro-M** | Swift denkt kürzer nach, bevor es antwortet |
|
||||
| **48 GB** | **IQ3_XXS** (47,0 GB) | Langsamer, besser |
|
||||
| **64 GB** | **IQ3_S** (54,8 GB) | Erreicht in den veröffentlichten Tests das volle Modell |
|
||||
| **96 GB oder mehr** | **UD-IQ4_XS** bis **Q8_0** von Unsloth | Am nächsten am vollen Modell |
|
||||
|
||||
Auch mit weniger RAM, als eine Zeile nennt, kann es funktionieren, nur langsamer: Strata wechselt von selbst in seinen
|
||||
[Modus für wenig RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Außerdem gibt es:
|
||||
[NVFP4](docs/NVFP4.md) für NVIDIA-Karten, [Qwen3.6-35B-A3B](docs/QWEN36.md) (kleiner, mehrere Anfragen gleichzeitig) und
|
||||
[OrcaRouters Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs). Alle Größen und was wo läuft:
|
||||
[docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
### DeepSeek V4.1-Flash (Terminal, ab 0.2.4)
|
||||
|
||||
Ein deutlich größeres Modell, das auf einer Grafikkarte mit 12 GB und 46 bis 64 GB RAM läuft. Strata hält die meistgenutzten
|
||||
Teile im Speicher und liest den Rest von deiner SSD. [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF)
|
||||
hat die Sets gebaut. Es läuft auf NVIDIA-RTX-Karten der Serien 30, 40 und 50 und auf der Radeon AI PRO R9700. Wähle es in
|
||||
`strata launch`. Die Desktop-App bietet es noch nicht an.
|
||||
|
||||
| Set | Download | Qualität | Schreibt Code mit |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M** (1,75 Bit) | 239 GB | Das kleinste, schwächer bei einfacher Prosa | auf einem PC noch nicht gemessen |
|
||||
| **IQ2_XXS** (2.00 Bit) | 257 GB | Gute Balance | 10 Tokens/s (RTX 3060), 5 (RTX 5070), 48 (4x R9700) |
|
||||
| **IQ2_XS** (2.25 Bit) | 274 GB | Am nächsten am Original | 15 Tokens/s (eine R9700), 80 (4x R9700) |
|
||||
| **IQ4_NL** (Engram-Tabelle) | in jedem Download oben enthalten | Die Nachschlagetabelle, die alle drei Sets teilen | |
|
||||
|
||||
Starte es mit `strata launch --model ist-das-2.00bpw` (oder `ist-das-1.75bpw`, `ist-das-2.25bpw`). Qualitätswerte,
|
||||
Geschwindigkeiten je PC und die manuelle Installation: [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
|
||||
|
||||
## Wie schnell ist es?
|
||||
|
||||
Gemessen mit einer Grafikkarte mit 12 GB (RTX 5070, 64 GB RAM). Ergebnisse anderer Karten:
|
||||
[Tempo jeder Größe](docs/MODELS.md#how-fast-is-each-size) und [Ergebnisse der Community](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
| Modell | Antworten erscheinen mit | Liest deinen Prompt mit |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████████` 94 Tokens/s | `████████████` 2.650 Tokens/s |
|
||||
| **IQ2_XS** | `██████████░░` 79 Tokens/s | `█████████░░░` 2.090 Tokens/s |
|
||||
| **IQ3_XXS** | `████████░░░░` 62 Tokens/s | `████████░░░░` 1.750 Tokens/s |
|
||||
| **IQ3_S** | `███████░░░░░` 53 Tokens/s | `███████░░░░░` 1.620 Tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 Tokens/s | `██████████░░` 2.180 Tokens/s |
|
||||
|
||||
Auf einer AMD-Karte mit 16 GB (RX 9070 XT, 47 GB RAM):
|
||||
|
||||
| Modell | Antworten erscheinen mit | Liest deinen Prompt mit |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████░░░░` 60 Tokens/s | `█████░░░░░░░` 1.160 Tokens/s |
|
||||
| **IQ2_XS** | `███████░░░░░` 52 Tokens/s | `█████░░░░░░░` 1.110 Tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 Tokens/s | `██████░░░░░░` 1.420 Tokens/s |
|
||||
|
||||
Ein Token ist etwa ¾ eines Wortes, 60 Tokens pro Sekunde sind also schneller, als du lesen kannst. "Liest deinen Prompt"
|
||||
beschreibt, wie schnell Strata ein langes Dokument, Code oder einen Chatverlauf aufnimmt.
|
||||
|
||||
## Benutzung
|
||||
|
||||
### In der App
|
||||
|
||||
Ein Chat, ein **Monitor** für das Modell sowie für GPU, CPU und RAM, eine Seite **Modelle** und die **Einstellungen**. Der
|
||||
Chat kann in einem Ordner auf deinem PC arbeiten. Im Modus **Manuell** fragt er nach, bevor er eine Datei ändert oder einen
|
||||
Befehl ausführt. Er kann das Web durchsuchen (DuckDuckGo oder Bing, ohne Konto). Die Suchbegriffe sind das Einzige, was
|
||||
deinen PC verlässt.
|
||||
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="Die Strata-App: ein Chat, in dem der Agent einen kleinen Voxel-Pagodengarten gebaut hat, die Seite ist rechts in der Vorschau geöffnet"><br>
|
||||
<sub>Der Agent hat einen Voxel-Pagodengarten als eine HTML-Datei geschrieben, und die Seite öffnet sich in der Vorschau neben dem Chat</sub></p>
|
||||
|
||||
### Im Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="Die Übersicht des Strata-Terminal-Monitors, während das Modell eine Antwort schreibt"><br>
|
||||
<sub><code>strata monitor</code>, während das Modell antwortet: Geschwindigkeit, GPU, Speicher und die letzten Anfragen</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve and print the address
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor # watch speed, GPU, memory and requests
|
||||
strata update # get the newest engine
|
||||
strata doctor # check the installed files
|
||||
```
|
||||
|
||||
Um Strata von deinem Handy oder einem anderen PC aus zu erreichen, nutze `strata launch --access lan`. Der Befehl erzeugt
|
||||
einen API-Schlüssel für dich. `strata --help` listet alles auf, und [docs/TERMINAL.md](docs/TERMINAL.md) erklärt es.
|
||||
|
||||
### Mit KI-Agents und anderen Apps
|
||||
|
||||
Ein Agent installiert Strata mit dem Einzeiler oben und führt `strata launch --model NAME --yes --no-tui` aus. Die Schritte,
|
||||
Flags und Exit-Codes stehen in [docs/AI_SETUP.md](docs/AI_SETUP.md). Die Regeln für das Repository stehen in [AGENTS.md](AGENTS.md).
|
||||
|
||||
| Deine App spricht | Basis-URL |
|
||||
| --- | --- |
|
||||
| OpenAI-API (die meisten Apps und Agents) | `http://127.0.0.1:8080/v1` (beliebiger API-Schlüssel, beliebiger Modellname) |
|
||||
| Anthropic-API (Claude Code) | `http://127.0.0.1:8080`, ohne `/v1`. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([Einrichtung](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` gibt die genauen Einstellungen für OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent und OpenClaw
|
||||
aus, und `strata connect opencode --write` schreibt sie für dich (mit einer Sicherung).
|
||||
|
||||
### Gut zu wissen
|
||||
|
||||
- **Bilder** sind für jedes Modell eingeschaltet, das sie lesen kann. Hänge ein Bild im Chat an oder füge es ein, oder sende es aus deiner App. Details: [docs/IMAGES.md](docs/IMAGES.md).
|
||||
- **Das Denkniveau** kann aus, niedrig, mittel oder hoch sein, im Chat-Menü oder unter "reasoning effort" in deiner App. Aus ist am schnellsten.
|
||||
- **Die erste Nachricht** eines Chats wird komplett gelesen, etwa 1 Minute pro 30.000 Tokens. Folgenachrichten starten in Sekunden.
|
||||
- **Eine Anfrage nach der anderen** ist die Standardeinstellung. Für mehrere nutze `--parallel N` ([BATCHING.md](docs/BATCHING.md)). Auf einer Karte mit 12 GB wird dadurch jede Antwort langsamer.
|
||||
|
||||
## Aus dem Quellcode bauen
|
||||
|
||||
<details>
|
||||
<summary>Windows, Linux und Mac aus dem Repository</summary>
|
||||
|
||||
[Lade Strata herunter](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) und entpacke es, oder nutze `git clone`.
|
||||
**Windows:** Doppelklick auf `START-HERE.bat`. **Linux:** Führe `./setup.sh` im Strata-Ordner aus.
|
||||
|
||||
**Mac (Apple Silicon, macOS 15 oder neuer):** Installiere einmal die Command Line Tools von Apple mit `xcode-select --install`
|
||||
(Xcode selbst brauchst du nicht). Öffne das Terminal im Strata-Ordner und führe `./setup.sh` aus. Das Skript baut die
|
||||
Mac-Engine auf deinem Mac (etwa 7 Minuten), stellt dieselben Fragen, lädt das Modell herunter und startet es. Beim nächsten
|
||||
Mal startet `./setup.sh` sofort. Welche Größe zu deinem Mac passt: [docs/MAC.md](docs/MAC.md).
|
||||
|
||||
Der Installer erkennt deine Karte und richtet die passende Engine ein. Er fragt, welches Modell und welche Größe du willst,
|
||||
wie viel Kontext (wie viel Text sich das Modell merkt) und ob es Bilder lesen soll. Drück Enter für die empfohlene Antwort.
|
||||
Dann lädt er das Modell herunter (etwa 70 GB) und startet es. Wenn der Download abbricht, führe das Skript erneut aus, dann
|
||||
geht es weiter. Dein Browser öffnet die Strata-Seite unter `http://127.0.0.1:8080`.
|
||||
|
||||
> Während das Modell startet, kann dein PC 1-3 Minuten langsam sein oder nicht reagieren (beim ersten Mal am längsten). Strata
|
||||
> lädt 35-55 GB in deinen RAM und sperrt einen Teil davon für die Grafikkarte. Warte ab und schließe das Fenster nicht.
|
||||
|
||||
Starte beim nächsten Mal wieder `START-HERE.bat` (oder `./setup.sh`). Es startet sofort und lädt nichts doppelt herunter.
|
||||
Schließ sein Fenster, um das Modell zu beenden. `UPDATE.bat` (`./update.sh`) aktualisiert Strata, ohne es zu starten.
|
||||
Aktualisieren, Docker, mehrere Karten, wohin die Dateien kommen und alle Optionen: [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
Um deinen PC zu messen, führe `BENCH.bat` (`./bench.sh`) aus. Das dauert 5-10 Minuten und schreibt einen Bericht, den du teilen
|
||||
kannst ([so geht's](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
|
||||
|
||||
**Optional: mehr Tempo auf einer Karte, wenn das ganze Modell in den RAM passt.** Zwei Einstellungen zum Einschalten von
|
||||
RubenTorney (#1904, #1905). Sie helfen, wenn du eine Grafikkarte nutzt und dein RAM alle Experten aufnimmt, zum Beispiel bei
|
||||
Q2_0 oder IQ3_XXS auf einem PC mit 64 GB. Öffne `strata-<model>.json` im Strata-Datenordner ([wo](docs/INSTALL.md#where-things-are-stored)) und ergänze:
|
||||
|
||||
- `"--adapt-async", "1"` in der Liste `"args"`. Der Expertencache wird dann in einem Hilfsthread aktualisiert, statt die
|
||||
Antwort anzuhalten. Gemessen auf einer RTX 5070 (Windows, Q2_0): +4-5 % beim Decodieren, +7-10 % bei Code und langen
|
||||
Dokumenten. Die Antworten können sich leicht von der Standardeinstellung unterscheiden und bleiben von Lauf zu Lauf gleich.
|
||||
- `"STRATA_PIN_BY_NEED": "1"` im Block `"env"` (Windows). Windows kann nur etwa die Hälfte des RAM für die Grafikkarte sperren,
|
||||
und diese Einstellung sperrt zuerst die Experten, die die Karte am dringendsten braucht. Gemessen auf der RTX 5070: +4,4 % beim Lesen des Prompts.
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
Starte Strata nach der Änderung neu. Zum Rückgängigmachen entfernst du die beiden Einträge.
|
||||
Folge den Anweisungen, um ein Modell herunterzuladen und zu starten. [Terminal-Anleitung](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
</details>
|
||||
|
||||
## Wie funktioniert das?
|
||||
<p align="center">
|
||||
<img src="docs/media/terminal-monitor.png" width="900" alt="Strata-Terminal-App mit Modellaktivität, Tempo, Speicher und letzten Anfragen">
|
||||
<br><sub>Behalte Modell und Hardware im Blick, während es läuft.</sub>
|
||||
</p>
|
||||
|
||||
Modelle wie dieses laufen normalerweise auf Servern mit Hunderten Gigabyte Grafikspeicher. Deine Grafikkarte hat 12-24 GB.
|
||||
Strata bringt das Modell auf deinen PC, indem es die Arbeit auf den ganzen Rechner verteilt. Stell dir eine Bibliothek vor.
|
||||
<a id="build-from-source"></a>
|
||||
|
||||
- Das Modell ist ein Team aus 24.576 kleinen Spezialisten ("Experten"), wie 24.576 Nachschlagewerke. Für jedes Wort braucht es nur 10 davon.
|
||||
- **Deine Grafikkarte ist der Schreibtisch.** Er ist klein und schnell. Die etwa 4.000 am häufigsten genutzten Bücher liegen
|
||||
dort aufgeschlagen.
|
||||
- **Dein RAM sind die Regale im Lesesaal.** Alle 24.576 Bücher stehen dort. Braucht ein Wort ein Buch, das nicht auf dem
|
||||
Schreibtisch liegt, holt dein Prozessor es sofort aus dem Regal, während die Karte weiterarbeitet.
|
||||
- **Deine SSD ist das Archiv.** Sie enthält eine Nachschlagetabelle von 29 GB. Das Modell liest pro Wort nur wenige Zeilen
|
||||
daraus, deshalb muss sie nicht im RAM liegen.
|
||||
[Aus dem Quellcode bauen](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [Vollständige Installationsanleitung](docs/INSTALL.md)
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Die 24.576 Experten des Modells: die meistgenutzten auf der Grafikkarte, alle im RAM, eine Nachschlagetabelle auf der SSD"></p>
|
||||
<a id="what-you-need"></a>
|
||||
|
||||
Ein kleiner Helfer rät außerdem die nächsten Wörter. Das große Modell prüft sie alle auf einmal und behält die richtigen. Du
|
||||
bekommst dieselbe Antwort, 1,6-1,8-mal früher. Lange Texte werden in großen Stücken gelesen, bis zu 8.192 Tokens auf einmal,
|
||||
mit über 1.000 Tokens pro Sekunde.
|
||||
## Hardware
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Ein kleiner Helfer rät die nächsten Wörter, das große Modell prüft sie alle auf einmal und behält die richtigen"></p>
|
||||
| Unterstützte Hardware | Anleitung |
|
||||
| --- | --- |
|
||||
| **NVIDIA GeForce RTX 20 / 30 / 40 / 50** | [NVIDIA-Setup](docs/INSTALL.md#what-you-need) |
|
||||
| **AMD Radeon RX 7700 / 7800 / 7900, RX 9060 / 9070, AI PRO R9700** | [AMD-Setup](docs/AMD_HIP.md) |
|
||||
| **AMD Radeon RX 6800 / 6900 · experimentell** | [RX-6000-Setup](docs/AMD_HIP.md#rdna2-gfx1030) |
|
||||
| **Intel Arc · Linux** | [Intel-Setup](docs/INTEL_ARC.md) |
|
||||
| **Apple Silicon · M1 oder neuer** | [Mac-Setup](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max · Strix Halo** | [Strix-Halo-Setup](docs/STRIX_HALO.md) |
|
||||
|
||||
Die ausführliche Erklärung: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Jeder Teil mit seinen Zahlen:
|
||||
[die Details](docs/DETAILS.md#how-it-works) und das [Paper](docs/paper/Strata-Paper.pdf).
|
||||
<a id="using-it"></a>
|
||||
|
||||
## Etwas ist schiefgegangen?
|
||||
## Tools
|
||||
|
||||
- **Mein PC ist beim ersten Start von Strata eingefroren.** Strata lädt das Modell in den RAM. Warte ab und schließe das
|
||||
Fenster nicht. Nach 10 Minuten immer noch eingefroren? Starte den PC neu, schließe andere Programme und versuch es noch
|
||||
einmal, oder wähle eine kleinere Größe.
|
||||
- **Es ist beim Herunterladen oder Installieren stehen geblieben.** Starte die App oder `strata launch` noch einmal. Es
|
||||
macht dort weiter, wo es aufgehört hat.
|
||||
- **Es ist sehr langsam, oder es meldet "the engine stopped unexpectedly".** Dein PC hat nicht genug freien RAM. Schließe
|
||||
andere Programme (Browser brauchen viel) oder wähle eine kleinere Größe wie Q2_0 oder IQ2_XS.
|
||||
- **Es meldet, dass es keine Engine für diesen PC gibt.** Öffne in der App **Einstellungen, Engine** und drücke **Engine
|
||||
aktualisieren**. Führe im Terminal `strata update` aus.
|
||||
- **Es meldet, dass Port 8080 schon belegt ist.** Strata läuft bereits. Such das Fenster.
|
||||
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw**, und weitere über die OpenAI- und Anthropic-kompatiblen APIs.
|
||||
|
||||
Weitere Lösungen: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Du kommst nicht weiter? Öffne ein
|
||||
[Issue](https://github.com/Niko1221/Strata/issues) und hänge die Protokolle an: in der App unter **Einstellungen, Engine,
|
||||
Diagnose, Protokolle speichern**, im Terminal die Ausgabe von `strata doctor`. Ein Sicherheitsproblem meldest du bitte
|
||||
privat: [SECURITY.md](SECURITY.md).
|
||||
Führe **`strata connect`** aus, um die Einrichtung für dein Tool zu sehen. [Verbindungsanleitung](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect).
|
||||
|
||||
## Danksagung und Lizenz
|
||||
<a id="guides"></a>
|
||||
<a id="how-fast-is-it"></a>
|
||||
<a id="how-does-it-work"></a>
|
||||
<a id="something-went-wrong"></a>
|
||||
|
||||
Das Modell ist [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) vom Qwen-Team. Komprimiert wurde es von
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5), agentionai (Gyro)
|
||||
und Unsloth. ISTA-DASLab hat auch die Sets von DeepSeek V4.1-Flash gebaut. Strata nutzt Teile von
|
||||
[llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Alle Danksagungen: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits).
|
||||
Strata ist Open Source unter der [MIT-Lizenz](LICENSE). Einige Teile und alle Modelle haben eigene Lizenzen
|
||||
([welche](docs/HOW_IT_WORKS.md#license)).
|
||||
## Hilfe
|
||||
|
||||
## Mitmachen
|
||||
|
||||
Du hast einen Fehler gefunden oder willst eine Änderung oder einen Benchmark einsenden? Lies zuerst [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
Dort steht, was dazugehört, damit andere deine Arbeit nachprüfen können.
|
||||
[Alle Anleitungen](docs) · [Fehlerbehebung](docs/TROUBLESHOOTING.md) · [Fehler melden](https://github.com/Niko1221/Strata/issues/new/choose) ·
|
||||
[Benchmarks](docs/COMMUNITY_BENCHMARKS.md) · [So funktioniert es](docs/HOW_IT_WORKS.md) · [Mitmachen](CONTRIBUTING.md)
|
||||
|
||||
## Strata unterstützen
|
||||
|
||||
Strata ist kostenlos und steht unter der MIT-Lizenz. Ich baue es und teste es auf jeder Hardware, die ich bekommen kann. Wenn es auf deinem PC gut läuft, hilft mir ein Kaffee,
|
||||
weiter neue Karten zu testen und das zu beheben, was ihr meldet. Es besteht kein Druck. Strata zu nutzen und zu melden, was dir auffällt, hilft genauso viel.
|
||||
**Hilf mit, lokale KI für alle offen zu halten.** Ein Kaffee hilft, Entwicklung und Tests auf mehr Hardware zu finanzieren.
|
||||
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: Strata unterstützen" height="48"></a>
|
||||
|
||||
Du möchtest Strata sponsern? Schreib mir über [GitHub](https://github.com/Niko1221).
|
||||
[MIT-Lizenz](LICENSE) · [Danksagung und Modelllizenzen](docs/HOW_IT_WORKS.md#credits) · [Datenschutz](docs/SAFETY.md)
|
||||
|
||||
<a id="option-c-let-your-ai-set-it-up"></a>
|
||||
|
||||
<sub>Für KI-Tools und automatische Leser: <a href="AGENTS.md">technische Referenz</a> · <a href="docs/AI_SETUP.md">Setup-Anleitung</a>.</sub>
|
||||
|
||||
**Sponsoring, Hardware-Partnerschaften oder Zusammenarbeit:** [[email protected]](mailto:[email protected]).
|
||||
+122
-298
@@ -1,364 +1,188 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="340">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <b>Español</b> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<h3 align="center">Ejecuta un modelo de última generación en tu propio PC gaming</h3>
|
||||
|
||||
<p align="center">Compatible con NVIDIA, AMD, Intel · Windows, Linux, Mac · libre y de código abierto</p>
|
||||
<h3 align="center">IA potente en tu propio ordenador.</h3>
|
||||
<p align="center">Chatea, programa y trabaja con imágenes, en local.</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Última versión"></a>
|
||||
<a href="#modelos">Modelos</a> · <a href="#instalación"><b>Instalación</b></a> ·
|
||||
<a href="#hardware">Hardware</a> · <a href="#herramientas">Herramientas</a> ·
|
||||
<a href="#ayuda">Ayuda</a> · <a href="#apoya-strata">Apoyar</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Descargar Strata"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licencia MIT"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="Estrellas en GitHub"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=black" alt="Invítame a un café"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Apoya Strata en Buy Me a Coffee"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Un jardín con una pagoda de vóxeles que escribió el modelo de Strata, funcionando en el navegador"></a><br>
|
||||
<sub>Un jardín con una pagoda de vóxeles, hecho con un solo prompt en una RTX 5070 con Strata (IQ3_S, contexto de 128K) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">vídeo completo (49 s)</a></sub></p>
|
||||
<p align="center"><sub><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <b>Español</b> · <a href="README.pt-BR.md">Português</a> · <a href="README.pl.md">Polski</a></sub></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#instalación"><b>Instalación</b></a> ·
|
||||
<a href="#modelos">Modelos</a> ·
|
||||
<a href="#qué-velocidad-tiene">Velocidad</a> ·
|
||||
<a href="#uso">Uso</a> ·
|
||||
<a href="#cómo-funciona">Cómo funciona</a> ·
|
||||
<a href="#algo-ha-ido-mal">Ayuda</a> ·
|
||||
<a href="#apoya-strata">Apoyar</a>
|
||||
</p>
|
||||
**Gratis, de código abierto y tuyo para ejecutar.** Usa Strata en tu escritorio o con tus herramientas de programación favoritas. No necesitas cuenta.
|
||||
|
||||
Strata ejecuta **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, un modelo de 125 mil millones
|
||||
de parámetros que normalmente necesita un servidor, en un PC normal. Desde 0.2.4 también ejecuta **DeepSeek V4.1-Flash**
|
||||
en la Terminal. Conversa, escribe código, lee imágenes y funciona con tus aplicaciones y tus agentes de programación. Tus
|
||||
chats y tus archivos se quedan en tu PC.
|
||||
## Modelos
|
||||
|
||||
- **Funciona en un PC gaming.** Con 32 GB entre RAM y memoria gráfica ya puedes empezar.
|
||||
- **Funciona con tus herramientas.** Claude Code, Codex CLI, OpenCode y otros agentes se conectan por las API de OpenAI y de Anthropic.
|
||||
- **Lee imágenes**, activado por defecto.
|
||||
- **Gratis y privado.** Licencia MIT, sin cuenta, sin nube, sin telemetría ([qué descarga y cuándo](docs/SAFETY.md)).
|
||||
Recomendamos **primero ISTA-DASLab**, después **Unsloth** y otros proveedores, siempre que haya un modelo compatible que quepa en tu memoria.
|
||||
|
||||
## Lo que necesitas
|
||||
<details>
|
||||
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> <b>Qwen3.8-Flash-Next</b></summary>
|
||||
|
||||
**Necesitas al menos 32 GB de memoria: RAM y memoria gráfica (VRAM) juntas.** En un Mac o un Ryzen AI Max, es la memoria
|
||||
unificada. Un PC de 32 GB con una tarjeta de 12 a 16 GB ejecuta los tamaños más pequeños, y con 64 GB funcionan todos los
|
||||
tamaños.
|
||||
|
||||
| Funciona en | Guía |
|
||||
| Proveedor | Opciones |
|
||||
| --- | --- |
|
||||
| **NVIDIA** GeForce RTX serie 20, 30, 40 y 50, 12 GB o más | [Instalación](#instalación) |
|
||||
| **AMD** Radeon RX 6800 y posteriores, Radeon AI PRO R9700 | [Tarjetas AMD](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md) |
|
||||
| **Mac con Apple Silicon**, M1 o posterior, macOS 15 o posterior | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** y otros PC con memoria unificada | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Tarjetas y procesadores antiguos | [GPU antiguas](docs/OLDER_GPUS.md) |
|
||||
| **ISTA-DASLab · recomendado** | Q2\_0, IQ2\_XS, IQ3\_XXS, IQ3\_S. [Coder: IQ1\_M](docs/MODELS.md#coder) para programar. |
|
||||
| **Unsloth** | UD-IQ4\_XS, UD-Q4\_K\_XL, UD-Q5\_K\_XL, UD-Q6\_K\_XL, Q8\_0 |
|
||||
| **Gyro · agentionai** | [Gyro-S, Gyro-M](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF). Usa menos memoria, pero es más lento. |
|
||||
| **Otras opciones** | [Swift 1.5](docs/MODELS.md#swift-15), [NVFP4](docs/NVFP4.md), [OrcaRouter](docs/ORCA.md) |
|
||||
|
||||
También necesitas unos 80 GB de disco libre (un SSD), Windows 10 / 11, Linux o macOS, y un controlador gráfico actual. Dos
|
||||
o tres tarjetas de la misma marca pueden compartir un modelo ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
[Compara tamaños, necesidades de memoria y compatibilidad](docs/MODELS.md).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> <b>Qwen3.6-35B-A3B</b></summary>
|
||||
|
||||
**Unsloth:** UD-Q4_K_XL, UD-Q6_K_XL, Q8_0. Son los paquetes documentados actualmente para este modelo.
|
||||
|
||||
[Guía del modelo y compatibilidad](docs/QWEN36.md).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><img src="docs/media/models/deepseek.svg" width="22" alt="" align="middle"> <b>DeepSeekV4.1-Flash-1017</b></summary>
|
||||
|
||||
En la **aplicación de escritorio** y en el **Terminal**. Lo que no cabe en la memoria se lee desde tu SSD, así que hace falta mucho espacio libre en disco.
|
||||
|
||||
| Conjunto (IST-DAS) | Descarga | Espacio libre necesario |
|
||||
| --- | ---: | ---: |
|
||||
| 1.75 bpw | 239 GB | unos 245 GB |
|
||||
| 2.00 bpw | 257 GB | unos 265 GB |
|
||||
| 2.25 bpw | 274 GB | unos 280 GB |
|
||||
|
||||
Funciona en NVIDIA RTX 30, 40 y 50 (Windows y Linux) y en AMD Radeon AI PRO R9700 (Linux). La instalación descarga los archivos y el motor de DeepSeek, y después inicia el modelo.
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
```
|
||||
|
||||
Medido con el conjunto de 2.00 bpw en Strata 0.2.5. «Salida» es la velocidad de la respuesta; «Prompt» es la rapidez con la que Strata lee tu texto.
|
||||
|
||||
| Hardware | Salida | Prompt |
|
||||
| --- | ---: | ---: |
|
||||
| RTX 5070 12 GB, 64 GB de RAM | 4 a 6 tokens/s | unos 110 tokens/s (4K) |
|
||||
| RTX 3060 12 GB, 46 GB de RAM | 9 a 11 tokens/s | unos 155 tokens/s (4K) |
|
||||
| 1x Radeon AI PRO R9700, 251 GB de RAM | 21 a 25 tokens/s | 320 a 360 tokens/s (8K) |
|
||||
| 4x Radeon AI PRO R9700 | 74 a 77 tokens/s | 400 a 450 tokens/s (8K) |
|
||||
|
||||
[Memoria, disco y detalles](docs/DEEPSEEK41.md) · [Notas de la versión](https://github.com/Niko1221/Strata/releases)
|
||||
|
||||
</details>
|
||||
|
||||
<a id="the-other-sizes"></a>
|
||||
|
||||
¿No sabes qué cabe? [Elige un modelo para tu ordenador](docs/MODELS.md#pick-by-memory).
|
||||
|
||||
## Instalación
|
||||
|
||||
### 1. La aplicación Strata (la más fácil)
|
||||
<a id="option-a-the-strata-app"></a>
|
||||
|
||||
Descarga el archivo para tu ordenador, ábrelo y pulsa Instalar en el modelo que recomienda.
|
||||
### Aplicación de escritorio
|
||||
|
||||
**Windows 10 / 11**
|
||||
**[Descargar Strata para Windows, Linux o Mac →](https://github.com/Niko1221/Strata/releases/latest)**
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-windows-x64-setup.exe)
|
||||
Instala la aplicación, elige un modelo y empieza a chatear. Hay un instalador por sistema. En el primer arranque, la aplicación descarga el motor para tu tarjeta gráfica. [Ayuda con el instalador](docs/INSTALL.md#the-app).
|
||||
|
||||
**Linux**
|
||||
<sub>Archivos: `Strata-0.2.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-linux-x86_64.AppImage)
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Aplicación de escritorio de Strata con un chat y una vista previa del código en vivo, una junto a otra">
|
||||
<br><sub>Chat, archivos y vista previa en vivo en una sola ventana.</sub>
|
||||
</p>
|
||||
|
||||
**Mac** (Apple Silicon, macOS 15 o posterior)
|
||||
<a id="option-b-the-terminal-one-line"></a>
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-macos-arm64.dmg)
|
||||
### Aplicación de Terminal
|
||||
|
||||
La aplicación elige sola el motor adecuado para tu tarjeta gráfica. Todos los archivos están en la
|
||||
[página de versiones](https://github.com/Niko1221/Strata/releases/latest).
|
||||
¿Prefieres la terminal? Instala Strata y ejecuta **`strata launch`**.
|
||||
|
||||
| Primer arranque | Qué hacer |
|
||||
| --- | --- |
|
||||
| **Windows** | Ejecuta el `.exe`. No necesita permisos de administrador. Si Windows muestra "Windows protegió tu PC", elige **Más información** y luego **Ejecutar de todas formas**. |
|
||||
| **Linux** | Haz el archivo ejecutable (`chmod +x`) y lánzalo. Necesita FUSE 2: `sudo apt install libfuse2t64` (Ubuntu 24.04 y posteriores) o `libfuse2` (22.04). |
|
||||
| **Mac** | Abre el `.dmg` y arrastra Strata a Aplicaciones. |
|
||||
<details open>
|
||||
<summary><b>Comandos de instalación</b></summary>
|
||||
|
||||
La descarga del modelo es grande (40-70 GB). Si se detiene, continúa donde se quedó.
|
||||
|
||||
### 2. La Terminal (una línea)
|
||||
|
||||
El comando `strata`, para scripts, servidores y agentes de IA. Es también la forma de ejecutar DeepSeek.
|
||||
|
||||
Linux y Mac:
|
||||
**Linux / Mac**
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
**Windows PowerShell**
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
Después arráncalo. Te hace una pregunta cada vez (modelo, contexto, imágenes, quién puede conectarse), descarga lo que
|
||||
necesita y empieza a servir:
|
||||
En Windows, abre una terminal nueva después. Luego:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
No necesita permisos de administrador. La instalación comprueba el SHA-256 de todo lo que descarga. En Windows, abre una
|
||||
ventana de terminal nueva después de la instalación. Todas las pantallas y opciones: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### 3. Que lo instale tu IA
|
||||
|
||||
Pega esto en Claude Code, Cursor, Codex, GitHub Copilot u otro asistente de programación:
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
El asistente revisa tu tarjeta gráfica, tu RAM y tu disco, elige el modelo adecuado, lo instala y lo arranca, y te explica
|
||||
cómo conectar tus aplicaciones. Las herramientas de IA también pueden instalar, arrancar y detener Strata mediante su
|
||||
[servidor MCP](docs/MCP_SERVER.md).
|
||||
|
||||
¿Prefieres compilarlo tú? Mira [Compilar desde el código fuente](#compilar-desde-el-código-fuente).
|
||||
|
||||
## Modelos
|
||||
|
||||
La aplicación y `strata launch` recomiendan un tamaño según tu memoria. Los tamaños pequeños son más rápidos. Los grandes
|
||||
son algo más inteligentes.
|
||||
|
||||
### ¿Poca memoria? Prueba Gyro
|
||||
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) de agentionai es la forma más pequeña de ejecutar
|
||||
Qwen3.8-Flash-Next. Funciona en NVIDIA, AMD, Intel Arc y Apple Silicon.
|
||||
|
||||
| Tamaño | Memoria que necesita (RAM + VRAM) | Descarga | Para qué sirve |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27,6 GB | 58,5 GB | PC donde no cabe nada más. 9 tokens/s en un Mac mini de 32 GB. |
|
||||
| **Gyro-M** | 35 GB | 92,0 GB | Calidad parecida a IQ3_XXS con un 20 % menos de memoria de GPU. |
|
||||
|
||||
Arráncalo con `strata launch --model gyro-s`. Detalles: [docs/GYRO.md](docs/GYRO.md).
|
||||
|
||||
### Los tamaños de Qwen3.8-Flash-Next
|
||||
|
||||
| Tu memoria (RAM + VRAM) | Elige | Por qué |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Gyro-S**, o el **[Coder](docs/MODELS.md#coder)** para código | El Coder conserva los expertos que usa el código |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB) o **Gyro-M** | Swift piensa menos tiempo antes de responder |
|
||||
| **48 GB** | **IQ3_XXS** (47,0 GB) | Más lento, mejor |
|
||||
| **64 GB** | **IQ3_S** (54,8 GB) | Iguala al modelo completo en las pruebas publicadas |
|
||||
| **96 GB o más** | **UD-IQ4_XS** hasta **Q8_0** de Unsloth | Lo más cerca del modelo completo |
|
||||
|
||||
Con menos RAM de la que indica una fila también puede funcionar, solo que más lento: Strata pasa solo a su
|
||||
[modo de poca RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). También hay:
|
||||
[NVFP4](docs/NVFP4.md) para tarjetas NVIDIA, [Qwen3.6-35B-A3B](docs/QWEN36.md) (más pequeño, varias peticiones a la vez) y
|
||||
[el Uncensored IQ3_XXS de OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs). Todos los tamaños y lo que cabe en cada equipo:
|
||||
[docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
### DeepSeek V4.1-Flash (Terminal, desde 0.2.4)
|
||||
|
||||
Un modelo mucho más grande que funciona con una sola tarjeta gráfica de 12 GB y de 46 a 64 GB de RAM. Strata mantiene en
|
||||
memoria las partes más usadas y lee el resto de tu SSD. [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF)
|
||||
hizo los conjuntos. Funciona en tarjetas NVIDIA RTX de las series 30, 40 y 50 y en la Radeon AI PRO R9700. Elígelo en
|
||||
`strata launch`. La aplicación de escritorio todavía no lo ofrece.
|
||||
|
||||
| Conjunto | Descarga | Calidad | Escribe código a |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M** (1,75 bits) | 239 GB | El más pequeño, más débil con prosa sencilla | aún sin medir en un PC |
|
||||
| **IQ2_XXS** (2.00 bits) | 257 GB | Buen equilibrio | 10 tokens/s (RTX 3060), 5 (RTX 5070), 48 (4x R9700) |
|
||||
| **IQ2_XS** (2.25 bits) | 274 GB | Lo más cerca del original | 15 tokens/s (una R9700), 80 (4x R9700) |
|
||||
| **IQ4_NL** (tabla Engram) | incluida en cada descarga de arriba | La tabla de consulta que comparten los tres conjuntos | |
|
||||
|
||||
Arráncalo con `strata launch --model ist-das-2.00bpw` (o `ist-das-1.75bpw`, `ist-das-2.25bpw`). Cifras de calidad,
|
||||
velocidades por PC e instalación manual: [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
|
||||
|
||||
## ¿Qué velocidad tiene?
|
||||
|
||||
Medido con una tarjeta gráfica de 12 GB (RTX 5070, 64 GB de RAM). Resultados con otras tarjetas:
|
||||
[velocidad de cada tamaño](docs/MODELS.md#how-fast-is-each-size) y [resultados de la comunidad](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
| Modelo | Las respuestas aparecen a | Lee tu prompt a |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████████` 94 tokens/s | `████████████` 2.650 tokens/s |
|
||||
| **IQ2_XS** | `██████████░░` 79 tokens/s | `█████████░░░` 2.090 tokens/s |
|
||||
| **IQ3_XXS** | `████████░░░░` 62 tokens/s | `████████░░░░` 1.750 tokens/s |
|
||||
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1.620 tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2.180 tokens/s |
|
||||
|
||||
Con una tarjeta AMD de 16 GB (RX 9070 XT, 47 GB de RAM):
|
||||
|
||||
| Modelo | Las respuestas aparecen a | Lee tu prompt a |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████░░░░` 60 tokens/s | `█████░░░░░░░` 1.160 tokens/s |
|
||||
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1.110 tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1.420 tokens/s |
|
||||
|
||||
Un token equivale a unos ¾ de palabra, así que 60 tokens por segundo es más rápido de lo que puedes leer. "Lee tu prompt"
|
||||
es lo rápido que Strata procesa un documento largo, código o un historial de chat.
|
||||
|
||||
## Uso
|
||||
|
||||
### En la aplicación
|
||||
|
||||
Un chat, un **Monitor** del modelo y de tu GPU, CPU y RAM, una página de **Modelos** y los **Ajustes**. El chat puede
|
||||
trabajar en una carpeta de tu PC. En modo **Manual** pregunta antes de cambiar un archivo o ejecutar un comando. Puede buscar
|
||||
en la web (DuckDuckGo o Bing, sin cuenta). Las palabras que buscas son lo único que sale de tu PC.
|
||||
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="La aplicación Strata: un chat donde el agente construyó un pequeño jardín con una pagoda de vóxeles, con la página abierta en la vista previa a la derecha"><br>
|
||||
<sub>El agente escribió un jardín con una pagoda de vóxeles en un solo archivo HTML, y la página se abre en la vista previa junto al chat</sub></p>
|
||||
|
||||
### En la Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="La vista general del monitor de la Terminal de Strata mientras el modelo escribe una respuesta"><br>
|
||||
<sub><code>strata monitor</code> mientras el modelo responde: velocidad, GPU, memoria y las últimas solicitudes</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve and print the address
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor # watch speed, GPU, memory and requests
|
||||
strata update # get the newest engine
|
||||
strata doctor # check the installed files
|
||||
```
|
||||
|
||||
Para llegar a Strata desde tu móvil u otro PC, usa `strata launch --access lan`. Crea una clave de API para ti.
|
||||
`strata --help` lo lista todo, y [docs/TERMINAL.md](docs/TERMINAL.md) lo explica.
|
||||
|
||||
### Con agentes de IA y otras aplicaciones
|
||||
|
||||
Un agente instala Strata con el comando de una línea de arriba y ejecuta `strata launch --model NAME --yes --no-tui`. Los
|
||||
pasos, las opciones y los códigos de salida están en [docs/AI_SETUP.md](docs/AI_SETUP.md). Las reglas del repositorio para agentes: [AGENTS.md](AGENTS.md).
|
||||
|
||||
| Tu aplicación habla | URL base |
|
||||
| --- | --- |
|
||||
| API de OpenAI (la mayoría de aplicaciones y agentes) | `http://127.0.0.1:8080/v1` (cualquier clave de API, cualquier nombre de modelo) |
|
||||
| API de Anthropic (Claude Code) | `http://127.0.0.1:8080`, sin `/v1`. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| API OpenAI Responses (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([configuración](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` muestra los ajustes exactos para OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent y OpenClaw,
|
||||
y `strata connect opencode --write` los escribe por ti (con una copia de seguridad).
|
||||
|
||||
### Conviene saber
|
||||
|
||||
- **Las imágenes** vienen activadas en todos los modelos que pueden leerlas. Adjunta o pega una imagen en el chat, o envíala desde tu aplicación. Detalles: [docs/IMAGES.md](docs/IMAGES.md).
|
||||
- **El razonamiento** puede estar desactivado, bajo, medio o alto, en el menú del chat o en "reasoning effort" de tu aplicación. Desactivado es lo más rápido.
|
||||
- **El primer mensaje** de un chat se lee entero, unos 1 minuto por cada 30.000 tokens. Los mensajes siguientes empiezan en segundos.
|
||||
- **Una petición a la vez** es lo normal. Para varias, usa `--parallel N` ([BATCHING.md](docs/BATCHING.md)). En una tarjeta de 12 GB, esto hace más lenta cada respuesta.
|
||||
|
||||
## Compilar desde el código fuente
|
||||
|
||||
<details>
|
||||
<summary>Windows, Linux y Mac desde el repositorio</summary>
|
||||
|
||||
[Descarga Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) y descomprímelo, o haz `git clone`.
|
||||
**Windows:** haz doble clic en `START-HERE.bat`. **Linux:** ejecuta `./setup.sh` en la carpeta de Strata.
|
||||
|
||||
**Mac (Apple Silicon, macOS 15 o posterior):** instala una vez las Command Line Tools de Apple con `xcode-select --install`
|
||||
(no hace falta Xcode). Abre la Terminal en la carpeta de Strata y ejecuta `./setup.sh`. El script compila el motor para Mac
|
||||
en tu Mac (unos 7 minutos), hace las mismas preguntas, descarga el modelo y lo arranca. La próxima vez, `./setup.sh` lo
|
||||
arranca enseguida. Qué tamaño cabe en tu Mac: [docs/MAC.md](docs/MAC.md).
|
||||
|
||||
El instalador detecta tu tarjeta y prepara el motor adecuado. Pregunta qué modelo y qué tamaño quieres, cuánto contexto
|
||||
(cuánto texto mantiene el modelo en mente) y si debe leer imágenes. Pulsa Enter para aceptar la respuesta recomendada.
|
||||
Luego descarga el modelo (unos 70 GB) y lo arranca. Si la descarga se detiene, vuelve a ejecutar el script y continúa. Tu
|
||||
navegador abre la página de Strata en `http://127.0.0.1:8080`.
|
||||
|
||||
> Mientras el modelo arranca, tu PC puede ir lento o dejar de responder durante 1-3 minutos (más tiempo la primera vez).
|
||||
> Strata carga 35-55 GB en tu RAM y bloquea una parte para la tarjeta gráfica. Espera y no cierres la ventana.
|
||||
|
||||
La próxima vez, ejecuta de nuevo `START-HERE.bat` (o `./setup.sh`). Arranca enseguida y no descarga nada dos veces. Cierra
|
||||
su ventana para detener el modelo. `UPDATE.bat` (`./update.sh`) actualiza Strata sin arrancarlo. Actualización, Docker,
|
||||
varias tarjetas, dónde van los archivos y todas las opciones: [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
Para medir tu PC, ejecuta `BENCH.bat` (`./bench.sh`). Tarda 5-10 minutos y escribe un informe que puedes compartir
|
||||
([cómo](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
|
||||
|
||||
**Opcional: más velocidad en una tarjeta cuando todo el modelo cabe en la RAM.** Dos ajustes que se activan a mano, de
|
||||
RubenTorney (#1904, #1905). Ayudan cuando usas una sola tarjeta gráfica y tu RAM guarda todos los expertos, por ejemplo con
|
||||
Q2_0 o IQ3_XXS en un PC de 64 GB. Abre `strata-<model>.json` en la carpeta de datos de Strata ([dónde](docs/INSTALL.md#where-things-are-stored)) y añade:
|
||||
|
||||
- `"--adapt-async", "1"` a la lista `"args"`. Así la caché de expertos se actualiza en un hilo auxiliar en vez de detener la
|
||||
respuesta. Medido en una RTX 5070 (Windows, Q2_0): +4-5 % en decodificación, +7-10 % en código y documentos largos. Las
|
||||
respuestas pueden diferir un poco de las del ajuste por defecto y son iguales de una ejecución a otra.
|
||||
- `"STRATA_PIN_BY_NEED": "1"` en el bloque `"env"` (Windows). Windows solo puede bloquear aproximadamente la mitad de la RAM
|
||||
para la tarjeta gráfica, y este ajuste bloquea primero los expertos que la tarjeta necesita antes. Medido en la RTX 5070: +4,4 % en lectura del prompt.
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
Reinicia Strata después del cambio. Para deshacerlo, quita las dos entradas.
|
||||
Sigue las indicaciones para descargar e iniciar un modelo. [Guía del Terminal](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
</details>
|
||||
|
||||
## ¿Cómo funciona?
|
||||
<p align="center">
|
||||
<img src="docs/media/terminal-monitor.png" width="900" alt="Aplicación de Terminal de Strata con la actividad del modelo, la velocidad, la memoria y las peticiones recientes">
|
||||
<br><sub>Observa tu modelo y tu hardware mientras se ejecuta.</sub>
|
||||
</p>
|
||||
|
||||
Los modelos como este suelen ejecutarse en servidores con cientos de gigabytes de memoria gráfica. Tu tarjeta gráfica tiene
|
||||
12-24 GB. Strata hace que el modelo quepa repartiendo el trabajo por todo tu PC. Imagina una biblioteca.
|
||||
<a id="build-from-source"></a>
|
||||
|
||||
- El modelo es un equipo de 24.576 pequeños especialistas ("expertos"), como 24.576 libros de consulta. Cada palabra necesita solo 10 de ellos.
|
||||
- **Tu tarjeta gráfica es el escritorio.** Es pequeño y rápido. Los unos 4.000 libros que más se usan están abiertos encima.
|
||||
- **Tu RAM son las estanterías de la sala de lectura.** Los 24.576 libros están ahí. Cuando una palabra necesita un libro que
|
||||
no está en el escritorio, tu procesador lo coge enseguida de la estantería mientras la tarjeta sigue trabajando.
|
||||
- **Tu SSD es el archivo.** Guarda una tabla de consulta de 29 GB. El modelo lee unas pocas filas por palabra, así que no
|
||||
hace falta que esté en la RAM.
|
||||
[Compilar desde el código fuente](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [Guía de instalación completa](docs/INSTALL.md)
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Los 24.576 expertos del modelo: los más usados en la tarjeta gráfica, todos en la RAM, una tabla de consulta en el SSD"></p>
|
||||
<a id="what-you-need"></a>
|
||||
|
||||
Un pequeño ayudante también adivina las siguientes palabras. El modelo grande las comprueba todas a la vez y se queda con
|
||||
las correctas. Obtienes la misma respuesta, entre 1,6 y 1,8 veces antes. Los textos largos se leen en trozos grandes, de
|
||||
hasta 8.192 tokens a la vez, a más de 1.000 tokens por segundo.
|
||||
## Hardware
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Un pequeño ayudante adivina las siguientes palabras; el modelo grande las comprueba todas a la vez y se queda con las correctas"></p>
|
||||
| Hardware compatible | Guía |
|
||||
| --- | --- |
|
||||
| **NVIDIA GeForce RTX 20 / 30 / 40 / 50** | [Instalación en NVIDIA](docs/INSTALL.md#what-you-need) |
|
||||
| **AMD Radeon RX 7700 / 7800 / 7900, RX 9060 / 9070, AI PRO R9700** | [Instalación en AMD](docs/AMD_HIP.md) |
|
||||
| **AMD Radeon RX 6800 / 6900 · experimental** | [Instalación en RX 6000](docs/AMD_HIP.md#rdna2-gfx1030) |
|
||||
| **Intel Arc · Linux** | [Instalación en Intel](docs/INTEL_ARC.md) |
|
||||
| **Apple Silicon · M1 o posterior** | [Instalación en Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max · Strix Halo** | [Instalación en Strix Halo](docs/STRIX_HALO.md) |
|
||||
|
||||
La explicación más larga: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte con sus cifras:
|
||||
[los detalles](docs/DETAILS.md#how-it-works) y el [artículo](docs/paper/Strata-Paper.pdf).
|
||||
<a id="using-it"></a>
|
||||
|
||||
## ¿Algo ha ido mal?
|
||||
## Herramientas
|
||||
|
||||
- **Mi PC se congeló la primera vez que arrancó Strata.** Carga el modelo en la RAM. Espera y no cierres la ventana. ¿Sigue
|
||||
congelado después de 10 minutos? Reinicia el PC, cierra otros programas e inténtalo de nuevo, o elige un tamaño más pequeño.
|
||||
- **Se detuvo durante la descarga o la instalación.** Abre de nuevo la aplicación o `strata launch`. Continúa donde se
|
||||
quedó.
|
||||
- **Va muy lento, o dice "the engine stopped unexpectedly".** Tu PC no tiene bastante RAM libre. Cierra otros programas
|
||||
(los navegadores gastan mucha) o elige un tamaño más pequeño, como Q2_0 o IQ2_XS.
|
||||
- **Dice que no hay motor para este PC.** En la aplicación, abre **Ajustes, Motor** y pulsa **Actualizar motor**. En la
|
||||
Terminal, ejecuta `strata update`.
|
||||
- **Dice que el puerto 8080 ya está en uso.** Strata ya se está ejecutando. Busca su ventana.
|
||||
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw**, y más, mediante las API compatibles con OpenAI y Anthropic.
|
||||
|
||||
Más soluciones: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). ¿Sigues atascado? Abre una
|
||||
[incidencia](https://github.com/Niko1221/Strata/issues) y adjunta los registros: en la aplicación desde **Ajustes, Motor,
|
||||
Diagnóstico, Guardar registros**, en la Terminal la salida de `strata doctor`. Para un problema de seguridad, avísanos en
|
||||
privado: [SECURITY.md](SECURITY.md).
|
||||
Ejecuta **`strata connect`** para ver las instrucciones de tu herramienta. [Guía de conexión](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect).
|
||||
|
||||
## Créditos y licencia
|
||||
<a id="guides"></a>
|
||||
<a id="how-fast-is-it"></a>
|
||||
<a id="how-does-it-work"></a>
|
||||
<a id="something-went-wrong"></a>
|
||||
|
||||
El modelo es [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), del equipo de Qwen. Lo comprimieron
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5), agentionai (Gyro)
|
||||
y Unsloth. ISTA-DASLab hizo también los conjuntos de DeepSeek V4.1-Flash. Strata usa partes de
|
||||
[llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Todos los créditos: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits).
|
||||
Strata es de código abierto con la [licencia MIT](LICENSE). Algunas partes y todos los modelos tienen sus propias licencias
|
||||
([cuáles](docs/HOW_IT_WORKS.md#license)).
|
||||
## Ayuda
|
||||
|
||||
## Contribuir
|
||||
|
||||
¿Has encontrado un error, o quieres enviar un cambio o un benchmark? Lee primero [CONTRIBUTING.md](CONTRIBUTING.md). Explica
|
||||
qué incluir para que otros puedan comprobar tu trabajo.
|
||||
[Todas las guías](docs) · [Solución de problemas](docs/TROUBLESHOOTING.md) · [Informar de un error](https://github.com/Niko1221/Strata/issues/new/choose) ·
|
||||
[Benchmarks](docs/COMMUNITY_BENCHMARKS.md) · [Cómo funciona](docs/HOW_IT_WORKS.md) · [Contribuir](CONTRIBUTING.md)
|
||||
|
||||
## Apoya Strata
|
||||
|
||||
Strata es gratis y tiene licencia MIT. Lo construyo y lo pruebo en el hardware que consigo. Si te funciona bien en tu PC, un café me ayuda
|
||||
a seguir probando tarjetas nuevas y a arreglar lo que nos cuentas. Sin presión. Usarlo y contar lo que encuentras ayuda igual de mucho.
|
||||
**Ayuda a mantener la IA local abierta a todos.** Un café ayuda a financiar el desarrollo y las pruebas en más hardware.
|
||||
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: apoya Strata" height="48"></a>
|
||||
|
||||
¿Te interesa patrocinar Strata? Escríbeme a través de [GitHub](https://github.com/Niko1221).
|
||||
[Licencia MIT](LICENSE) · [Créditos y licencias de los modelos](docs/HOW_IT_WORKS.md#credits) · [Privacidad](docs/SAFETY.md)
|
||||
|
||||
<a id="option-c-let-your-ai-set-it-up"></a>
|
||||
|
||||
<sub>Para herramientas de IA y lectores automáticos: <a href="AGENTS.md">referencia técnica</a> · <a href="docs/AI_SETUP.md">instrucciones de instalación</a>.</sub>
|
||||
|
||||
**Patrocinios, alianzas de hardware o colaboraciones:** [[email protected]](mailto:[email protected]).
|
||||
+122
-301
@@ -1,367 +1,188 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="340">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <b>Français</b> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<h3 align="center">Faites tourner un modèle de pointe sur votre propre PC de jeu</h3>
|
||||
|
||||
<p align="center">Compatible avec NVIDIA, AMD, Intel · Windows, Linux, Mac · gratuit et open source</p>
|
||||
<h3 align="center">Une IA puissante sur votre propre ordinateur.</h3>
|
||||
<p align="center">Discutez, programmez et travaillez avec des images, en local.</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Dernière version"></a>
|
||||
<a href="#modèles">Modèles</a> · <a href="#installation"><b>Installation</b></a> ·
|
||||
<a href="#matériel">Matériel</a> · <a href="#outils">Outils</a> ·
|
||||
<a href="#aide">Aide</a> · <a href="#soutenir-strata">Soutenir</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Télécharger Strata"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licence MIT"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="Étoiles GitHub"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=black" alt="Offrez-moi un café"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Soutenir Strata sur Buy Me a Coffee"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Un jardin de pagode en voxels écrit par le modèle de Strata, qui tourne dans le navigateur"></a><br>
|
||||
<sub>Un jardin de pagode en voxels, généré en une seule requête sur une RTX 5070 avec Strata (IQ3_S, contexte de 128K) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">vidéo complète (49 s)</a></sub></p>
|
||||
<p align="center"><sub><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <b>Français</b> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a> · <a href="README.pl.md">Polski</a></sub></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#installation"><b>Installation</b></a> ·
|
||||
<a href="#modèles">Modèles</a> ·
|
||||
<a href="#quelle-est-sa-vitesse-">Vitesse</a> ·
|
||||
<a href="#utilisation">Utilisation</a> ·
|
||||
<a href="#comment-ça-marche-">Comment ça marche</a> ·
|
||||
<a href="#un-problème-">Aide</a> ·
|
||||
<a href="#soutenir-strata">Soutenir</a>
|
||||
</p>
|
||||
**Gratuit, open source, et à vous de l'exécuter.** Utilisez Strata sur votre bureau ou avec vos outils de programmation préférés. Aucun compte nécessaire.
|
||||
|
||||
Strata fait tourner **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, un modèle de 125 milliards
|
||||
de paramètres qui demande normalement un serveur, sur un PC ordinaire. Depuis la 0.2.4, il fait aussi tourner **DeepSeek
|
||||
V4.1-Flash** dans le Terminal. Il discute, écrit du code, lit des images et fonctionne avec vos applications et vos agents
|
||||
de code. Vos conversations et vos fichiers restent sur votre PC.
|
||||
## Modèles
|
||||
|
||||
- **Tourne sur un PC de jeu.** 32 Go de RAM et de mémoire graphique réunis suffisent pour commencer.
|
||||
- **Fonctionne avec vos outils.** Claude Code, Codex CLI, OpenCode et d'autres agents se connectent par les API d'OpenAI et d'Anthropic.
|
||||
- **Lit les images**, activé par défaut.
|
||||
- **Gratuit et privé.** Licence MIT, pas de compte, pas de cloud, aucune télémétrie ([ce qu'il télécharge et quand](docs/SAFETY.md)).
|
||||
Nous recommandons **d'abord ISTA-DASLab**, puis **Unsloth** et d'autres fournisseurs, lorsqu'un modèle compatible tient en mémoire.
|
||||
|
||||
## Ce qu'il vous faut
|
||||
<details>
|
||||
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> <b>Qwen3.8-Flash-Next</b></summary>
|
||||
|
||||
**Il vous faut au moins 32 Go de mémoire : RAM et mémoire graphique (VRAM) réunis.** Sur un Mac ou un Ryzen AI Max, c'est
|
||||
la mémoire unifiée. Un PC de 32 Go avec une carte de 12 à 16 Go fait tourner les petites tailles, et 64 Go font tourner
|
||||
toutes les tailles.
|
||||
|
||||
| Fonctionne sur | Guide |
|
||||
| Fournisseur | Choix |
|
||||
| --- | --- |
|
||||
| **NVIDIA** GeForce RTX série 20, 30, 40 et 50, 12 Go ou plus | [Installation](#installation) |
|
||||
| **AMD** Radeon RX 6800 et plus récent, Radeon AI PRO R9700 | [Cartes AMD](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md) |
|
||||
| **Mac Apple Silicon**, M1 ou plus récent, macOS 15 ou plus récent | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** et autres PC à mémoire unifiée | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Cartes et processeurs plus anciens | [GPU anciens](docs/OLDER_GPUS.md) |
|
||||
| **ISTA-DASLab · recommandé** | Q2\_0, IQ2\_XS, IQ3\_XXS, IQ3\_S. [Coder : IQ1\_M](docs/MODELS.md#coder) pour programmer. |
|
||||
| **Unsloth** | UD-IQ4\_XS, UD-Q4\_K\_XL, UD-Q5\_K\_XL, UD-Q6\_K\_XL, Q8\_0 |
|
||||
| **Gyro · agentionai** | [Gyro-S, Gyro-M](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF). Utilise moins de mémoire, mais plus lent. |
|
||||
| **Autres options** | [Swift 1.5](docs/MODELS.md#swift-15), [NVFP4](docs/NVFP4.md), [OrcaRouter](docs/ORCA.md) |
|
||||
|
||||
Il vous faut aussi environ 80 Go d'espace disque libre (un SSD), Windows 10 / 11, Linux ou macOS, et un pilote graphique
|
||||
récent. Deux ou trois cartes de la même marque peuvent se partager un modèle ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
[Comparer les tailles, la mémoire nécessaire et la compatibilité](docs/MODELS.md).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> <b>Qwen3.6-35B-A3B</b></summary>
|
||||
|
||||
**Unsloth:** UD-Q4_K_XL, UD-Q6_K_XL, Q8_0. Ce sont les paquets actuellement documentés pour ce modèle.
|
||||
|
||||
[Guide du modèle et compatibilité](docs/QWEN36.md).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><img src="docs/media/models/deepseek.svg" width="22" alt="" align="middle"> <b>DeepSeekV4.1-Flash-1017</b></summary>
|
||||
|
||||
Dans l'**application de bureau** et dans le **Terminal**. Ce qui ne tient pas en mémoire est lu depuis votre SSD : il faut donc beaucoup d'espace disque libre.
|
||||
|
||||
| Jeu (IST-DAS) | Téléchargement | Espace disque libre nécessaire |
|
||||
| --- | ---: | ---: |
|
||||
| 1.75 bpw | 239 GB | environ 245 GB |
|
||||
| 2.00 bpw | 257 GB | environ 265 GB |
|
||||
| 2.25 bpw | 274 GB | environ 280 GB |
|
||||
|
||||
Fonctionne sur les NVIDIA RTX 30, 40 et 50 (Windows et Linux) et sur l'AMD Radeon AI PRO R9700 (Linux). L'installation télécharge les fichiers et le moteur DeepSeek, puis démarre le modèle.
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
```
|
||||
|
||||
Mesuré avec le jeu 2.00 bpw sur Strata 0.2.5. « Sortie » est la vitesse de la réponse, « Prompt » la vitesse à laquelle Strata lit votre texte.
|
||||
|
||||
| Matériel | Sortie | Prompt |
|
||||
| --- | ---: | ---: |
|
||||
| RTX 5070 12 GB, 64 GB de RAM | 4 à 6 tokens/s | environ 110 tokens/s (4K) |
|
||||
| RTX 3060 12 GB, 46 GB de RAM | 9 à 11 tokens/s | environ 155 tokens/s (4K) |
|
||||
| 1x Radeon AI PRO R9700, 251 GB de RAM | 21 à 25 tokens/s | 320 à 360 tokens/s (8K) |
|
||||
| 4x Radeon AI PRO R9700 | 74 à 77 tokens/s | 400 à 450 tokens/s (8K) |
|
||||
|
||||
[Mémoire, disque et détails](docs/DEEPSEEK41.md) · [Notes de version](https://github.com/Niko1221/Strata/releases)
|
||||
|
||||
</details>
|
||||
|
||||
<a id="the-other-sizes"></a>
|
||||
|
||||
Vous ne savez pas ce qui tient ? [Choisir un modèle pour votre ordinateur](docs/MODELS.md#pick-by-memory).
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. L'application Strata (le plus simple)
|
||||
<a id="option-a-the-strata-app"></a>
|
||||
|
||||
Téléchargez le fichier pour votre ordinateur, ouvrez-le et appuyez sur Installer pour le modèle qu'elle recommande.
|
||||
### Application de bureau
|
||||
|
||||
**Windows 10 / 11**
|
||||
**[Télécharger Strata pour Windows, Linux ou Mac →](https://github.com/Niko1221/Strata/releases/latest)**
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-windows-x64-setup.exe)
|
||||
Installez l'application, choisissez un modèle et commencez à discuter. Il y a un seul installateur par système. Au premier lancement, l'application télécharge le moteur de votre carte graphique. [Aide pour l'installateur](docs/INSTALL.md#the-app).
|
||||
|
||||
**Linux**
|
||||
<sub>Fichiers: `Strata-0.2.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-linux-x86_64.AppImage)
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Application de bureau Strata avec une discussion et un aperçu du code en direct côte à côte">
|
||||
<br><sub>Discussion, fichiers et aperçu en direct dans une seule fenêtre.</sub>
|
||||
</p>
|
||||
|
||||
**Mac** (Apple Silicon, macOS 15 ou plus récent)
|
||||
<a id="option-b-the-terminal-one-line"></a>
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-macos-arm64.dmg)
|
||||
### Application Terminal
|
||||
|
||||
L'application choisit toute seule le bon moteur pour votre carte graphique. Tous les fichiers sont sur la
|
||||
[page des versions](https://github.com/Niko1221/Strata/releases/latest).
|
||||
Vous préférez le terminal ? Installez Strata, puis lancez **`strata launch`**.
|
||||
|
||||
| Premier démarrage | Ce qu'il faut faire |
|
||||
| --- | --- |
|
||||
| **Windows** | Lancez le `.exe`. Pas besoin de droits d'administrateur. Si Windows affiche « Windows a protégé votre ordinateur », choisissez **Informations complémentaires**, puis **Exécuter quand même**. |
|
||||
| **Linux** | Rendez le fichier exécutable (`chmod +x`) et lancez-le. Il a besoin de FUSE 2 : `sudo apt install libfuse2t64` (Ubuntu 24.04 et plus récent) ou `libfuse2` (22.04). |
|
||||
| **Mac** | Ouvrez le `.dmg` et faites glisser Strata dans Applications. |
|
||||
<details open>
|
||||
<summary><b>Commandes d'installation</b></summary>
|
||||
|
||||
Le téléchargement du modèle est gros (40 à 70 Go). S'il s'arrête, il reprend là où il s'était arrêté.
|
||||
|
||||
### 2. Le Terminal (une ligne)
|
||||
|
||||
La commande `strata`, pour les scripts, les serveurs et les agents d'IA. C'est aussi le moyen de faire tourner DeepSeek.
|
||||
|
||||
Linux et Mac :
|
||||
**Linux / Mac**
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows (PowerShell) :
|
||||
**Windows PowerShell**
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
Puis lancez-le. Il pose une question à la fois (modèle, contexte, images, qui peut se connecter), télécharge ce dont il a
|
||||
besoin et commence à servir :
|
||||
Sous Windows, ouvrez ensuite un nouveau terminal. Puis :
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
Pas besoin de droits d'administrateur. L'installation vérifie l'empreinte SHA-256 de tout ce qu'elle télécharge. Sous
|
||||
Windows, ouvrez une nouvelle fenêtre de terminal après l'installation. Tous les écrans et toutes les options : [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### 3. Laissez votre IA l'installer
|
||||
|
||||
Collez ceci dans Claude Code, Cursor, Codex, GitHub Copilot ou un autre assistant de programmation :
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
L'assistant vérifie votre carte graphique, votre RAM et votre disque, choisit le modèle adapté, l'installe, le démarre et
|
||||
vous explique comment connecter vos applications. Les outils d'IA peuvent aussi installer, démarrer et arrêter Strata via son
|
||||
[serveur MCP](docs/MCP_SERVER.md).
|
||||
|
||||
Vous préférez le compiler vous-même ? Voir [Compiler depuis les sources](#compiler-depuis-les-sources).
|
||||
|
||||
## Modèles
|
||||
|
||||
L'application et `strata launch` recommandent une taille selon votre mémoire. Les petites tailles sont plus rapides. Les
|
||||
grandes sont un peu plus intelligentes.
|
||||
|
||||
### Peu de mémoire ? Essayez Gyro
|
||||
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) d'agentionai est la façon la plus légère de faire tourner
|
||||
Qwen3.8-Flash-Next. Il fonctionne sur NVIDIA, AMD, Intel Arc et Apple Silicon.
|
||||
|
||||
| Taille | Mémoire nécessaire (RAM + VRAM) | Téléchargement | Pour quoi |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27,6 Go | 58,5 Go | Les PC où rien d'autre ne tient. 9 tokens/s sur un Mac mini de 32 Go. |
|
||||
| **Gyro-M** | 35 Go | 92,0 Go | À peu près la qualité d'IQ3_XXS avec 20 % de mémoire GPU en moins. |
|
||||
|
||||
Lancez-le avec `strata launch --model gyro-s`. Détails : [docs/GYRO.md](docs/GYRO.md).
|
||||
|
||||
### Les tailles de Qwen3.8-Flash-Next
|
||||
|
||||
| Votre mémoire (RAM + VRAM) | Prenez | Pourquoi |
|
||||
| --- | --- | --- |
|
||||
| **32 Go** | **Gyro-S**, ou le **[Coder](docs/MODELS.md#coder)** pour le code | Le Coder garde les experts dont le code a besoin |
|
||||
| **40 Go** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 Go) ou **Gyro-M** | Swift réfléchit moins longtemps avant de répondre |
|
||||
| **48 Go** | **IQ3_XXS** (47,0 Go) | Plus lent, meilleur |
|
||||
| **64 Go** | **IQ3_S** (54,8 Go) | Égale le modèle complet dans les tests publiés |
|
||||
| **96 Go ou plus** | **UD-IQ4_XS** à **Q8_0** d'Unsloth | Au plus près du modèle complet |
|
||||
|
||||
Avoir moins de RAM que ce qu'indique une ligne peut encore fonctionner, en plus lent : Strata passe tout seul en
|
||||
[mode peu de RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Il existe aussi :
|
||||
[NVFP4](docs/NVFP4.md) pour les cartes NVIDIA, [Qwen3.6-35B-A3B](docs/QWEN36.md) (plus petit, plusieurs requêtes à la fois) et
|
||||
[l'Uncensored IQ3_XXS d'OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs). Toutes les tailles et ce qui tient où :
|
||||
[docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
### DeepSeek V4.1-Flash (Terminal, depuis 0.2.4)
|
||||
|
||||
Un modèle bien plus gros, qui tourne sur une seule carte graphique de 12 Go avec 46 à 64 Go de RAM. Strata garde en mémoire
|
||||
les parties les plus utilisées et lit le reste sur votre SSD. [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF)
|
||||
a produit les versions. Il fonctionne sur les cartes NVIDIA RTX des séries 30, 40 et 50 et sur la Radeon AI PRO R9700. Choisissez-le
|
||||
dans `strata launch`. L'application de bureau ne le propose pas encore.
|
||||
|
||||
| Version | Téléchargement | Qualité | Écrit du code à |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M** (1,75 bit) | 239 Go | La plus petite, plus faible sur la prose simple | pas encore mesuré sur un PC |
|
||||
| **IQ2_XXS** (2.00 bits) | 257 Go | Bon équilibre | 10 tokens/s (RTX 3060), 5 (RTX 5070), 48 (4x R9700) |
|
||||
| **IQ2_XS** (2.25 bits) | 274 Go | Au plus près de l'original | 15 tokens/s (une R9700), 80 (4x R9700) |
|
||||
| **IQ4_NL** (table Engram) | compris dans chaque téléchargement ci-dessus | La table de consultation que les trois versions partagent | |
|
||||
|
||||
Lancez-le avec `strata launch --model ist-das-2.00bpw` (ou `ist-das-1.75bpw`, `ist-das-2.25bpw`). Chiffres de qualité,
|
||||
vitesses par PC et installation manuelle : [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
|
||||
|
||||
## Quelle est sa vitesse ?
|
||||
|
||||
Mesuré avec une carte graphique de 12 Go (RTX 5070, 64 Go de RAM). Voir les résultats d'autres cartes :
|
||||
[vitesse de chaque taille](docs/MODELS.md#how-fast-is-each-size) et [résultats de la communauté](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
| Modèle | Les réponses s'affichent à | Lit votre prompt à |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████████` 94 tokens/s | `████████████` 2 650 tokens/s |
|
||||
| **IQ2_XS** | `██████████░░` 79 tokens/s | `█████████░░░` 2 090 tokens/s |
|
||||
| **IQ3_XXS** | `████████░░░░` 62 tokens/s | `████████░░░░` 1 750 tokens/s |
|
||||
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1 620 tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2 180 tokens/s |
|
||||
|
||||
Avec une carte AMD de 16 Go (RX 9070 XT, 47 Go de RAM) :
|
||||
|
||||
| Modèle | Les réponses s'affichent à | Lit votre prompt à |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████░░░░` 60 tokens/s | `█████░░░░░░░` 1 160 tokens/s |
|
||||
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1 110 tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1 420 tokens/s |
|
||||
|
||||
Un token vaut environ ¾ de mot, donc 60 tokens par seconde, c'est plus rapide que votre lecture. « Lit votre prompt »
|
||||
indique la vitesse à laquelle Strata absorbe un long document, du code ou un historique de discussion.
|
||||
|
||||
## Utilisation
|
||||
|
||||
### Dans l'application
|
||||
|
||||
Un chat, un **Monitor** du modèle et de votre GPU, CPU et RAM, une page **Models** et les **Settings**. Le chat peut
|
||||
travailler dans un dossier de votre PC. En mode **Manual**, il demande avant de modifier un fichier ou d'exécuter une
|
||||
commande. Il peut chercher sur le web (DuckDuckGo ou Bing, sans compte). Les mots recherchés sont la seule chose qui quitte
|
||||
votre PC.
|
||||
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="L'application Strata : un chat où l'agent a construit un petit jardin de pagode en voxels, avec la page ouverte dans l'aperçu à droite"><br>
|
||||
<sub>L'agent a écrit un jardin de pagode en voxels dans un seul fichier HTML, et la page s'ouvre dans l'aperçu à côté du chat</sub></p>
|
||||
|
||||
### Dans le Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="La vue d'ensemble du moniteur du Terminal Strata pendant que le modèle écrit une réponse"><br>
|
||||
<sub><code>strata monitor</code> pendant que le modèle répond : vitesse, GPU, mémoire et les dernières requêtes</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve and print the address
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor # watch speed, GPU, memory and requests
|
||||
strata update # get the newest engine
|
||||
strata doctor # check the installed files
|
||||
```
|
||||
|
||||
Pour joindre Strata depuis votre téléphone ou un autre PC, utilisez `strata launch --access lan`. La commande crée une clé
|
||||
d'API pour vous. `strata --help` liste tout, et [docs/TERMINAL.md](docs/TERMINAL.md) l'explique.
|
||||
|
||||
### Avec des agents d'IA et d'autres applications
|
||||
|
||||
Un agent installe Strata avec la commande en une ligne ci-dessus et lance `strata launch --model NAME --yes --no-tui`. Les
|
||||
étapes, les options et les codes de sortie sont dans [docs/AI_SETUP.md](docs/AI_SETUP.md). Les règles du dépôt pour les agents : [AGENTS.md](AGENTS.md).
|
||||
|
||||
| Votre application parle | URL de base |
|
||||
| --- | --- |
|
||||
| API OpenAI (la plupart des applications et des agents) | `http://127.0.0.1:8080/v1` (n'importe quelle clé d'API, n'importe quel nom de modèle) |
|
||||
| API Anthropic (Claude Code) | `http://127.0.0.1:8080`, sans `/v1`. Claude Code : `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| API OpenAI Responses (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([configuration](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` affiche les réglages exacts pour OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent et OpenClaw,
|
||||
et `strata connect opencode --write` les écrit pour vous (avec une sauvegarde).
|
||||
|
||||
### Bon à savoir
|
||||
|
||||
- **Les images** sont activées pour tous les modèles qui savent les lire. Joignez ou collez une image dans le chat, ou envoyez-la depuis votre application. Détails : [docs/IMAGES.md](docs/IMAGES.md).
|
||||
- **La réflexion** peut être désactivée, faible, moyenne ou élevée, dans le menu du chat ou dans le réglage « reasoning effort » de votre application. Désactivée est le plus rapide.
|
||||
- **Le premier message** d'une discussion est lu en entier, environ 1 minute pour 30 000 tokens. Les messages suivants démarrent en quelques secondes.
|
||||
- **Une requête à la fois** par défaut. Pour en traiter plusieurs, utilisez `--parallel N` ([BATCHING.md](docs/BATCHING.md)). Sur une carte de 12 Go, chaque réponse devient alors plus lente.
|
||||
|
||||
## Compiler depuis les sources
|
||||
|
||||
<details>
|
||||
<summary>Windows, Linux et Mac depuis le dépôt</summary>
|
||||
|
||||
[Téléchargez Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) et décompressez-le, ou faites un `git clone`.
|
||||
**Windows :** double-cliquez sur `START-HERE.bat`. **Linux :** lancez `./setup.sh` dans le dossier Strata.
|
||||
|
||||
**Mac (Apple Silicon, macOS 15 ou plus récent) :** installez une fois les Command Line Tools d'Apple avec `xcode-select --install`
|
||||
(Xcode lui-même n'est pas nécessaire). Ouvrez le Terminal dans le dossier Strata et lancez `./setup.sh`. Le script compile
|
||||
le moteur Mac sur votre Mac (environ 7 minutes), pose les mêmes questions, télécharge le modèle et le démarre. La fois
|
||||
suivante, `./setup.sh` le démarre tout de suite. Quelle taille convient à votre Mac : [docs/MAC.md](docs/MAC.md).
|
||||
|
||||
L'installateur détecte votre carte et prépare le bon moteur. Il demande quel modèle et quelle taille, combien de contexte
|
||||
(la quantité de texte que le modèle garde en tête) et s'il doit lire les images. Appuyez sur Entrée pour la réponse
|
||||
recommandée. Puis il télécharge le modèle (environ 70 Go) et le démarre. Si le téléchargement s'arrête, relancez le script
|
||||
et il reprend. Votre navigateur ouvre la page de Strata à l'adresse `http://127.0.0.1:8080`.
|
||||
|
||||
> Pendant le démarrage du modèle, votre PC peut être lent ou ne plus répondre pendant 1 à 3 minutes (le plus longtemps la
|
||||
> première fois). Strata charge 35 à 55 Go dans votre RAM et en verrouille une partie pour la carte graphique. Patientez et
|
||||
> ne fermez pas la fenêtre.
|
||||
|
||||
La fois suivante, relancez `START-HERE.bat` (ou `./setup.sh`). Il démarre tout de suite et ne télécharge rien deux fois.
|
||||
Fermez sa fenêtre pour arrêter le modèle. `UPDATE.bat` (`./update.sh`) met Strata à jour sans le démarrer. Mise à jour,
|
||||
Docker, plusieurs cartes, où vont les fichiers et toutes les options : [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
Pour mesurer votre PC, lancez `BENCH.bat` (`./bench.sh`). Cela prend 5 à 10 minutes et produit un rapport que vous pouvez
|
||||
partager ([comment](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
|
||||
|
||||
**Facultatif : plus de vitesse sur une carte quand tout le modèle tient en RAM.** Deux réglages à activer soi-même, par
|
||||
RubenTorney (#1904, #1905). Ils aident quand vous utilisez une seule carte graphique et que votre RAM contient tous les
|
||||
experts, par exemple Q2_0 ou IQ3_XXS sur un PC de 64 Go. Ouvrez `strata-<model>.json` dans le dossier de données de Strata ([où](docs/INSTALL.md#where-things-are-stored)) et ajoutez :
|
||||
|
||||
- `"--adapt-async", "1"` à la liste `"args"`. Le cache des experts se met alors à jour sur un thread auxiliaire au lieu de
|
||||
mettre la réponse en pause. Mesuré sur une RTX 5070 (Windows, Q2_0) : +4 à 5 % en décodage, +7 à 10 % sur le code et les
|
||||
longs documents. Les réponses peuvent différer légèrement de celles du réglage par défaut, et restent identiques d'une exécution à l'autre.
|
||||
- `"STRATA_PIN_BY_NEED": "1"` dans le bloc `"env"` (Windows). Windows ne peut verrouiller qu'environ la moitié de la RAM
|
||||
pour la carte graphique, et ce réglage verrouille d'abord les experts dont la carte a le plus besoin. Mesuré sur la RTX 5070 : +4,4 % en lecture du prompt.
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
Redémarrez Strata après la modification. Pour annuler, supprimez les deux entrées.
|
||||
Suivez les invites pour télécharger et démarrer un modèle. [Guide du Terminal](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
</details>
|
||||
|
||||
## Comment ça marche ?
|
||||
<p align="center">
|
||||
<img src="docs/media/terminal-monitor.png" width="900" alt="Application Terminal de Strata affichant l'activité du modèle, la vitesse, la mémoire et les requêtes récentes">
|
||||
<br><sub>Suivez votre modèle et votre matériel pendant qu'il tourne.</sub>
|
||||
</p>
|
||||
|
||||
Les modèles de ce genre tournent d'habitude sur des serveurs qui disposent de centaines de gigaoctets de mémoire graphique.
|
||||
Votre carte graphique en a 12 à 24. Strata fait tenir le modèle en répartissant le travail sur tout votre PC. Imaginez une
|
||||
bibliothèque.
|
||||
<a id="build-from-source"></a>
|
||||
|
||||
- Le modèle est une équipe de 24 576 petits spécialistes (les « experts »), comme 24 576 ouvrages de référence. Chaque mot n'en demande que 10.
|
||||
- **Votre carte graphique est le bureau.** Il est petit et rapide. Les quelque 4 000 livres les plus utilisés y sont ouverts.
|
||||
- **Votre RAM, ce sont les étagères de la salle de lecture.** Les 24 576 livres s'y trouvent. Quand un mot demande un livre
|
||||
qui n'est pas sur le bureau, votre processeur va le chercher aussitôt sur l'étagère, pendant que la carte continue de travailler.
|
||||
- **Votre SSD, ce sont les archives.** Il contient une table de consultation de 29 Go. Le modèle n'en lit que quelques lignes par
|
||||
mot, elle n'a donc pas besoin de tenir en RAM.
|
||||
[Compiler depuis les sources](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [Guide d'installation complet](docs/INSTALL.md)
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Les 24 576 experts du modèle : les plus sollicités sur la carte graphique, tous dans la RAM, une table de consultation sur le SSD"></p>
|
||||
<a id="what-you-need"></a>
|
||||
|
||||
Un petit assistant devine aussi les mots suivants. Le grand modèle les vérifie tous d'un coup et garde les bons. Vous
|
||||
obtenez la même réponse, 1,6 à 1,8 fois plus tôt. Les textes longs sont lus par gros morceaux, jusqu'à 8 192 tokens à la
|
||||
fois, à plus de 1 000 tokens par seconde.
|
||||
## Matériel
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Un petit assistant devine les mots suivants ; le grand modèle les vérifie tous d'un coup et garde les bons"></p>
|
||||
| Matériel pris en charge | Guide |
|
||||
| --- | --- |
|
||||
| **NVIDIA GeForce RTX 20 / 30 / 40 / 50** | [Installation NVIDIA](docs/INSTALL.md#what-you-need) |
|
||||
| **AMD Radeon RX 7700 / 7800 / 7900, RX 9060 / 9070, AI PRO R9700** | [Installation AMD](docs/AMD_HIP.md) |
|
||||
| **AMD Radeon RX 6800 / 6900 · expérimental** | [Installation RX 6000](docs/AMD_HIP.md#rdna2-gfx1030) |
|
||||
| **Intel Arc · Linux** | [Installation Intel](docs/INTEL_ARC.md) |
|
||||
| **Apple Silicon · M1 ou plus récent** | [Installation sur Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max · Strix Halo** | [Installation Strix Halo](docs/STRIX_HALO.md) |
|
||||
|
||||
L'explication plus longue : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Chaque élément avec ses chiffres :
|
||||
[les détails](docs/DETAILS.md#how-it-works) et l'[article](docs/paper/Strata-Paper.pdf).
|
||||
<a id="using-it"></a>
|
||||
|
||||
## Un problème ?
|
||||
## Outils
|
||||
|
||||
- **Mon PC s'est figé au premier démarrage de Strata.** Il charge le modèle en RAM. Patientez et ne fermez pas la fenêtre.
|
||||
Toujours figé après 10 minutes ? Redémarrez le PC, fermez les autres programmes et réessayez, ou choisissez une taille plus petite.
|
||||
- **Ça s'est arrêté pendant le téléchargement ou l'installation.** Relancez l'application ou `strata launch`. Cela reprend là
|
||||
où ça s'était arrêté.
|
||||
- **C'est très lent, ou il affiche « the engine stopped unexpectedly ».** Votre PC n'a pas assez de RAM libre. Fermez
|
||||
d'autres programmes (les navigateurs en consomment beaucoup) ou choisissez une taille plus petite, comme Q2_0 ou IQ2_XS.
|
||||
- **Il indique qu'il n'y a pas de moteur pour ce PC.** Dans l'application, ouvrez **Settings, Engine** et appuyez sur **Update engine**
|
||||
(mettre à jour le moteur). Dans le Terminal, lancez `strata update`.
|
||||
- **Il indique que le port 8080 est déjà utilisé.** Strata tourne déjà. Cherchez sa fenêtre.
|
||||
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw**, et d'autres via les API compatibles OpenAI et Anthropic.
|
||||
|
||||
Autres solutions : [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Toujours bloqué ? Ouvrez une
|
||||
[issue](https://github.com/Niko1221/Strata/issues) et joignez les journaux : dans l'application via **Settings, Engine,
|
||||
Diagnostics, Save logs**, dans le Terminal la sortie de `strata doctor`. Pour un problème de sécurité, signalez-le en
|
||||
privé : [SECURITY.md](SECURITY.md).
|
||||
Lancez **`strata connect`** pour voir la configuration de votre outil. [Guide de connexion](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect).
|
||||
|
||||
## Remerciements et licence
|
||||
<a id="guides"></a>
|
||||
<a id="how-fast-is-it"></a>
|
||||
<a id="how-does-it-work"></a>
|
||||
<a id="something-went-wrong"></a>
|
||||
|
||||
Le modèle est [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), de l'équipe Qwen. Il a été compressé par
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5), agentionai (Gyro)
|
||||
et Unsloth. ISTA-DASLab a aussi produit les versions de DeepSeek V4.1-Flash. Strata utilise des parties de
|
||||
[llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Tous les remerciements : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits).
|
||||
Strata est open source sous [licence MIT](LICENSE). Quelques parties et tous les modèles ont leurs propres licences
|
||||
([lesquelles](docs/HOW_IT_WORKS.md#license)).
|
||||
## Aide
|
||||
|
||||
## Contribuer
|
||||
|
||||
Vous avez trouvé un bug, ou vous voulez proposer une modification ou un benchmark ? Lisez d'abord [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
Il indique ce qu'il faut inclure pour que d'autres puissent vérifier votre travail.
|
||||
[Tous les guides](docs) · [Dépannage](docs/TROUBLESHOOTING.md) · [Signaler un bogue](https://github.com/Niko1221/Strata/issues/new/choose) ·
|
||||
[Benchmarks](docs/COMMUNITY_BENCHMARKS.md) · [Comment ça marche](docs/HOW_IT_WORKS.md) · [Contribuer](CONTRIBUTING.md)
|
||||
|
||||
## Soutenir Strata
|
||||
|
||||
Strata est gratuit et sous licence MIT. Je le construis et je le teste sur le matériel que je peux obtenir. S'il tourne bien sur votre PC, un café m'aide
|
||||
à continuer de tester de nouvelles cartes et à corriger ce que vous signalez. Aucune obligation. L'utiliser et signaler ce que vous constatez aide tout autant.
|
||||
**Aidez à garder l'IA locale ouverte à tous.** Un café aide à financer le développement et les tests sur davantage de matériel.
|
||||
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee : soutenir Strata" height="48"></a>
|
||||
|
||||
Vous souhaitez sponsoriser Strata ? Contactez-moi via [GitHub](https://github.com/Niko1221).
|
||||
[Licence MIT](LICENSE) · [Crédits et licences des modèles](docs/HOW_IT_WORKS.md#credits) · [Confidentialité](docs/SAFETY.md)
|
||||
|
||||
<a id="option-c-let-your-ai-set-it-up"></a>
|
||||
|
||||
<sub>Pour les outils d'IA et les lecteurs automatiques : <a href="AGENTS.md">référence technique</a> · <a href="docs/AI_SETUP.md">instructions d'installation</a>.</sub>
|
||||
|
||||
**Parrainages, partenariats matériel ou collaborations :** [[email protected]](mailto:[email protected]).
|
||||
+122
-284
@@ -1,350 +1,188 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="340">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <b>日本語</b> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<h3 align="center">最先端のモデルを、手元のゲーミングPCで動かす</h3>
|
||||
|
||||
<p align="center">NVIDIA、AMD、Intel に対応 · Windows、Linux、Mac · 無料のオープンソース</p>
|
||||
<h3 align="center">あなた自身のパソコンで、高性能な AI を。</h3>
|
||||
<p align="center">チャット、コーディング、画像の処理を、すべてローカルで。</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="最新リリース"></a>
|
||||
<a href="#モデル">モデル</a> · <a href="#インストール"><b>インストール</b></a> ·
|
||||
<a href="#ハードウェア">ハードウェア</a> · <a href="#ツール">ツール</a> ·
|
||||
<a href="#ヘルプ">ヘルプ</a> · <a href="#strata-を支援する">支援</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Strata をダウンロード"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT ライセンス"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub のスター"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=black" alt="コーヒーをおごる"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Buy Me a Coffee で Strata を支援する"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Strata のモデルが書いたボクセルの塔の庭園。ブラウザで動いています"></a><br>
|
||||
<sub>ボクセルの塔の庭園。1回のプロンプトで生成し、RTX 5070 上の Strata で動かしています(IQ3_S、128K コンテキスト) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">動画全体(49秒)</a></sub></p>
|
||||
<p align="center"><sub><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <b>日本語</b> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a> · <a href="README.pl.md">Polski</a></sub></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#インストール"><b>インストール</b></a> ·
|
||||
<a href="#モデル">モデル</a> ·
|
||||
<a href="#どのくらい速い">速度</a> ·
|
||||
<a href="#使い方">使い方</a> ·
|
||||
<a href="#仕組み">仕組み</a> ·
|
||||
<a href="#うまくいかないとき">ヘルプ</a> ·
|
||||
<a href="#strata-を応援する">応援する</a>
|
||||
</p>
|
||||
**無料でオープンソース。自分の環境で動かせます。** デスクトップで、またはお好みのコーディングツールと一緒に Strata を使えます。アカウントは不要です。
|
||||
|
||||
Strata は **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** を動かします。1250億パラメータのモデルで、
|
||||
ふつうはサーバーが必要ですが、Strata なら一般的なPCで使えます。0.2.4 からは、ターミナル版で **DeepSeek V4.1-Flash** も動かせます。
|
||||
チャット、コード作成、画像の読み取りができ、お使いのアプリやコーディングエージェントとも連携します。チャットとファイルはPCの外に出ません。
|
||||
## モデル
|
||||
|
||||
- **ゲーミングPCで動きます。** RAMとグラフィックスメモリの合計が32 GBあれば始められます。
|
||||
- **お使いのツールと連携します。** Claude Code、Codex CLI、OpenCode などのエージェントが、OpenAI と Anthropic の API で接続できます。
|
||||
- **画像を読めます。** 初期設定でオンです。
|
||||
- **無料でプライバシーも守ります。** MIT ライセンス。アカウントもクラウドもテレメトリも不要です([何をいつダウンロードするか](docs/SAFETY.md))。
|
||||
まず **ISTA-DASLab** をおすすめします。次に **Unsloth** などの提供元です。メモリに収まる互換モデルがある場合に選べます。
|
||||
|
||||
## 必要な環境
|
||||
<details>
|
||||
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> <b>Qwen3.8-Flash-Next</b></summary>
|
||||
|
||||
**RAMとグラフィックスメモリ(VRAM)の合計が、少なくとも32 GB必要です。** MacやRyzen AI Maxでは、ユニファイドメモリがその量にあたります。
|
||||
RAM 32 GBのPCに12〜16 GBのカードがあれば小さいサイズが動き、64 GBならどのサイズも動きます。
|
||||
|
||||
| 動く環境 | ガイド |
|
||||
| 提供元 | 選択肢 |
|
||||
| --- | --- |
|
||||
| **NVIDIA** GeForce RTX 20、30、40、50 シリーズ、12 GB以上 | [インストール](#インストール) |
|
||||
| **AMD** Radeon RX 6800 以降、Radeon AI PRO R9700 | [AMD のカード](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md) |
|
||||
| **Apple Silicon の Mac**、M1 以降、macOS 15 以降 | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max(Strix Halo)** などのユニファイドメモリ搭載機 | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| 古いカードやプロセッサ | [古いGPU](docs/OLDER_GPUS.md) |
|
||||
| **ISTA-DASLab · おすすめ** | Q2\_0, IQ2\_XS, IQ3\_XXS, IQ3\_S。[Coder: IQ1\_M](docs/MODELS.md#coder)(コーディング向け)。 |
|
||||
| **Unsloth** | UD-IQ4\_XS, UD-Q4\_K\_XL, UD-Q5\_K\_XL, UD-Q6\_K\_XL, Q8\_0 |
|
||||
| **Gyro · agentionai** | [Gyro-S, Gyro-M](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)。メモリ使用量は少ないですが、動作は遅くなります。 |
|
||||
| **その他の選択肢** | [Swift 1.5](docs/MODELS.md#swift-15), [NVFP4](docs/NVFP4.md), [OrcaRouter](docs/ORCA.md) |
|
||||
|
||||
そのほかに、約80 GBの空きディスク容量(SSD)、Windows 10 / 11、Linux、または macOS、最新のグラフィックスドライバーが必要です。
|
||||
同じメーカーのカードを2〜3枚使って、1つのモデルを動かせます([マルチGPU](docs/MULTI_GPU.md))。
|
||||
[サイズ、必要なメモリ、互換性を比べる](docs/MODELS.md)。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> <b>Qwen3.6-35B-A3B</b></summary>
|
||||
|
||||
**Unsloth:** UD-Q4_K_XL, UD-Q6_K_XL, Q8_0。現在、このモデルで文書化されているパックです。
|
||||
|
||||
[モデルガイドと互換性](docs/QWEN36.md)。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><img src="docs/media/models/deepseek.svg" width="22" alt="" align="middle"> <b>DeepSeekV4.1-Flash-1017</b></summary>
|
||||
|
||||
**デスクトップアプリ**と**ターミナル**で使えます。メモリに収まらない分は SSD から読み込むため、空きディスク容量がたくさん必要です。
|
||||
|
||||
| セット(IST-DAS) | ダウンロード | 必要な空き容量 |
|
||||
| --- | ---: | ---: |
|
||||
| 1.75 bpw | 239 GB | 約245 GB |
|
||||
| 2.00 bpw | 257 GB | 約265 GB |
|
||||
| 2.25 bpw | 274 GB | 約280 GB |
|
||||
|
||||
NVIDIA RTX 30・40・50 シリーズ(Windows と Linux)と、AMD Radeon AI PRO R9700(Linux)で動きます。セットアップがファイルと DeepSeek エンジンをダウンロードし、モデルを起動します。
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
```
|
||||
|
||||
Strata 0.2.5 で 2.00 bpw のセットを使って測定しました。「出力」は回答の速さ、「プロンプト」は Strata が入力を読み込む速さです。
|
||||
|
||||
| ハードウェア | 出力 | プロンプト |
|
||||
| --- | ---: | ---: |
|
||||
| RTX 5070 12 GB, 64 GB RAM | 4〜6 トークン/秒 | 約 110 トークン/秒 (4K) |
|
||||
| RTX 3060 12 GB, 46 GB RAM | 9〜11 トークン/秒 | 約 155 トークン/秒 (4K) |
|
||||
| 1x Radeon AI PRO R9700, 251 GB RAM | 21〜25 トークン/秒 | 320〜360 トークン/秒 (8K) |
|
||||
| 4x Radeon AI PRO R9700 | 74〜77 トークン/秒 | 400〜450 トークン/秒 (8K) |
|
||||
|
||||
[メモリ、ディスク、詳細](docs/DEEPSEEK41.md) · [リリースノート](https://github.com/Niko1221/Strata/releases)
|
||||
|
||||
</details>
|
||||
|
||||
<a id="the-other-sizes"></a>
|
||||
|
||||
どれが動くかわからない場合は、[お使いのパソコンに合うモデルを選ぶ](docs/MODELS.md#pick-by-memory)。
|
||||
|
||||
## インストール
|
||||
|
||||
### 1. Strata アプリ(いちばん簡単)
|
||||
<a id="option-a-the-strata-app"></a>
|
||||
|
||||
お使いのPCに合うファイルをダウンロードして開き、すすめられたモデルのインストールを押します。
|
||||
### デスクトップアプリ
|
||||
|
||||
**Windows 10 / 11**
|
||||
**[Windows、Linux、Mac 用の Strata をダウンロード →](https://github.com/Niko1221/Strata/releases/latest)**
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-windows-x64-setup.exe)
|
||||
アプリをインストールし、モデルを選んで、チャットを始めましょう。インストーラーはシステムごとに1つです。初回起動時に、アプリがお使いのグラフィックスカード用のエンジンをダウンロードします。[インストーラーのヘルプ](docs/INSTALL.md#the-app)。
|
||||
|
||||
**Linux**
|
||||
<sub>ファイル: `Strata-0.2.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-linux-x86_64.AppImage)
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="チャットとライブのコードプレビューを並べて表示した Strata デスクトップアプリ">
|
||||
<br><sub>チャット、ファイル、ライブプレビューを1つのウィンドウで。</sub>
|
||||
</p>
|
||||
|
||||
**Mac**(Apple Silicon、macOS 15 以降)
|
||||
<a id="option-b-the-terminal-one-line"></a>
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-macos-arm64.dmg)
|
||||
### ターミナルアプリ
|
||||
|
||||
お使いのグラフィックスカードに合うエンジンは、アプリが自動で選びます。すべてのファイルは
|
||||
[リリースページ](https://github.com/Niko1221/Strata/releases/latest)にあります。
|
||||
ターミナルのほうが好みですか? Strata をインストールして、**`strata launch`** を実行します。
|
||||
|
||||
| 初回の起動 | すること |
|
||||
| --- | --- |
|
||||
| **Windows** | `.exe` を実行します。管理者権限は不要です。「Windows によってPCが保護されました」と表示されたら、**詳細情報**を選び、**実行**を選んでください。 |
|
||||
| **Linux** | ファイルを実行可能にして(`chmod +x`)起動します。FUSE 2 が必要です。`sudo apt install libfuse2t64`(Ubuntu 24.04 以降)または `libfuse2`(22.04)を実行してください。 |
|
||||
| **Mac** | `.dmg` を開き、Strata を「アプリケーション」にドラッグします。 |
|
||||
<details open>
|
||||
<summary><b>インストールコマンド</b></summary>
|
||||
|
||||
モデルのダウンロードは大きく(40〜70 GB)、止まっても続きから再開します。
|
||||
|
||||
### 2. ターミナル(1行)
|
||||
|
||||
`strata` コマンドです。スクリプトやサーバー、AI エージェント向けです。DeepSeek を動かすにもこれを使います。
|
||||
|
||||
Linux と Mac:
|
||||
**Linux / Mac**
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows(PowerShell):
|
||||
**Windows PowerShell**
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
そのあと起動します。質問は1つずつ表示されます(モデル、コンテキスト、画像、接続を許可する相手)。必要なものをダウンロードして、
|
||||
サーバーを開始します。
|
||||
Windows では、インストール後に新しいターミナルを開きます。そのあと:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
管理者権限は不要です。インストールでは、ダウンロードしたものすべての SHA-256 を検証します。Windows では、インストール後に新しいターミナルを開いてください。
|
||||
すべての画面とフラグは [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md) にあります。
|
||||
|
||||
### 3. AI にセットアップを任せる
|
||||
|
||||
Claude Code、Cursor、Codex、GitHub Copilot などのコーディングアシスタントに、次の文を貼り付けます。
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
AI はグラフィックスカード、RAM、ディスクを調べ、合うモデルを選んで、インストールと起動をし、アプリの接続方法を教えてくれます。
|
||||
AI ツールは [MCP サーバー](docs/MCP_SERVER.md)を通じて、Strata のインストール、起動、停止もできます。
|
||||
|
||||
自分でビルドしたい方は[ソースからビルド](#ソースからビルド)を見てください。
|
||||
|
||||
## モデル
|
||||
|
||||
アプリと `strata launch` は、メモリ量に合うサイズをすすめます。小さいサイズは速く、大きいサイズは少し賢くなります。
|
||||
|
||||
### メモリが少ないなら Gyro
|
||||
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)(agentionai 製)は、Qwen3.8-Flash-Next をいちばん小さく動かせる方法です。
|
||||
NVIDIA、AMD、Intel Arc、Apple Silicon で動きます。
|
||||
|
||||
| サイズ | 必要なメモリ(RAM + VRAM) | ダウンロード | 向いているPC |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27.6 GB | 58.5 GB | ほかのサイズが入らないPC向け。32 GB の Mac mini で毎秒 9 トークン。 |
|
||||
| **Gyro-M** | 35 GB | 92.0 GB | 品質は IQ3_XXS 程度で、GPUメモリの使用量は20%少なくなります。 |
|
||||
|
||||
`strata launch --model gyro-s` で起動します。詳細は [docs/GYRO.md](docs/GYRO.md) にあります。
|
||||
|
||||
### Qwen3.8-Flash-Next のサイズ
|
||||
|
||||
| メモリ(RAM + VRAM) | 選ぶモデル | 理由 |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Gyro-S**、またはコード向けの **[Coder](docs/MODELS.md#coder)** | Coder は、コードで使うエキスパートを残します |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS(39.2 GB)、または **Gyro-M** | Swift は答える前に考える時間が短くなります |
|
||||
| **48 GB** | **IQ3_XXS**(47.0 GB) | 遅くなりますが、品質は上がります |
|
||||
| **64 GB** | **IQ3_S**(54.8 GB) | 公開されているテストでは完全版と同等です |
|
||||
| **96 GB 以上** | Unsloth の **UD-IQ4_XS** から **Q8_0** | 完全版にいちばん近いです |
|
||||
|
||||
表の行より RAM が少なくても、遅くなるだけで動くことがあります。Strata は自動で[低RAMモード](docs/MODELS.md#a-big-graphics-card-and-little-ram)に切り替わります。
|
||||
そのほかに、NVIDIA カード向けの [NVFP4](docs/NVFP4.md)、[Qwen3.6-35B-A3B](docs/QWEN36.md)(より小さく、複数のリクエストを同時に処理)、
|
||||
[OrcaRouter の Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs) があります。すべてのサイズと、どの構成で何が動くか:
|
||||
[docs/MODELS.md](docs/MODELS.md)。
|
||||
|
||||
### DeepSeek V4.1-Flash(ターミナル版、0.2.4 から)
|
||||
|
||||
ずっと大きなモデルで、12 GB のグラフィックスカード 1 枚と 46 から 64 GB の RAM で動きます。Strata はよく使う部分をメモリに置き、
|
||||
残りは SSD から読みます。セットは [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF) が作りました。
|
||||
NVIDIA RTX 30、40、50 シリーズのカードと Radeon AI PRO R9700 で動きます。`strata launch` で選んでください。デスクトップアプリはまだ対応していません。
|
||||
|
||||
| セット | ダウンロード | 品質 | コードの出力速度 |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M**(1.75 ビット) | 239 GB | いちばん小さく、ふつうの文章は弱めです | PC ではまだ測っていません |
|
||||
| **IQ2_XXS**(2.00 ビット) | 257 GB | バランスが良いです | 毎秒 10 トークン(RTX 3060)、5(RTX 5070)、48(R9700 4 枚) |
|
||||
| **IQ2_XS**(2.25 ビット) | 274 GB | 元のモデルにいちばん近いです | 毎秒 15 トークン(R9700 1 枚)、80(R9700 4 枚) |
|
||||
| **IQ4_NL**(Engram テーブル) | 上のどのダウンロードにも含まれます | 3 つのセットが共有する参照テーブルです | |
|
||||
|
||||
`strata launch --model ist-das-2.00bpw`(または `ist-das-1.75bpw`、`ist-das-2.25bpw`)で起動します。品質の数値、PC ごとの速度、手動インストールは
|
||||
[docs/DEEPSEEK41.md](docs/DEEPSEEK41.md) にあります。
|
||||
|
||||
## どのくらい速い?
|
||||
|
||||
12 GBのグラフィックスカード(RTX 5070、RAM 64 GB)での数値です。ほかのカードの結果は
|
||||
[各サイズの速度](docs/MODELS.md#how-fast-is-each-size)と[コミュニティの結果](docs/COMMUNITY_BENCHMARKS.md)を見てください。
|
||||
|
||||
| モデル | 回答が出る速さ | プロンプトを読む速さ |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████████` 94 tokens/s | `████████████` 2,650 tokens/s |
|
||||
| **IQ2_XS** | `██████████░░` 79 tokens/s | `█████████░░░` 2,090 tokens/s |
|
||||
| **IQ3_XXS** | `████████░░░░` 62 tokens/s | `████████░░░░` 1,750 tokens/s |
|
||||
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1,620 tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2,180 tokens/s |
|
||||
|
||||
16 GBの AMD カード(RX 9070 XT、RAM 47 GB)の場合:
|
||||
|
||||
| モデル | 回答が出る速さ | プロンプトを読む速さ |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████░░░░` 60 tokens/s | `█████░░░░░░░` 1,160 tokens/s |
|
||||
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1,110 tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1,420 tokens/s |
|
||||
|
||||
1トークンは英単語の約¾にあたるので、毎秒60トークンは読む速さより速くなります。「プロンプトを読む速さ」は、長い文書やコード、チャット履歴を Strata が取り込む速さです。
|
||||
|
||||
## 使い方
|
||||
|
||||
### アプリで
|
||||
|
||||
チャット、モデルとGPU・CPU・RAMの状態を見る**モニター**、**モデル**ページ、**設定**があります。チャットはPC上のフォルダで作業できます。
|
||||
**手動**モードでは、ファイルを変更したりコマンドを実行したりする前に確認します。ウェブ検索もできます(DuckDuckGo または Bing、アカウント不要)。
|
||||
PCの外に出るのは検索語だけです。
|
||||
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="Strata アプリ。エージェントが小さなボクセルの塔の庭を作ったチャットで、右側のプレビューにそのページが開いています"><br>
|
||||
<sub>エージェントがボクセルの塔の庭を 1 つの HTML ファイルとして書き、そのページがチャットの横のプレビューで開きます</sub></p>
|
||||
|
||||
### ターミナルで
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="モデルが返信を書いている間の Strata ターミナルのモニター概要"><br>
|
||||
<sub>モデルが返信を書いている間の <code>strata monitor</code>。速度、GPU、メモリ、最近のリクエストが表示されます</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve and print the address
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor # watch speed, GPU, memory and requests
|
||||
strata update # get the newest engine
|
||||
strata doctor # check the installed files
|
||||
```
|
||||
|
||||
スマートフォンや別のPCから Strata に接続するには、`strata launch --access lan` を使います。API キーが自動で作られます。
|
||||
`strata --help` ですべてのコマンドを確認できます。説明は [docs/TERMINAL.md](docs/TERMINAL.md) にあります。
|
||||
|
||||
### AI エージェントやほかのアプリと使う
|
||||
|
||||
エージェントは、上の1行コマンドで Strata をインストールし、`strata launch --model NAME --yes --no-tui` を実行します。手順、フラグ、終了コードは
|
||||
[docs/AI_SETUP.md](docs/AI_SETUP.md) にあります。エージェント向けのリポジトリのルールは [AGENTS.md](AGENTS.md) にあります。
|
||||
|
||||
| アプリが使うもの | ベース URL |
|
||||
| --- | --- |
|
||||
| OpenAI API(ほとんどのアプリとエージェント) | `http://127.0.0.1:8080/v1`(API キーもモデル名も何でも可) |
|
||||
| Anthropic API(Claude Code) | `http://127.0.0.1:8080`、`/v1` は付けません。Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API(Codex CLI) | `http://127.0.0.1:8080/v1/responses`([設定方法](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` は、OpenCode、Codex CLI、Claude Code、oh-my-pi、pi、Hermes Agent、OpenClaw の正確な設定を表示します。
|
||||
`strata connect opencode --write` なら、設定を書き込みます(バックアップ付き)。
|
||||
|
||||
### 知っておくと便利なこと
|
||||
|
||||
- **画像**は、画像を読めるモデルすべてでオンです。チャットに画像を添付または貼り付けるか、アプリから送ってください。詳細: [docs/IMAGES.md](docs/IMAGES.md)。
|
||||
- **思考のレベル**は off、low、medium、high から、チャットのメニューか、お使いのアプリの「reasoning effort」で選びます。off がいちばん速くなります。
|
||||
- **チャットの最初のメッセージ**は全文読みます。30,000トークンあたり約1分かかります。続きのメッセージは数秒で始まります。
|
||||
- **一度に1つのリクエスト**が初期設定です。複数にするには `--parallel N` を使います([BATCHING.md](docs/BATCHING.md))。12 GBのカードでは、1つずつの回答が遅くなります。
|
||||
|
||||
## ソースからビルド
|
||||
|
||||
<details>
|
||||
<summary>リポジトリから Windows、Linux、Mac 用にビルドする</summary>
|
||||
|
||||
[Strata をダウンロード](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip)して展開するか、`git clone` します。
|
||||
**Windows:** `START-HERE.bat` をダブルクリックします。**Linux:** Strata フォルダで `./setup.sh` を実行します。
|
||||
|
||||
**Mac(Apple Silicon、macOS 15 以降):** 最初に一度、`xcode-select --install` で Apple のコマンドラインツールをインストールします
|
||||
(Xcode 本体は不要です)。Strata フォルダでターミナルを開き、`./setup.sh` を実行します。Mac 用エンジンをお使いの Mac 上でビルドし
|
||||
(約7分)、同じ質問をして、モデルをダウンロードし、起動します。2回目以降は `./setup.sh` ですぐに起動します。
|
||||
お使いの Mac に合うサイズは [docs/MAC.md](docs/MAC.md) を見てください。
|
||||
|
||||
インストーラーはカードを見つけて、合うエンジンを設定します。どのモデルとサイズにするか、コンテキストをどれだけにするか(モデルが一度に覚えておけるテキストの量)、
|
||||
画像を読ませるかを聞かれます。おすすめの答えでよければ Enter を押します。そのあとモデル(約70 GB)をダウンロードして起動します。
|
||||
ダウンロードが止まったら、もう一度実行すると続きから再開します。ブラウザで `http://127.0.0.1:8080` の Strata ページが開きます。
|
||||
|
||||
> モデルの起動中は、PCが1〜3分ほど(初回がいちばん長い)遅くなったり、応答しなくなったりすることがあります。Strata は35〜55 GBをRAMに読み込み、
|
||||
> その一部をグラフィックスカード用に固定するためです。ウィンドウを閉じずに待ってください。
|
||||
|
||||
次回からは `START-HERE.bat`(または `./setup.sh`)をもう一度実行するだけです。すぐに起動し、同じものを2回ダウンロードすることはありません。ウィンドウを閉じるとモデルが止まります。
|
||||
`UPDATE.bat`(`./update.sh`)は、起動せずに Strata を更新します。更新、Docker、複数のカード、ファイルの保存先、すべてのオプションは
|
||||
[docs/INSTALL.md](docs/INSTALL.md) にあります。
|
||||
|
||||
PCの性能を測るには、`BENCH.bat`(`./bench.sh`)を実行します。5〜10分かかり、共有できるレポートが作られます
|
||||
([方法](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark))。
|
||||
|
||||
**オプション: モデル全体がRAMに入るとき、1枚のカードをさらに速くする。** RubenTorney による、自分でオンにする2つの設定です(#1904、#1905)。
|
||||
グラフィックスカードが1枚で、RAM がすべてのエキスパートを保持できるときに効果があります。たとえば64 GBのPCでの Q2_0 や IQ3_XXS です。
|
||||
Strata のデータフォルダにある `strata-<model>.json`([場所](docs/INSTALL.md#where-things-are-stored))を開いて、次を追加します。
|
||||
|
||||
- `"args"` のリストに `"--adapt-async", "1"` を追加します。エキスパートキャッシュが、回答を止めずに補助スレッドで更新されます。
|
||||
RTX 5070(Windows、Q2_0)での測定: デコードが4〜5%、コードや長い文書では7〜10%速くなりました。回答は初期設定と少し違うことがありますが、実行ごとの結果は変わりません。
|
||||
- `"env"` ブロックに `"STRATA_PIN_BY_NEED": "1"` を追加します(Windows)。Windows ではグラフィックスカード用に固定できる RAM が半分ほどしかありません。
|
||||
この設定は、カードが最初に必要とするエキスパートを優先して固定します。RTX 5070 での測定: プロンプトの読み取りが4.4%速くなりました。
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
変更したら Strata を再起動してください。元に戻すには、2つの項目を削除します。
|
||||
画面の案内に従って、モデルをダウンロードして起動します。[ターミナルガイド](docs/TERMINAL_LAUNCH.md)。
|
||||
|
||||
</details>
|
||||
|
||||
## 仕組み
|
||||
<p align="center">
|
||||
<img src="docs/media/terminal-monitor.png" width="900" alt="モデルの動作、速度、メモリ、最近のリクエストを表示する Strata ターミナルアプリ">
|
||||
<br><sub>動作中のモデルとハードウェアの状態を確認できます。</sub>
|
||||
</p>
|
||||
|
||||
このようなモデルは、ふつう数百GBのグラフィックスメモリを持つサーバーで動きます。お使いのグラフィックスカードは12〜24 GBです。Strata は、PC全体で作業を分担して、モデルを収めます。
|
||||
図書館にたとえてみます。
|
||||
<a id="build-from-source"></a>
|
||||
|
||||
- モデルは、24,576人の小さな専門家(「エキスパート」)からなるチームです。24,576冊の参考書のようなものです。1つの単語に必要なのは、そのうち10冊だけです。
|
||||
- **グラフィックスカードは机です。** 小さくて速く、最もよく使う約4,000冊が開いて置いてあります。
|
||||
- **RAM は閲覧室の書棚です。** 24,576冊すべてがここにあります。単語に必要な本が机になければ、プロセッサがすぐに書棚から読み、その間もカードは作業を続けます。
|
||||
- **SSD は書庫です。** 29 GBの参照テーブルがしまってあります。モデルは単語ごとにその数行だけを読むので、RAM に置いておく必要はありません。
|
||||
[ソースからビルド](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [インストールガイド(全文)](docs/INSTALL.md)
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="モデルの24,576人のエキスパート。よく使うものはグラフィックスカードに、全員が RAM に、参照テーブルは SSD にあります"></p>
|
||||
<a id="what-you-need"></a>
|
||||
|
||||
小さな助手が、次の数単語も予想します。大きなモデルがそれをまとめて確認し、正しいものだけを残します。答えは同じで、1.6〜1.8倍早く返ります。
|
||||
長い文章は、一度に最大8,192トークンの大きな塊で、毎秒1,000トークンを超える速さで読みます。
|
||||
## ハードウェア
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="小さな助手が次の単語を予想し、大きなモデルがそれをまとめて確認して、正しいものだけを残します"></p>
|
||||
| 対応ハードウェア | ガイド |
|
||||
| --- | --- |
|
||||
| **NVIDIA GeForce RTX 20 / 30 / 40 / 50 シリーズ** | [NVIDIA のセットアップ](docs/INSTALL.md#what-you-need) |
|
||||
| **AMD Radeon RX 7700 / 7800 / 7900、RX 9060 / 9070、AI PRO R9700** | [AMD のセットアップ](docs/AMD_HIP.md) |
|
||||
| **AMD Radeon RX 6800 / 6900 · 試験的** | [RX 6000 のセットアップ](docs/AMD_HIP.md#rdna2-gfx1030) |
|
||||
| **Intel Arc · Linux** | [Intel のセットアップ](docs/INTEL_ARC.md) |
|
||||
| **Apple Silicon · M1 以降** | [Mac のセットアップ](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max · Strix Halo** | [Strix Halo のセットアップ](docs/STRIX_HALO.md) |
|
||||
|
||||
詳しい説明は [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md) にあります。各部分と数値は
|
||||
[詳細](docs/DETAILS.md#how-it-works)と[論文](docs/paper/Strata-Paper.pdf)を見てください。
|
||||
<a id="using-it"></a>
|
||||
|
||||
## うまくいかないとき
|
||||
## ツール
|
||||
|
||||
- **Strata を初めて起動したときに、PCが固まりました。** モデルを RAM に読み込んでいます。ウィンドウを閉じずに待ってください。10分たっても固まったままなら、
|
||||
PCを再起動し、ほかのプログラムを閉じてからもう一度試すか、小さいサイズを選んでください。
|
||||
- **ダウンロードやインストールの途中で止まりました。** アプリか `strata launch` をもう一度起動してください。止まったところから再開します。
|
||||
- **とても遅い、または「the engine stopped unexpectedly」と表示される。** PCの空き RAM が足りません。
|
||||
ほかのプログラムを閉じるか(ブラウザはメモリを多く使います)、Q2_0 や IQ2_XS のような小さいサイズを選んでください。
|
||||
- **このPCに合うエンジンがないと表示される。** アプリでは、**設定、エンジン**を開いて**エンジンを更新**を押します。ターミナルでは
|
||||
`strata update` を実行します。
|
||||
- **ポート 8080 はすでに使われていると表示される。** Strata がすでに動いています。ウィンドウを探してください。
|
||||
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw**。OpenAI 互換と Anthropic 互換の API を通じて、ほかのツールも使えます。
|
||||
|
||||
そのほかの対処法は [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) にあります。それでも解決しない場合は、
|
||||
[issue](https://github.com/Niko1221/Strata/issues) を作成して、ログを添付してください。アプリでは**設定、エンジン、診断、ログを保存**、
|
||||
ターミナルでは `strata doctor` の出力です。セキュリティ上の問題は、非公開で報告してください: [SECURITY.md](SECURITY.md)。
|
||||
**`strata connect`** を実行すると、お使いのツールの設定方法が表示されます。[接続ガイド](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect)。
|
||||
|
||||
## クレジットとライセンス
|
||||
<a id="guides"></a>
|
||||
<a id="how-fast-is-it"></a>
|
||||
<a id="how-does-it-work"></a>
|
||||
<a id="something-went-wrong"></a>
|
||||
|
||||
モデルは Qwen チームの [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) です。圧縮は
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF)、UkisAI(Swift 1.5)、agentionai(Gyro)、
|
||||
Unsloth が行いました。DeepSeek V4.1-Flash のセットも ISTA-DASLab が作りました。Strata は
|
||||
[llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) の一部を使っています。すべてのクレジットは
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits) にあります。Strata は [MIT ライセンス](LICENSE)のオープンソースです。一部のコンポーネントと
|
||||
すべてのモデルには、それぞれのライセンスがあります([該当するもの](docs/HOW_IT_WORKS.md#license))。
|
||||
## ヘルプ
|
||||
|
||||
## コントリビューション
|
||||
[すべてのガイド](docs) · [トラブルシューティング](docs/TROUBLESHOOTING.md) · [不具合を報告](https://github.com/Niko1221/Strata/issues/new/choose) ·
|
||||
[ベンチマーク](docs/COMMUNITY_BENCHMARKS.md) · [仕組み](docs/HOW_IT_WORKS.md) · [貢献する](CONTRIBUTING.md)
|
||||
|
||||
バグを見つけた、変更やベンチマークを送りたい、という方は、先に [CONTRIBUTING.md](CONTRIBUTING.md) を読んでください。
|
||||
ほかの人があなたの成果を確認できるように、何を書くべきかが説明してあります。
|
||||
## Strata を支援する
|
||||
|
||||
## Strata を応援する
|
||||
**ローカル AI をみんなに開かれたものに。** コーヒー1杯が、開発と、より多くのハードウェアでのテストの支えになります。
|
||||
|
||||
Strata は無料で、MIT ライセンスです。私が入手できるハードウェアで開発とテストをしています。お使いのPCで快適に動いたなら、コーヒー1杯分の支援は、
|
||||
新しいカードのテストと、報告いただいた不具合の修正を続ける助けになります。無理のない範囲で大丈夫です。使って、気づいたことを報告してもらえるだけでも同じように助かります。
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: Strata を支援する" height="48"></a>
|
||||
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
|
||||
[MIT ライセンス](LICENSE) · [クレジットとモデルのライセンス](docs/HOW_IT_WORKS.md#credits) · [プライバシー](docs/SAFETY.md)
|
||||
|
||||
Strata のスポンサーに関心がある方は、[GitHub](https://github.com/Niko1221) から連絡してください。
|
||||
<a id="option-c-let-your-ai-set-it-up"></a>
|
||||
|
||||
<sub>AI ツールと自動リーダー向け: <a href="AGENTS.md">技術リファレンス</a> · <a href="docs/AI_SETUP.md">セットアップ手順</a></sub>
|
||||
|
||||
**スポンサー、ハードウェア提携、共同開発のご相談:** [[email protected]](mailto:[email protected]).
|
||||
@@ -1,357 +1,193 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="340">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
<p align="center"><b>English</b> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<h3 align="center">Run a state-of-the-art model on your own gaming PC</h3>
|
||||
|
||||
<p align="center">Support for NVIDIA, AMD, Intel · Windows, Linux, Mac · free and open source</p>
|
||||
<h3 align="center">Run powerful AI on your own computer.</h3>
|
||||
<p align="center">Chat, code and work with images, locally.</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
|
||||
<a href="#models">Models</a> · <a href="#install"><b>Install</b></a> ·
|
||||
<a href="#hardware">Hardware</a> · <a href="#tools">Tools</a> ·
|
||||
<a href="#help">Help</a> · <a href="#support-strata">Support</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Download Strata"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=black" alt="Buy me a coffee"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Support Strata on Buy Me a Coffee"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<sub>A voxel pagoda garden, 1 shot prompt running on an RTX 5070 with Strata (IQ3_S, 128K context) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">full video (49 s)</a></sub></p>
|
||||
<p align="center"><sub><b>English</b> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a> · <a href="README.pl.md">Polski</a></sub></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#install"><b>Install</b></a> ·
|
||||
<a href="#models">Models</a> ·
|
||||
<a href="#how-fast-is-it">Speed</a> ·
|
||||
<a href="#using-it">Using it</a> ·
|
||||
<a href="#how-does-it-work">How it works</a> ·
|
||||
<a href="#something-went-wrong">Help</a> ·
|
||||
<a href="#support-strata">Support</a>
|
||||
</p>
|
||||
**Free, open source, and yours to run.** Use Strata on your desktop or with your favorite coding tools. No account needed.
|
||||
|
||||
Strata runs **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, a 125-billion-parameter model that
|
||||
normally needs a server, on a normal PC. From 0.2.4 it also runs **DeepSeek V4.1-Flash** in the Terminal. It chats, writes
|
||||
code, reads pictures and works with your apps and coding agents. Your chats and files stay on your PC.
|
||||
## Models
|
||||
|
||||
- **Runs on a gaming PC.** 32 GB of RAM and graphics memory together is enough to start.
|
||||
- **Works with your tools.** Claude Code, Codex CLI, OpenCode and other agents connect through the OpenAI and Anthropic APIs.
|
||||
- **Reads pictures**, on by default.
|
||||
- **Free and private.** MIT license, no account, no cloud, no telemetry ([what it downloads and when](docs/SAFETY.md)).
|
||||
We recommend **ISTA-DASLab first**, then **Unsloth** and other providers, where a compatible model fits.
|
||||
|
||||
## What you need
|
||||
<details>
|
||||
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> <b>Qwen3.8-Flash-Next</b></summary>
|
||||
|
||||
**At least 32 GB of memory: RAM and graphics memory (VRAM) together.** On a Mac or a Ryzen AI Max that is the unified
|
||||
memory. A 32 GB PC with a 12-16 GB card runs the smaller sizes, and 64 GB runs every size.
|
||||
|
||||
| Works on | Guide |
|
||||
| Provider | Choices |
|
||||
| --- | --- |
|
||||
| **NVIDIA** GeForce RTX 20, 30, 40 and 50 series, 12 GB or more | [Install](#install) |
|
||||
| **AMD** Radeon RX 6800 and newer, Radeon AI PRO R9700 | [AMD cards](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md) |
|
||||
| **Apple Silicon Mac**, M1 or newer, macOS 15 or newer | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** and other unified-memory PCs | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Older cards and processors | [Older GPUs](docs/OLDER_GPUS.md) |
|
||||
| **ISTA-DASLab · recommended** | Q2\_0, IQ2\_XS, IQ3\_XXS, IQ3\_S. [Coder: IQ1\_M](docs/MODELS.md#coder) for coding. |
|
||||
| **Unsloth** | UD-IQ4\_XS, UD-Q4\_K\_XL, UD-Q5\_K\_XL, UD-Q6\_K\_XL, Q8\_0 |
|
||||
| **Gyro · agentionai** | [Gyro-S, Gyro-M](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF). Lower memory use but slower. |
|
||||
| **Other options** | [Swift 1.5](docs/MODELS.md#swift-15), [NVFP4](docs/NVFP4.md), [OrcaRouter](docs/ORCA.md) |
|
||||
|
||||
You also need about 80 GB of free disk (an SSD), Windows 10 / 11, Linux or macOS, and a current graphics driver. Two or
|
||||
three cards of the same make can share a model ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
[Compare sizes, memory needs and compatibility](docs/MODELS.md).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> <b>Qwen3.6-35B-A3B</b></summary>
|
||||
|
||||
**Unsloth:** UD-Q4_K_XL, UD-Q6_K_XL, Q8_0. These are the currently documented packs for this model.
|
||||
|
||||
[Model guide and compatibility](docs/QWEN36.md).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><img src="docs/media/models/deepseek.svg" width="22" alt="" align="middle"> <b>DeepSeekV4.1-Flash-1017</b></summary>
|
||||
|
||||
In the **desktop app** and the **Terminal**. What does not fit in memory is read from your SSD, so it needs a lot of free
|
||||
disk space.
|
||||
|
||||
| Set (IST-DAS) | Download | Free disk needed |
|
||||
| --- | ---: | ---: |
|
||||
| 1.75 bpw | 239 GB | about 245 GB |
|
||||
| 2.00 bpw | 257 GB | about 265 GB |
|
||||
| 2.25 bpw | 274 GB | about 280 GB |
|
||||
|
||||
Runs on NVIDIA RTX 30, 40 and 50 series (Windows and Linux), AMD Radeon AI PRO R9700 (Linux), and AMD Ryzen AI Max — Strix Halo
|
||||
(Windows). Setup downloads the files and the DeepSeek engine, then starts the model.
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
```
|
||||
|
||||
Measured with the 2.00 bpw set on Strata 0.2.5 (Strix Halo: 0.2.7). Output is the speed of the answer, prompt is how fast Strata reads your input.
|
||||
|
||||
| Hardware | Output | Prompt |
|
||||
| --- | ---: | ---: |
|
||||
| RTX 5070 12 GB, 64 GB RAM | 4 to 6 tokens/s | about 110 tokens/s (4K) |
|
||||
| RTX 3060 12 GB, 46 GB RAM | 9 to 11 tokens/s | about 155 tokens/s (4K) |
|
||||
| 1x Radeon AI PRO R9700, 251 GB RAM | 21 to 25 tokens/s | 320 to 360 tokens/s (8K) |
|
||||
| 4x Radeon AI PRO R9700 | 74 to 77 tokens/s | 400 to 450 tokens/s (8K) |
|
||||
| Ryzen AI Max+ 395 (Strix Halo), 128 GB unified | 19 to 21 tokens/s (code), 11 (chat) | 87 tokens/s (4K), 125 tokens/s (8K) |
|
||||
|
||||
[Memory, disk and details](docs/DEEPSEEK41.md) · [Release notes](https://github.com/Niko1221/Strata/releases)
|
||||
|
||||
</details>
|
||||
|
||||
<a id="the-other-sizes"></a>
|
||||
|
||||
Not sure what fits? [Choose a model for your computer](docs/MODELS.md#pick-by-memory).
|
||||
|
||||
## Install
|
||||
|
||||
### 1. The Strata app (easiest)
|
||||
<a id="option-a-the-strata-app"></a>
|
||||
|
||||
Download the file for your computer, open it, and press install on the model it recommends.
|
||||
### Desktop app
|
||||
|
||||
**Windows 10 / 11**
|
||||
**[Download Strata for Windows, Linux or Mac →](https://github.com/Niko1221/Strata/releases/latest)**
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-windows-x64-setup.exe)
|
||||
Install the app, choose a model, and start chatting. There is one installer for each system. On first start the app
|
||||
downloads the engine for your graphics card. [Help with the installer](docs/INSTALL.md#the-app).
|
||||
|
||||
**Linux**
|
||||
<sub>Files: `Strata-0.2.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-linux-x86_64.AppImage)
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Strata desktop app with a chat and live code preview side by side">
|
||||
<br><sub>Chat, files and a live preview in one window.</sub>
|
||||
</p>
|
||||
|
||||
**Mac** (Apple Silicon, macOS 15 or newer)
|
||||
<a id="option-b-the-terminal-one-line"></a>
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-macos-arm64.dmg)
|
||||
### Terminal app
|
||||
|
||||
The app picks the right engine for your graphics card by itself. All files are on the [releases page](https://github.com/Niko1221/Strata/releases/latest).
|
||||
Prefer the terminal? Install Strata, then run **`strata launch`**.
|
||||
|
||||
| First start | What to do |
|
||||
| --- | --- |
|
||||
| **Windows** | Run the `.exe`. No administrator rights needed. If Windows says "Windows protected your PC", choose **More info**, then **Run anyway**. |
|
||||
| **Linux** | `chmod +x` the file and start it. It needs FUSE 2: `sudo apt install libfuse2t64` (Ubuntu 24.04 and newer) or `libfuse2` (22.04). |
|
||||
| **Mac** | Open the `.dmg` and drag Strata onto Applications. |
|
||||
<details open>
|
||||
<summary><b>Install commands</b></summary>
|
||||
|
||||
The model download is big (40-70 GB). If it stops, it continues where it left off.
|
||||
|
||||
### 2. The Terminal (one line)
|
||||
|
||||
The `strata` command, for scripts, servers and AI agents. It is also the way to run DeepSeek.
|
||||
|
||||
Linux and Mac:
|
||||
**Linux / Mac**
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
**Windows PowerShell**
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
Then start it. It asks one question at a time (model, context, pictures, who may connect), downloads what it needs and
|
||||
starts serving:
|
||||
On Windows, open a new terminal afterwards. Then:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
No admin rights needed. The install checks the SHA-256 of everything it downloads. On Windows, open a new terminal window
|
||||
after the install. All screens and flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### 3. Let your AI set it up
|
||||
|
||||
Paste this into Claude Code, Cursor, Codex, GitHub Copilot or another coding assistant:
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
It checks your graphics card, RAM and disk, picks the model that fits, installs it, starts it and tells you how to connect
|
||||
your apps. AI tools can also install, start and stop Strata through its [MCP server](docs/MCP_SERVER.md).
|
||||
|
||||
Prefer to build it yourself? See [Build from source](#build-from-source).
|
||||
|
||||
## Models
|
||||
|
||||
The app and `strata launch` recommend a size for your memory. Smaller sizes are faster, larger sizes are a bit smarter.
|
||||
|
||||
### Low on memory? Start with Gyro
|
||||
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) by agentionai is the smallest way to run
|
||||
Qwen3.8-Flash-Next. It runs on NVIDIA, AMD, Intel Arc and Apple Silicon.
|
||||
|
||||
| Size | Memory it needs (RAM + VRAM) | Download | Good for |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27.6 GB | 58.5 GB | PCs that fit nothing else. 9 tokens/s on a 32 GB Mac mini. |
|
||||
| **Gyro-M** | 35 GB | 92.0 GB | About IQ3_XXS quality with 20% less GPU memory. |
|
||||
|
||||
Start it with `strata launch --model gyro-s`. Details: [docs/GYRO.md](docs/GYRO.md).
|
||||
|
||||
### Qwen3.8-Flash-Next sizes
|
||||
|
||||
| Your memory (RAM + VRAM) | Take | Why |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Gyro-S**, or the **[Coder](docs/MODELS.md#coder)** for code | The Coder keeps the experts that code uses |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39.2 GB) or **Gyro-M** | Swift thinks for a shorter time before it answers |
|
||||
| **48 GB** | **IQ3_XXS** (47.0 GB) | Slower, better |
|
||||
| **64 GB** | **IQ3_S** (54.8 GB) | Matches the full model on the published tests |
|
||||
| **96 GB or more** | Unsloth's **UD-IQ4_XS** to **Q8_0** | Closest to the full model |
|
||||
|
||||
Less RAM than a row says can still work, only slower: Strata switches to its
|
||||
[low-RAM mode](docs/MODELS.md#a-big-graphics-card-and-little-ram) by itself. Also available:
|
||||
[NVFP4](docs/NVFP4.md) for NVIDIA cards, [Qwen3.6-35B-A3B](docs/QWEN36.md) (smaller, several requests at once) and
|
||||
[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs). Every size and what fits where:
|
||||
[docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
### DeepSeek V4.1-Flash (Terminal, from 0.2.4)
|
||||
|
||||
A much bigger model that runs on one 12 GB graphics card with 46 to 64 GB of RAM. Strata keeps the busiest parts in memory and
|
||||
reads the rest from your SSD. [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF)
|
||||
made the sets. It runs on NVIDIA RTX 30, 40 and 50 cards and the Radeon AI PRO R9700. Pick it in `strata launch`; the desktop
|
||||
app does not offer it yet.
|
||||
|
||||
| Set | Download | Quality | Writes code at |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M** (1.75 bits) | 239 GB | The smallest; weaker on plain prose | not measured on a PC yet |
|
||||
| **IQ2_XXS** (2.00 bits) | 257 GB | Good balance | 10 tokens/s (RTX 3060), 5 (RTX 5070), 48 (4x R9700) |
|
||||
| **IQ2_XS** (2.25 bits) | 274 GB | Closest to the original | 15 tokens/s (one R9700), 80 (4x R9700) |
|
||||
| **IQ4_NL** (Engram table) | in every download above | The lookup table all three sets share | |
|
||||
|
||||
Start it with `strata launch --model ist-das-2.00bpw` (or `ist-das-1.75bpw`, `ist-das-2.25bpw`). Quality numbers, speeds
|
||||
per PC and the manual install: [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
|
||||
|
||||
## How fast is it?
|
||||
|
||||
Based on a 12 GB graphics card (RTX 5070, 64 GB RAM). Results from other cards:
|
||||
[speed of each model](docs/MODELS.md#how-fast-is-each-size) and [community results](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
| Model | Answers appear at | Reads your prompt at |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████████` 94 tokens/s | `████████████` 2,650 tokens/s |
|
||||
| **IQ2_XS** | `██████████░░` 79 tokens/s | `█████████░░░` 2,090 tokens/s |
|
||||
| **IQ3_XXS** | `████████░░░░` 62 tokens/s | `████████░░░░` 1,750 tokens/s |
|
||||
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1,620 tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2,180 tokens/s |
|
||||
|
||||
On a 16 GB AMD card (RX 9070 XT, 47 GB RAM):
|
||||
|
||||
| Model | Answers appear at | Reads your prompt at |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████░░░░` 60 tokens/s | `█████░░░░░░░` 1,160 tokens/s |
|
||||
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1,110 tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1,420 tokens/s |
|
||||
|
||||
A token is about ¾ of a word, so 60 tokens per second is faster than you can read. "Reads your prompt" is how fast Strata
|
||||
takes in a long document, code or chat history.
|
||||
|
||||
## Using it
|
||||
|
||||
### In the app
|
||||
|
||||
Chat, a **Monitor** of the model and your GPU, CPU and RAM, a **Models** page and **Settings**. The chat can work in a
|
||||
folder on your PC. In **Manual** mode it asks before it changes a file or runs a command. It can search the web
|
||||
(DuckDuckGo or Bing, no account); the search words are the one thing that leaves your PC.
|
||||
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
|
||||
<sub>The agent wrote a voxel pagoda garden as one HTML file, and the page opens in the preview next to the chat</sub></p>
|
||||
|
||||
### In the Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<sub><code>strata monitor</code> while the model writes a reply: speed, GPU, memory and the recent requests</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve and print the address
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor # watch speed, GPU, memory and requests
|
||||
strata update # get the newest engine
|
||||
strata doctor # check the installed files
|
||||
```
|
||||
|
||||
To reach Strata from your phone or another PC, use `strata launch --access lan`. It makes an API key for you.
|
||||
`strata --help` lists everything, and [docs/TERMINAL.md](docs/TERMINAL.md) explains it.
|
||||
|
||||
### With AI agents and other apps
|
||||
|
||||
An agent installs Strata with the one-line command above and runs `strata launch --model NAME --yes --no-tui`. Steps,
|
||||
flags and exit codes: [docs/AI_SETUP.md](docs/AI_SETUP.md). Repository rules for agents: [AGENTS.md](AGENTS.md).
|
||||
|
||||
| Your app speaks | Base URL |
|
||||
| --- | --- |
|
||||
| OpenAI API (most apps and agents) | `http://127.0.0.1:8080/v1` (any API key, any model name) |
|
||||
| Anthropic API (Claude Code) | `http://127.0.0.1:8080`, without `/v1`. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([setup](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` prints the exact settings for OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent and OpenClaw,
|
||||
and `strata connect opencode --write` writes them for you (with a backup).
|
||||
|
||||
### Good to know
|
||||
|
||||
- **Pictures** are on for every model that can read them. Attach or paste one in the chat, or send it from your app. Details: [docs/IMAGES.md](docs/IMAGES.md).
|
||||
- **Thinking** can be off, low, medium or high, in the chat menu or your app's "reasoning effort". Off is fastest.
|
||||
- **The first message** of a chat is read in full, about 1 minute per 30,000 tokens. Follow-ups start in seconds.
|
||||
- **One request at a time** by default. For more, use `--parallel N` ([BATCHING.md](docs/BATCHING.md)). On a 12 GB card each answer gets slower.
|
||||
|
||||
## Build from source
|
||||
|
||||
<details>
|
||||
<summary>Windows, Linux and Mac from the repository</summary>
|
||||
|
||||
[Download Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) and unzip it, or `git clone` it.
|
||||
**Windows:** double-click `START-HERE.bat`. **Linux:** run `./setup.sh` in the Strata folder.
|
||||
|
||||
**Mac (Apple Silicon, macOS 15 or newer):** install Apple's Command Line Tools once with `xcode-select --install`
|
||||
(Xcode itself is not needed). Open Terminal in the Strata folder and run `./setup.sh`. It builds the Mac engine on your
|
||||
Mac (about 7 minutes), asks the same questions, downloads the model and starts it. Next time, `./setup.sh` starts it
|
||||
right away. Which size fits your Mac: [docs/MAC.md](docs/MAC.md).
|
||||
|
||||
The installer finds your card and sets up the right engine. It asks which model and size, how much context (how much text
|
||||
the model keeps in mind) and whether it should read pictures. Press Enter for the recommended answer. Then it downloads the
|
||||
model (about 70 GB) and starts it. If the download stops, run it again and it continues. Your browser opens the Strata
|
||||
page at `http://127.0.0.1:8080`.
|
||||
|
||||
> While the model starts, your PC can be slow or stop responding for 1-3 minutes (longest the first time). Strata loads
|
||||
> 35-55 GB into your RAM and locks part of it for the graphics card. Wait, and don't close the window.
|
||||
|
||||
Next time, run `START-HERE.bat` (or `./setup.sh`) again. It starts right away and downloads nothing twice. Close its
|
||||
window to stop the model. `UPDATE.bat` (`./update.sh`) updates Strata without starting it. Updating, Docker, several cards,
|
||||
where the files go and every option: [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
To measure your PC, run `BENCH.bat` (`./bench.sh`). It takes 5-10 minutes and writes a report you can share
|
||||
([how](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
|
||||
|
||||
**Optional: more speed on one card when the whole model fits in RAM.** Two opt-in settings by RubenTorney (#1904, #1905).
|
||||
They help when you run one graphics card and your RAM holds every expert, for example Q2_0 or IQ3_XXS on a 64 GB PC. Open
|
||||
`strata-<model>.json` in the Strata data folder ([where](docs/INSTALL.md#where-things-are-stored)) and add:
|
||||
|
||||
- `"--adapt-async", "1"` to the `"args"` list. The expert cache then updates on a helper thread instead of pausing the
|
||||
answer. Measured on an RTX 5070 (Windows, Q2_0): +4-5% decode, +7-10% on code and long documents. Answers can differ
|
||||
slightly from the default and stay the same from run to run.
|
||||
- `"STRATA_PIN_BY_NEED": "1"` in the `"env"` block (Windows). Windows can lock only about half the RAM for the graphics
|
||||
card, and this locks the experts the card needs first. Measured on the RTX 5070: +4.4% prompt reading.
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
Restart Strata after the change. To undo, remove the two entries.
|
||||
Follow the prompts to download and start a model. [Terminal guide](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
</details>
|
||||
|
||||
## How does it work?
|
||||
<p align="center">
|
||||
<img src="docs/media/terminal-monitor.png" width="900" alt="Strata terminal app showing model activity, speed, memory and recent requests">
|
||||
<br><sub>Watch your model and hardware while it runs.</sub>
|
||||
</p>
|
||||
|
||||
Models like this one usually run on servers with hundreds of gigabytes of graphics memory. Your graphics card has
|
||||
12-24 GB. Strata makes the model fit by sharing the work across your whole PC. Picture a library.
|
||||
<a id="build-from-source"></a>
|
||||
|
||||
- The model is a team of 24,576 small specialists ("experts"), like 24,576 reference books. Each word needs only 10 of them.
|
||||
- **Your graphics card is the desk.** It is small and fast. The roughly 4,000 books used most often lie open on it.
|
||||
- **Your RAM is the shelves in the reading room.** All 24,576 books are there. When a word needs a book that is not on the
|
||||
desk, your processor reads it from the shelf at once, while the card keeps working.
|
||||
- **Your SSD is the archive.** It holds a 29 GB lookup table. The model reads a few rows of it per word, so it does not
|
||||
need to sit in RAM.
|
||||
[Build from source](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [Full install guide](docs/INSTALL.md)
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
|
||||
<a id="what-you-need"></a>
|
||||
|
||||
A small helper also guesses the next few words. The big model checks all of them at once and keeps the right ones. You get
|
||||
the same answer, 1.6-1.8x sooner. Long texts are read in big pieces, up to 8,192 tokens at a time, at over 1,000 tokens
|
||||
per second.
|
||||
## Hardware
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
|
||||
| Supported hardware | Guide |
|
||||
| --- | --- |
|
||||
| **NVIDIA GeForce RTX 20 / 30 / 40 / 50 series** | [NVIDIA setup](docs/INSTALL.md#what-you-need) |
|
||||
| **AMD Radeon RX 7700 / 7800 / 7900, RX 9060 / 9070, AI PRO R9700** | [AMD setup](docs/AMD_HIP.md) |
|
||||
| **AMD Radeon RX 6800 / 6900 · experimental** | [RX 6000 setup](docs/AMD_HIP.md#rdna2-gfx1030) |
|
||||
| **Intel Arc · Linux** | [Intel setup](docs/INTEL_ARC.md) |
|
||||
| **Apple Silicon · M1 or newer** | [Mac setup](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max · Strix Halo** | [Strix Halo setup](docs/STRIX_HALO.md) |
|
||||
|
||||
The longer explanation: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Every part and its numbers:
|
||||
[the details](docs/DETAILS.md#how-it-works) and the [paper](docs/paper/Strata-Paper.pdf).
|
||||
<a id="using-it"></a>
|
||||
|
||||
## Something went wrong?
|
||||
## Tools
|
||||
|
||||
- **My PC froze the first time Strata started.** It loads the model into RAM. Wait, and don't close the window. Still
|
||||
frozen after 10 minutes? Restart the PC, close other programs and try again, or pick a smaller size.
|
||||
- **It stopped while downloading or installing.** Start the app or `strata launch` again. It continues where it stopped.
|
||||
- **It is very slow, or it says "the engine stopped unexpectedly".** Your PC is short on free RAM. Close other programs
|
||||
(browsers use a lot), or pick a smaller size such as Q2_0 or IQ2_XS.
|
||||
- **It says there is no engine for this PC.** In the app, open **Settings, Engine** and press **Update engine**. In the
|
||||
Terminal, run `strata update`.
|
||||
- **It says port 8080 is already in use.** Strata is already running. Look for its window.
|
||||
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw**, and more through the OpenAI- and Anthropic-compatible APIs.
|
||||
|
||||
More fixes: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Still stuck? Open an
|
||||
[issue](https://github.com/Niko1221/Strata/issues) and attach the logs: in the app from **Settings, Engine, Diagnostics,
|
||||
Save logs**, in the Terminal the output of `strata doctor`. For a security problem, report it privately: [SECURITY.md](SECURITY.md).
|
||||
Run **`strata connect`** to see setup instructions for your tool. [Connection guide](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect).
|
||||
|
||||
## Credits and license
|
||||
<a id="guides"></a>
|
||||
<a id="how-fast-is-it"></a>
|
||||
<a id="how-does-it-work"></a>
|
||||
<a id="something-went-wrong"></a>
|
||||
|
||||
The model is [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) by the Qwen team. It was compressed by
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5), agentionai (Gyro)
|
||||
and Unsloth. ISTA-DASLab also made the DeepSeek V4.1-Flash sets. Strata uses parts of
|
||||
[llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). All credits: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits).
|
||||
Strata is open source under the [MIT License](LICENSE). A few parts and every model have their own licenses
|
||||
([which ones](docs/HOW_IT_WORKS.md#license)).
|
||||
## Help
|
||||
|
||||
## Contributing
|
||||
|
||||
Found a bug, or want to send a change or a benchmark? Read [CONTRIBUTING.md](CONTRIBUTING.md) first. It says what to
|
||||
include so that others can check your work.
|
||||
[All guides](docs) · [Troubleshooting](docs/TROUBLESHOOTING.md) · [Report a bug](https://github.com/Niko1221/Strata/issues/new/choose) ·
|
||||
[Benchmarks](docs/COMMUNITY_BENCHMARKS.md) · [How it works](docs/HOW_IT_WORKS.md) · [Contribute](CONTRIBUTING.md)
|
||||
|
||||
## Support Strata
|
||||
|
||||
Strata is free and MIT-licensed. I build it and test it on whatever hardware I can get. If it runs well on your PC, a coffee helps
|
||||
me keep testing new cards and fixing what you report. No pressure. Using it and reporting what you find helps just as much.
|
||||
**Help keep local AI open to everyone.** A coffee helps fund development and testing on more hardware.
|
||||
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: support Strata" height="48"></a>
|
||||
|
||||
[MIT license](LICENSE) · [Credits and model licenses](docs/HOW_IT_WORKS.md#credits) · [Privacy](docs/SAFETY.md)
|
||||
|
||||
<a id="option-c-let-your-ai-set-it-up"></a>
|
||||
|
||||
<sub>For AI tools and automated readers: <a href="AGENTS.md">technical reference</a> · <a href="docs/AI_SETUP.md">setup instructions</a>.</sub>
|
||||
|
||||
**Sponsorships, hardware partnerships or collaborations:** [[email protected]](mailto:[email protected]).
|
||||
|
||||
Interested in sponsoring Strata? Contact me via [GitHub](https://github.com/Niko1221).
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="340">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
<h3 align="center">Uruchom potężną sztuczną inteligencję na własnym komputerze.</h3>
|
||||
<p align="center">Rozmawiaj, programuj i pracuj z obrazami — lokalnie.</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#modele">Modele</a> · <a href="#instalacja"><b>Instalacja</b></a> ·
|
||||
<a href="#sprzęt">Sprzęt</a> · <a href="#narzędzia">Narzędzia</a> ·
|
||||
<a href="#pomoc">Pomoc</a> · <a href="#wesprzyj-stratę">Wsparcie</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Pobierz Stratę"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licencja MIT"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Wesprzyj Stratę na Buy Me a Coffee"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><sub><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a> · <b>Polski</b></sub></p>
|
||||
|
||||
**Bezpłatna, z otwartym kodem źródłowym i do uruchomienia u siebie.** Używaj Straty na swoim komputerze albo razem z ulubionymi narzędziami do programowania. Konto nie jest potrzebne.
|
||||
|
||||
## Modele
|
||||
|
||||
Najpierw polecamy **ISTA-DASLab**, potem **Unsloth** i innych dostawców — o ile zgodny model mieści się w pamięci.
|
||||
|
||||
<details>
|
||||
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> <b>Qwen3.8-Flash-Next</b></summary>
|
||||
|
||||
| Dostawca | Do wyboru |
|
||||
| --- | --- |
|
||||
| **ISTA-DASLab · polecany** | Q2\_0, IQ2\_XS, IQ3\_XXS, IQ3\_S. [Coder: IQ1\_M](docs/MODELS.md#coder) do programowania. |
|
||||
| **Unsloth** | UD-IQ4\_XS, UD-Q4\_K\_XL, UD-Q5\_K\_XL, UD-Q6\_K\_XL, Q8\_0 |
|
||||
| **Gyro · agentionai** | [Gyro-S, Gyro-M](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF). Zużywa mniej pamięci, ale działa wolniej. |
|
||||
| **Inne opcje** | [Swift 1.5](docs/MODELS.md#swift-15), [NVFP4](docs/NVFP4.md), [OrcaRouter](docs/ORCA.md) |
|
||||
|
||||
[Porównaj rozmiary, wymagania pamięciowe i zgodność](docs/MODELS.md).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> <b>Qwen3.6-35B-A3B</b></summary>
|
||||
|
||||
**Unsloth:** UD-Q4_K_XL, UD-Q6_K_XL, Q8_0. To obecnie udokumentowane pakiety dla tego modelu.
|
||||
|
||||
[Przewodnik po modelu i zgodność](docs/QWEN36.md).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><img src="docs/media/models/deepseek.svg" width="22" alt="" align="middle"> <b>DeepSeekV4.1-Flash-1017</b></summary>
|
||||
|
||||
W **aplikacji na komputer** i w **Terminalu**. To, co nie mieści się w pamięci, jest czytane z dysku SSD, więc potrzebujesz
|
||||
dużo wolnego miejsca na dysku.
|
||||
|
||||
| Zestaw (IST-DAS) | Do pobrania | Potrzebne wolne miejsce |
|
||||
| --- | ---: | ---: |
|
||||
| 1.75 bpw | 239 GB | około 245 GB |
|
||||
| 2.00 bpw | 257 GB | około 265 GB |
|
||||
| 2.25 bpw | 274 GB | około 280 GB |
|
||||
|
||||
Działa na kartach NVIDIA RTX serii 30, 40 i 50 (Windows i Linux), AMD Radeon AI PRO R9700 (Linux) oraz AMD Ryzen AI Max — Strix Halo
|
||||
(Windows). Instalacja pobiera pliki i silnik DeepSeek, a potem uruchamia model.
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
```
|
||||
|
||||
Zmierzone na zestawie 2.00 bpw w Strata 0.2.5 (Strix Halo: 0.2.7). „Wyjście” to szybkość odpowiedzi, „prompt” — jak szybko Strata czyta twój tekst.
|
||||
|
||||
| Sprzęt | Wyjście | Prompt |
|
||||
| --- | ---: | ---: |
|
||||
| RTX 5070 12 GB, 64 GB RAM | 4–6 tokenów/s | około 110 tokenów/s (4K) |
|
||||
| RTX 3060 12 GB, 46 GB RAM | 9–11 tokenów/s | około 155 tokenów/s (4K) |
|
||||
| 1x Radeon AI PRO R9700, 251 GB RAM | 21–25 tokenów/s | 320–360 tokenów/s (8K) |
|
||||
| 4x Radeon AI PRO R9700 | 74–77 tokenów/s | 400–450 tokenów/s (8K) |
|
||||
| Ryzen AI Max+ 395 (Strix Halo), 128 GB pamięci współdzielonej | 19–21 tokenów/s (kod), 11 (czat) | 87 tokenów/s (4K), 125 tokenów/s (8K) |
|
||||
|
||||
[Pamięć, dysk i szczegóły](docs/DEEPSEEK41.md) · [Informacje o wydaniach](https://github.com/Niko1221/Strata/releases)
|
||||
|
||||
</details>
|
||||
|
||||
<a id="the-other-sizes"></a>
|
||||
|
||||
Nie wiesz, co się zmieści? [Wybierz model do swojego komputera](docs/MODELS.md#pick-by-memory).
|
||||
|
||||
## Instalacja
|
||||
|
||||
<a id="option-a-the-strata-app"></a>
|
||||
|
||||
### Aplikacja na komputer
|
||||
|
||||
**[Pobierz Stratę dla Windows, Linuxa lub Maca →](https://github.com/Niko1221/Strata/releases/latest)**
|
||||
|
||||
Zainstaluj aplikację, wybierz model i zacznij rozmawiać. Dla każdego systemu jest jeden instalator. Przy pierwszym
|
||||
uruchomieniu aplikacja pobiera silnik dla twojej karty graficznej. [Pomoc z instalatorem](docs/INSTALL.md#the-app).
|
||||
|
||||
<sub>Pliki: `Strata-0.2.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Aplikacja Strata na komputer: czat i podgląd kodu na żywo obok siebie">
|
||||
<br><sub>Czat, pliki i podgląd na żywo w jednym oknie.</sub>
|
||||
</p>
|
||||
|
||||
<a id="option-b-the-terminal-one-line"></a>
|
||||
|
||||
### Aplikacja Terminal
|
||||
|
||||
Wolisz terminal? Zainstaluj Stratę, a potem uruchom **`strata launch`**.
|
||||
|
||||
<details open>
|
||||
<summary><b>Polecenia instalacji</b></summary>
|
||||
|
||||
**Linux / Mac**
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
**Windows PowerShell**
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
W Windows otwórz potem nowe okno terminala. Następnie:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
Postępuj zgodnie z podpowiedziami, aby pobrać i uruchomić model. [Przewodnik po Terminalu](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
</details>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/terminal-monitor.png" width="900" alt="Aplikacja Terminal Straty pokazująca aktywność modelu, szybkość, pamięć i ostatnie zapytania">
|
||||
<br><sub>Obserwuj model i sprzęt w trakcie pracy.</sub>
|
||||
</p>
|
||||
|
||||
<a id="build-from-source"></a>
|
||||
|
||||
[Budowanie ze źródeł](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [Pełny przewodnik instalacji](docs/INSTALL.md)
|
||||
|
||||
<a id="what-you-need"></a>
|
||||
|
||||
## Sprzęt
|
||||
|
||||
| Obsługiwany sprzęt | Przewodnik |
|
||||
| --- | --- |
|
||||
| **NVIDIA GeForce RTX serii 20 / 30 / 40 / 50** | [Konfiguracja NVIDIA](docs/INSTALL.md#what-you-need) |
|
||||
| **AMD Radeon RX 7700 / 7800 / 7900, RX 9060 / 9070, AI PRO R9700** | [Konfiguracja AMD](docs/AMD_HIP.md) |
|
||||
| **AMD Radeon RX 6800 / 6900 · eksperymentalnie** | [Konfiguracja RX 6000](docs/AMD_HIP.md#rdna2-gfx1030) |
|
||||
| **Intel Arc · Linux** | [Konfiguracja Intel](docs/INTEL_ARC.md) |
|
||||
| **Apple Silicon · M1 lub nowszy** | [Konfiguracja Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max · Strix Halo** | [Konfiguracja Strix Halo](docs/STRIX_HALO.md) |
|
||||
|
||||
<a id="using-it"></a>
|
||||
|
||||
## Narzędzia
|
||||
|
||||
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw** i inne — przez API zgodne z OpenAI i Anthropic.
|
||||
|
||||
Uruchom **`strata connect`**, aby zobaczyć instrukcję konfiguracji dla swojego narzędzia. [Przewodnik po połączeniach](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect).
|
||||
|
||||
<a id="guides"></a>
|
||||
<a id="how-fast-is-it"></a>
|
||||
<a id="how-does-it-work"></a>
|
||||
<a id="something-went-wrong"></a>
|
||||
|
||||
## Pomoc
|
||||
|
||||
[Wszystkie przewodniki](docs) · [Rozwiązywanie problemów](docs/TROUBLESHOOTING.md) · [Zgłoś błąd](https://github.com/Niko1221/Strata/issues/new/choose) ·
|
||||
[Testy wydajności](docs/COMMUNITY_BENCHMARKS.md) · [Jak to działa](docs/HOW_IT_WORKS.md) · [Współtworzenie](CONTRIBUTING.md)
|
||||
|
||||
## Wesprzyj Stratę
|
||||
|
||||
**Pomóż zachować lokalną sztuczną inteligencję otwartą dla wszystkich.** Kawa pomaga finansować rozwój i testy na większej liczbie sprzętu.
|
||||
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: wesprzyj Stratę" height="48"></a>
|
||||
|
||||
[Licencja MIT](LICENSE) · [Podziękowania i licencje modeli](docs/HOW_IT_WORKS.md#credits) · [Prywatność](docs/SAFETY.md)
|
||||
|
||||
<a id="option-c-let-your-ai-set-it-up"></a>
|
||||
|
||||
<sub>Dla narzędzi AI i automatycznych czytników: <a href="AGENTS.md">dokumentacja techniczna</a> · <a href="docs/AI_SETUP.md">instrukcja instalacji</a>.</sub>
|
||||
|
||||
**Sponsoring, partnerstwa sprzętowe i współpraca:** [[email protected]](mailto:[email protected]).
|
||||
+122
-294
@@ -1,360 +1,188 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="340">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <b>Português</b></p>
|
||||
|
||||
<h3 align="center">Rode um modelo de ponta no seu próprio PC gamer</h3>
|
||||
|
||||
<p align="center">Suporte a NVIDIA, AMD, Intel · Windows, Linux, Mac · gratuito e de código aberto</p>
|
||||
<h3 align="center">IA poderosa no seu próprio computador.</h3>
|
||||
<p align="center">Converse, programe e trabalhe com imagens, localmente.</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Última versão"></a>
|
||||
<a href="#modelos">Modelos</a> · <a href="#instalação"><b>Instalação</b></a> ·
|
||||
<a href="#hardware">Hardware</a> · <a href="#ferramentas">Ferramentas</a> ·
|
||||
<a href="#ajuda">Ajuda</a> · <a href="#apoie-o-strata">Apoiar</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Baixar o Strata"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licença MIT"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="Estrelas no GitHub"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=black" alt="Me pague um café"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Apoie o Strata no Buy Me a Coffee"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Um jardim de pagodes em voxels que o modelo do Strata escreveu, rodando no navegador"></a><br>
|
||||
<sub>Um jardim de pagodes em voxels, gerado com um único prompt, rodando numa RTX 5070 com o Strata (IQ3_S, contexto de 128K) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">vídeo completo (49 s)</a></sub></p>
|
||||
<p align="center"><sub><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <b>Português</b> · <a href="README.pl.md">Polski</a></sub></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#instalação"><b>Instalação</b></a> ·
|
||||
<a href="#modelos">Modelos</a> ·
|
||||
<a href="#qual-é-a-velocidade">Velocidade</a> ·
|
||||
<a href="#como-usar">Como usar</a> ·
|
||||
<a href="#como-funciona">Como funciona</a> ·
|
||||
<a href="#algo-deu-errado">Ajuda</a> ·
|
||||
<a href="#apoie-o-strata">Apoiar</a>
|
||||
</p>
|
||||
**Gratuito, de código aberto e seu para executar.** Use o Strata no desktop ou com suas ferramentas de programação favoritas. Não precisa de conta.
|
||||
|
||||
O Strata roda o **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, um modelo de 125 bilhões de parâmetros que
|
||||
normalmente precisa de um servidor, num PC comum. A partir da 0.2.4 ele também roda o **DeepSeek V4.1-Flash** no Terminal. Ele
|
||||
conversa, escreve código, lê imagens e funciona com os seus aplicativos e agentes de programação. Suas conversas e arquivos
|
||||
ficam no seu PC.
|
||||
## Modelos
|
||||
|
||||
- **Roda num PC gamer.** 32 GB somando memória RAM e memória da placa de vídeo já bastam para começar.
|
||||
- **Funciona com as suas ferramentas.** Claude Code, Codex CLI, OpenCode e outros agentes se conectam pelas APIs da OpenAI e da Anthropic.
|
||||
- **Lê imagens**, ativado por padrão.
|
||||
- **Gratuito e privado.** Licença MIT, sem conta, sem nuvem, sem telemetria ([o que ele baixa e quando](docs/SAFETY.md)).
|
||||
Recomendamos **primeiro a ISTA-DASLab**, depois a **Unsloth** e outros provedores, quando um modelo compatível cabe na memória.
|
||||
|
||||
## O que você precisa
|
||||
<details>
|
||||
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> <b>Qwen3.8-Flash-Next</b></summary>
|
||||
|
||||
**Você precisa de pelo menos 32 GB de memória: RAM e memória da placa de vídeo (VRAM) somadas.** Num Mac ou num Ryzen AI Max, isso é a
|
||||
memória unificada. Um PC de 32 GB com uma placa de 12 a 16 GB roda os tamanhos menores, e 64 GB rodam todos os tamanhos.
|
||||
|
||||
| Funciona em | Guia |
|
||||
| Provedor | Opções |
|
||||
| --- | --- |
|
||||
| **NVIDIA** GeForce RTX série 20, 30, 40 e 50, 12 GB ou mais | [Instalação](#instalação) |
|
||||
| **AMD** Radeon RX 6800 e mais novas, Radeon AI PRO R9700 | [Placas AMD](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md) |
|
||||
| **Mac com Apple Silicon**, M1 ou mais novo, macOS 15 ou mais novo | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** e outros PCs de memória unificada | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Placas e processadores mais antigos | [GPUs antigas](docs/OLDER_GPUS.md) |
|
||||
| **ISTA-DASLab · recomendado** | Q2\_0, IQ2\_XS, IQ3\_XXS, IQ3\_S. [Coder: IQ1\_M](docs/MODELS.md#coder) para programar. |
|
||||
| **Unsloth** | UD-IQ4\_XS, UD-Q4\_K\_XL, UD-Q5\_K\_XL, UD-Q6\_K\_XL, Q8\_0 |
|
||||
| **Gyro · agentionai** | [Gyro-S, Gyro-M](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF). Usa menos memória, mas é mais lento. |
|
||||
| **Outras opções** | [Swift 1.5](docs/MODELS.md#swift-15), [NVFP4](docs/NVFP4.md), [OrcaRouter](docs/ORCA.md) |
|
||||
|
||||
Você também precisa de cerca de 80 GB de espaço livre no disco (um SSD), Windows 10 / 11, Linux ou macOS, e um driver de vídeo
|
||||
atualizado. Duas ou três placas da mesma marca podem dividir um modelo ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
[Compare tamanhos, necessidades de memória e compatibilidade](docs/MODELS.md).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> <b>Qwen3.6-35B-A3B</b></summary>
|
||||
|
||||
**Unsloth:** UD-Q4_K_XL, UD-Q6_K_XL, Q8_0. Esses são os pacotes documentados atualmente para este modelo.
|
||||
|
||||
[Guia do modelo e compatibilidade](docs/QWEN36.md).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><img src="docs/media/models/deepseek.svg" width="22" alt="" align="middle"> <b>DeepSeekV4.1-Flash-1017</b></summary>
|
||||
|
||||
No **aplicativo de desktop** e no **Terminal**. O que não cabe na memória é lido do seu SSD, por isso é preciso muito espaço livre em disco.
|
||||
|
||||
| Conjunto (IST-DAS) | Download | Espaço livre necessário |
|
||||
| --- | ---: | ---: |
|
||||
| 1.75 bpw | 239 GB | cerca de 245 GB |
|
||||
| 2.00 bpw | 257 GB | cerca de 265 GB |
|
||||
| 2.25 bpw | 274 GB | cerca de 280 GB |
|
||||
|
||||
Roda em NVIDIA RTX 30, 40 e 50 (Windows e Linux) e em AMD Radeon AI PRO R9700 (Linux). A instalação baixa os arquivos e o motor do DeepSeek e depois inicia o modelo.
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
```
|
||||
|
||||
Medido com o conjunto de 2.00 bpw no Strata 0.2.5. “Saída” é a velocidade da resposta; “Prompt” é a rapidez com que o Strata lê o seu texto.
|
||||
|
||||
| Hardware | Saída | Prompt |
|
||||
| --- | ---: | ---: |
|
||||
| RTX 5070 12 GB, 64 GB de RAM | 4 a 6 tokens/s | cerca de 110 tokens/s (4K) |
|
||||
| RTX 3060 12 GB, 46 GB de RAM | 9 a 11 tokens/s | cerca de 155 tokens/s (4K) |
|
||||
| 1x Radeon AI PRO R9700, 251 GB de RAM | 21 a 25 tokens/s | 320 a 360 tokens/s (8K) |
|
||||
| 4x Radeon AI PRO R9700 | 74 a 77 tokens/s | 400 a 450 tokens/s (8K) |
|
||||
|
||||
[Memória, disco e detalhes](docs/DEEPSEEK41.md) · [Notas da versão](https://github.com/Niko1221/Strata/releases)
|
||||
|
||||
</details>
|
||||
|
||||
<a id="the-other-sizes"></a>
|
||||
|
||||
Não sabe o que cabe? [Escolha um modelo para o seu computador](docs/MODELS.md#pick-by-memory).
|
||||
|
||||
## Instalação
|
||||
|
||||
### 1. O app Strata (o mais fácil)
|
||||
<a id="option-a-the-strata-app"></a>
|
||||
|
||||
Baixe o arquivo para o seu computador, abra e clique em instalar no modelo que ele recomenda.
|
||||
### Aplicativo de desktop
|
||||
|
||||
**Windows 10 / 11**
|
||||
**[Baixar o Strata para Windows, Linux ou Mac →](https://github.com/Niko1221/Strata/releases/latest)**
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-windows-x64-setup.exe)
|
||||
Instale o aplicativo, escolha um modelo e comece a conversar. Há um instalador por sistema. Na primeira vez que abre, o aplicativo baixa o motor da sua placa de vídeo. [Ajuda com o instalador](docs/INSTALL.md#the-app).
|
||||
|
||||
**Linux**
|
||||
<sub>Arquivos: `Strata-0.2.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-linux-x86_64.AppImage)
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Aplicativo de desktop do Strata com um chat e uma prévia do código ao vivo lado a lado">
|
||||
<br><sub>Chat, arquivos e prévia ao vivo em uma só janela.</sub>
|
||||
</p>
|
||||
|
||||
**Mac** (Apple Silicon, macOS 15 ou mais novo)
|
||||
<a id="option-b-the-terminal-one-line"></a>
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-macos-arm64.dmg)
|
||||
### Aplicativo de Terminal
|
||||
|
||||
O aplicativo escolhe sozinho o mecanismo certo para a sua placa de vídeo. Todos os arquivos estão na
|
||||
[página de releases](https://github.com/Niko1221/Strata/releases/latest).
|
||||
Prefere o terminal? Instale o Strata e execute **`strata launch`**.
|
||||
|
||||
| Primeira inicialização | O que fazer |
|
||||
| --- | --- |
|
||||
| **Windows** | Execute o `.exe`. Não precisa de direitos de administrador. Se o Windows disser "O Windows protegeu o computador", escolha **Mais informações** e depois **Executar assim mesmo**. |
|
||||
| **Linux** | Torne o arquivo executável (`chmod +x`) e inicie. É preciso ter o FUSE 2: `sudo apt install libfuse2t64` (Ubuntu 24.04 e mais novos) ou `libfuse2` (22.04). |
|
||||
| **Mac** | Abra o `.dmg` e arraste o Strata para Aplicativos. |
|
||||
<details open>
|
||||
<summary><b>Comandos de instalação</b></summary>
|
||||
|
||||
O download do modelo é grande (40 a 70 GB). Se parar, continua de onde parou.
|
||||
|
||||
### 2. O Terminal (uma linha)
|
||||
|
||||
O comando `strata`, para scripts, servidores e agentes de IA. É também o jeito de rodar o DeepSeek.
|
||||
|
||||
Linux e Mac:
|
||||
**Linux / Mac**
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
**Windows PowerShell**
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
Depois inicie. Ele faz uma pergunta por vez (modelo, contexto, imagens, quem pode se conectar), baixa o que precisa e começa
|
||||
a servir:
|
||||
No Windows, abra um novo terminal depois. Em seguida:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
Não precisa de direitos de administrador. A instalação confere o SHA-256 de tudo que baixa. No Windows, abra uma nova janela do
|
||||
terminal depois da instalação. Todas as telas e flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### 3. Deixe a sua IA configurar
|
||||
|
||||
Cole isto no Claude Code, Cursor, Codex, GitHub Copilot ou outro assistente de programação:
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
Ele confere a sua placa de vídeo, a RAM e o disco, escolhe o modelo que cabe, instala e inicia, e diz como conectar os seus
|
||||
aplicativos. Ferramentas de IA também podem instalar, iniciar e parar o Strata pelo [servidor MCP](docs/MCP_SERVER.md) dele.
|
||||
|
||||
Prefere compilar por conta própria? Veja [Compilar a partir do código-fonte](#compilar-a-partir-do-código-fonte).
|
||||
|
||||
## Modelos
|
||||
|
||||
O app e o `strata launch` recomendam um tamanho para a sua memória. Os tamanhos menores são mais rápidos. Os maiores são um
|
||||
pouco mais inteligentes.
|
||||
|
||||
### Pouca memória? Experimente o Gyro
|
||||
|
||||
O [Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF), da agentionai, é a menor forma de rodar o
|
||||
Qwen3.8-Flash-Next. Ele roda em NVIDIA, AMD, Intel Arc e Apple Silicon.
|
||||
|
||||
| Tamanho | Memória necessária (RAM + VRAM) | Download | Serve para |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27,6 GB | 58,5 GB | PCs onde nenhum outro tamanho cabe. 9 tokens/s num Mac mini de 32 GB. |
|
||||
| **Gyro-M** | 35 GB | 92,0 GB | Qualidade próxima à do IQ3_XXS, com 20% menos memória da GPU. |
|
||||
|
||||
Inicie com `strata launch --model gyro-s`. Detalhes: [docs/GYRO.md](docs/GYRO.md).
|
||||
|
||||
### Os tamanhos do Qwen3.8-Flash-Next
|
||||
|
||||
| Sua memória (RAM + VRAM) | Escolha | Por quê |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Gyro-S**, ou o **[Coder](docs/MODELS.md#coder)** para código | O Coder mantém os experts que o código usa |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB) ou **Gyro-M** | O Swift pensa por menos tempo antes de responder |
|
||||
| **48 GB** | **IQ3_XXS** (47,0 GB) | Mais lento, melhor |
|
||||
| **64 GB** | **IQ3_S** (54,8 GB) | Empata com o modelo completo nos testes publicados |
|
||||
| **96 GB ou mais** | **UD-IQ4_XS** a **Q8_0** da Unsloth | O mais perto do modelo completo |
|
||||
|
||||
Com menos RAM do que a linha indica, ainda pode funcionar, só que mais devagar: o Strata muda sozinho para o
|
||||
[modo de pouca RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Há também:
|
||||
[NVFP4](docs/NVFP4.md) para placas NVIDIA, [Qwen3.6-35B-A3B](docs/QWEN36.md) (menor, vários pedidos ao mesmo tempo) e
|
||||
[o Uncensored IQ3_XXS da OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs). Todos os tamanhos e o que cabe em cada configuração:
|
||||
[docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
### DeepSeek V4.1-Flash (Terminal, a partir da 0.2.4)
|
||||
|
||||
Um modelo bem maior, que roda numa única placa de vídeo de 12 GB com 46 a 64 GB de RAM. O Strata mantém na memória as partes mais
|
||||
usadas e lê o resto do seu SSD. A [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF)
|
||||
fez os conjuntos. Ele roda em placas NVIDIA RTX das séries 30, 40 e 50 e na Radeon AI PRO R9700. Escolha no `strata launch`. O
|
||||
app de desktop ainda não o oferece.
|
||||
|
||||
| Conjunto | Download | Qualidade | Escreve código a |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M** (1,75 bits) | 239 GB | O menor, mais fraco em prosa simples | ainda não medido num PC |
|
||||
| **IQ2_XXS** (2.00 bits) | 257 GB | Bom equilíbrio | 10 tokens/s (RTX 3060), 5 (RTX 5070), 48 (4x R9700) |
|
||||
| **IQ2_XS** (2.25 bits) | 274 GB | O mais perto do original | 15 tokens/s (uma R9700), 80 (4x R9700) |
|
||||
| **IQ4_NL** (tabela Engram) | incluída em cada download acima | A tabela de consulta que os três conjuntos compartilham | |
|
||||
|
||||
Inicie com `strata launch --model ist-das-2.00bpw` (ou `ist-das-1.75bpw`, `ist-das-2.25bpw`). Números de qualidade,
|
||||
velocidades por PC e a instalação manual: [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
|
||||
|
||||
## Qual é a velocidade?
|
||||
|
||||
Com base numa placa de vídeo de 12 GB (RTX 5070, 64 GB de RAM). Veja resultados de outras placas:
|
||||
[velocidade de cada tamanho](docs/MODELS.md#how-fast-is-each-size) e [resultados da comunidade](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
| Modelo | As respostas aparecem a | Lê o seu prompt a |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████████` 94 tokens/s | `████████████` 2.650 tokens/s |
|
||||
| **IQ2_XS** | `██████████░░` 79 tokens/s | `█████████░░░` 2.090 tokens/s |
|
||||
| **IQ3_XXS** | `████████░░░░` 62 tokens/s | `████████░░░░` 1.750 tokens/s |
|
||||
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1.620 tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2.180 tokens/s |
|
||||
|
||||
Numa placa AMD de 16 GB (RX 9070 XT, 47 GB de RAM):
|
||||
|
||||
| Modelo | As respostas aparecem a | Lê o seu prompt a |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████░░░░` 60 tokens/s | `█████░░░░░░░` 1.160 tokens/s |
|
||||
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1.110 tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1.420 tokens/s |
|
||||
|
||||
Um token equivale a cerca de ¾ de palavra, então 60 tokens por segundo é mais rápido do que você lê. "Lê o seu prompt" é a velocidade com que o Strata
|
||||
absorve um documento longo, um código ou um histórico de conversa.
|
||||
|
||||
## Como usar
|
||||
|
||||
### No app
|
||||
|
||||
Um chat, um **Monitor** do modelo e da sua GPU, CPU e RAM, uma página de **Modelos** e as **Configurações**. O chat pode
|
||||
trabalhar numa pasta do seu PC. No modo **Manual**, ele pergunta antes de alterar um arquivo ou executar um comando. Ele pode
|
||||
pesquisar na web (DuckDuckGo ou Bing, sem conta). Os termos da pesquisa são a única coisa que sai do seu PC.
|
||||
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="O app Strata: um chat em que o agente montou um pequeno jardim de pagodes em voxels, com a página aberta na pré-visualização à direita"><br>
|
||||
<sub>O agente escreveu um jardim de pagodes em voxels em um único arquivo HTML, e a página abre na pré-visualização ao lado do chat</sub></p>
|
||||
|
||||
### No Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="A visão geral do monitor do Terminal do Strata enquanto o modelo escreve uma resposta"><br>
|
||||
<sub><code>strata monitor</code> enquanto o modelo responde: velocidade, GPU, memória e as solicitações recentes</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve and print the address
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor # watch speed, GPU, memory and requests
|
||||
strata update # get the newest engine
|
||||
strata doctor # check the installed files
|
||||
```
|
||||
|
||||
Para acessar o Strata do celular ou de outro PC, use `strata launch --access lan`. Ele cria uma chave de API para você.
|
||||
`strata --help` lista tudo, e o [docs/TERMINAL.md](docs/TERMINAL.md) explica.
|
||||
|
||||
### Com agentes de IA e outros aplicativos
|
||||
|
||||
Um agente instala o Strata com o comando de uma linha acima e executa `strata launch --model NAME --yes --no-tui`. Os passos,
|
||||
as flags e os códigos de saída estão em [docs/AI_SETUP.md](docs/AI_SETUP.md). As regras do repositório para agentes: [AGENTS.md](AGENTS.md).
|
||||
|
||||
| Seu aplicativo fala | URL base |
|
||||
| --- | --- |
|
||||
| API da OpenAI (a maioria dos apps e agentes) | `http://127.0.0.1:8080/v1` (qualquer chave de API, qualquer nome de modelo) |
|
||||
| API da Anthropic (Claude Code) | `http://127.0.0.1:8080`, sem `/v1`. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([configuração](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` mostra as configurações exatas para OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent e OpenClaw,
|
||||
e `strata connect opencode --write` grava para você (com um backup).
|
||||
|
||||
### Bom saber
|
||||
|
||||
- **As imagens** vêm ativadas em todo modelo que sabe lê-las. Anexe ou cole uma imagem no chat, ou envie pelo seu aplicativo. Detalhes: [docs/IMAGES.md](docs/IMAGES.md).
|
||||
- **O raciocínio** pode ser off, low, medium ou high, no menu do chat ou no "reasoning effort" do seu aplicativo. Off é o mais rápido.
|
||||
- **A primeira mensagem** de uma conversa é lida por inteiro, cerca de 1 minuto a cada 30.000 tokens. As mensagens seguintes começam em segundos.
|
||||
- **Um pedido por vez** é o padrão. Para mais de um, use `--parallel N` ([BATCHING.md](docs/BATCHING.md)). Numa placa de 12 GB, isso deixa cada resposta mais lenta.
|
||||
|
||||
## Compilar a partir do código-fonte
|
||||
|
||||
<details>
|
||||
<summary>Windows, Linux e Mac a partir do repositório</summary>
|
||||
|
||||
[Baixe o Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) e descompacte, ou use `git clone`.
|
||||
**Windows:** dê dois cliques em `START-HERE.bat`. **Linux:** execute `./setup.sh` na pasta do Strata.
|
||||
|
||||
**Mac (Apple Silicon, macOS 15 ou mais novo):** instale uma vez as Command Line Tools da Apple com `xcode-select --install`
|
||||
(o Xcode em si não é necessário). Abra o Terminal na pasta do Strata e execute `./setup.sh`. Ele compila o mecanismo do Mac no seu
|
||||
Mac (cerca de 7 minutos), faz as mesmas perguntas, baixa o modelo e inicia. Da próxima vez, `./setup.sh` inicia
|
||||
na hora. Qual tamanho cabe no seu Mac: [docs/MAC.md](docs/MAC.md).
|
||||
|
||||
O instalador encontra a sua placa e configura o mecanismo certo. Ele pergunta qual modelo e tamanho, quanto contexto (quanto texto
|
||||
o modelo mantém em mente) e se deve ler imagens. Pressione Enter para aceitar a resposta recomendada. Depois ele baixa o
|
||||
modelo (cerca de 70 GB) e inicia. Se o download parar, execute de novo e ele continua. O navegador abre a página do Strata em
|
||||
`http://127.0.0.1:8080`.
|
||||
|
||||
> Enquanto o modelo inicia, o seu PC pode ficar lento ou parar de responder por 1 a 3 minutos (mais tempo na primeira vez). O Strata carrega
|
||||
> de 35 a 55 GB na sua RAM e trava parte dela para a placa de vídeo. Espere e não feche a janela.
|
||||
|
||||
Da próxima vez, execute `START-HERE.bat` (ou `./setup.sh`) de novo. Ele inicia na hora e não baixa nada duas vezes. Feche a janela
|
||||
dele para parar o modelo. `UPDATE.bat` (`./update.sh`) atualiza o Strata sem iniciá-lo. Atualização, Docker, várias placas,
|
||||
onde ficam os arquivos e todas as opções: [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
Para medir o seu PC, execute `BENCH.bat` (`./bench.sh`). Leva de 5 a 10 minutos e gera um relatório que você pode compartilhar
|
||||
([como](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
|
||||
|
||||
**Opcional: mais velocidade numa placa quando o modelo inteiro cabe na RAM.** Duas configurações opcionais do RubenTorney (#1904, #1905).
|
||||
Elas ajudam quando você usa uma placa de vídeo e a sua RAM comporta todos os experts, por exemplo Q2_0 ou IQ3_XXS num PC de 64 GB. Abra
|
||||
`strata-<model>.json` na pasta de dados do Strata ([onde fica](docs/INSTALL.md#where-things-are-stored)) e adicione:
|
||||
|
||||
- `"--adapt-async", "1"` à lista `"args"`. O cache de experts passa a ser atualizado numa thread auxiliar, em vez de pausar a
|
||||
resposta. Medido numa RTX 5070 (Windows, Q2_0): +4 a 5% na geração, +7 a 10% em código e documentos longos. As respostas podem diferir
|
||||
um pouco do padrão e continuam iguais de uma execução para outra.
|
||||
- `"STRATA_PIN_BY_NEED": "1"` no bloco `"env"` (Windows). O Windows só consegue travar cerca de metade da RAM para a placa
|
||||
de vídeo, e isso trava primeiro os experts de que a placa precisa. Medido na RTX 5070: +4,4% na leitura do prompt.
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
Reinicie o Strata depois da alteração. Para desfazer, remova as duas entradas.
|
||||
Siga as instruções para baixar e iniciar um modelo. [Guia do Terminal](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
</details>
|
||||
|
||||
## Como funciona?
|
||||
<p align="center">
|
||||
<img src="docs/media/terminal-monitor.png" width="900" alt="Aplicativo de Terminal do Strata mostrando a atividade do modelo, a velocidade, a memória e as solicitações recentes">
|
||||
<br><sub>Acompanhe seu modelo e seu hardware enquanto ele roda.</sub>
|
||||
</p>
|
||||
|
||||
Modelos como este costumam rodar em servidores com centenas de gigabytes de memória de vídeo. A sua placa de vídeo tem
|
||||
12 a 24 GB. O Strata faz o modelo caber dividindo o trabalho entre o PC inteiro. Imagine uma biblioteca.
|
||||
<a id="build-from-source"></a>
|
||||
|
||||
- O modelo é uma equipe de 24.576 pequenos especialistas ("experts"), como 24.576 livros de consulta. Cada palavra precisa de apenas 10 deles.
|
||||
- **A sua placa de vídeo é a mesa.** Ela é pequena e rápida. Os cerca de 4.000 livros mais usados ficam abertos nela.
|
||||
- **A sua RAM são as estantes da sala de leitura.** Os 24.576 livros estão lá. Quando uma palavra precisa de um livro que não está na
|
||||
mesa, o seu processador lê da estante na hora, enquanto a placa continua trabalhando.
|
||||
- **O seu SSD é o arquivo.** Ele guarda uma tabela de consulta de 29 GB. O modelo lê algumas linhas dela por palavra, então ela não
|
||||
precisa ficar na RAM.
|
||||
[Compilar a partir do código-fonte](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [Guia de instalação completo](docs/INSTALL.md)
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Os 24.576 experts do modelo: os mais usados na placa de vídeo, todos na RAM, uma tabela de consulta no SSD"></p>
|
||||
<a id="what-you-need"></a>
|
||||
|
||||
Um pequeno ajudante também adivinha as próximas palavras. O modelo grande confere todas de uma vez e fica com as certas. Você recebe
|
||||
a mesma resposta, de 1,6 a 1,8 vez mais rápido. Textos longos são lidos em blocos grandes, de até 8.192 tokens por vez, a mais de 1.000 tokens
|
||||
por segundo.
|
||||
## Hardware
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Um pequeno ajudante adivinha as próximas palavras; o modelo grande confere todas de uma vez e fica com as certas"></p>
|
||||
| Hardware compatível | Guia |
|
||||
| --- | --- |
|
||||
| **NVIDIA GeForce RTX 20 / 30 / 40 / 50** | [Instalação para NVIDIA](docs/INSTALL.md#what-you-need) |
|
||||
| **AMD Radeon RX 7700 / 7800 / 7900, RX 9060 / 9070, AI PRO R9700** | [Instalação para AMD](docs/AMD_HIP.md) |
|
||||
| **AMD Radeon RX 6800 / 6900 · experimental** | [Instalação para RX 6000](docs/AMD_HIP.md#rdna2-gfx1030) |
|
||||
| **Intel Arc · Linux** | [Instalação para Intel](docs/INTEL_ARC.md) |
|
||||
| **Apple Silicon · M1 ou mais novo** | [Instalação para Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max · Strix Halo** | [Instalação para Strix Halo](docs/STRIX_HALO.md) |
|
||||
|
||||
A explicação mais longa: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte e seus números:
|
||||
[os detalhes](docs/DETAILS.md#how-it-works) e o [artigo](docs/paper/Strata-Paper.pdf).
|
||||
<a id="using-it"></a>
|
||||
|
||||
## Algo deu errado?
|
||||
## Ferramentas
|
||||
|
||||
- **Meu PC travou na primeira vez que o Strata iniciou.** Ele carrega o modelo na RAM. Espere e não feche a janela. Ainda
|
||||
travado depois de 10 minutos? Reinicie o PC, feche outros programas e tente de novo, ou escolha um tamanho menor.
|
||||
- **Parou durante o download ou a instalação.** Abra o app ou o `strata launch` de novo. Ele continua de onde parou.
|
||||
- **Está muito lento, ou aparece "the engine stopped unexpectedly".** O seu PC não tem RAM livre suficiente. Feche outros
|
||||
programas (os navegadores usam muita), ou escolha um tamanho menor, como Q2_0 ou IQ2_XS.
|
||||
- **Diz que não há mecanismo para este PC.** No app, abra **Configurações, Mecanismo** e clique em **Atualizar mecanismo**. No
|
||||
Terminal, execute `strata update`.
|
||||
- **Diz que a porta 8080 já está em uso.** O Strata já está rodando. Procure a janela dele.
|
||||
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw**, e outras, pelas APIs compatíveis com OpenAI e Anthropic.
|
||||
|
||||
Mais soluções: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Ainda sem resolver? Abra uma
|
||||
[issue](https://github.com/Niko1221/Strata/issues) e anexe os logs: no app, em **Configurações, Mecanismo, Diagnóstico,
|
||||
Salvar logs**; no Terminal, a saída de `strata doctor`. Para um problema de segurança, informe em particular: [SECURITY.md](SECURITY.md).
|
||||
Execute **`strata connect`** para ver as instruções da sua ferramenta. [Guia de conexão](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect).
|
||||
|
||||
## Créditos e licença
|
||||
<a id="guides"></a>
|
||||
<a id="how-fast-is-it"></a>
|
||||
<a id="how-does-it-work"></a>
|
||||
<a id="something-went-wrong"></a>
|
||||
|
||||
O modelo é o [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), da equipe Qwen. Ele foi comprimido por
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5), agentionai (Gyro)
|
||||
e Unsloth. A ISTA-DASLab também fez os conjuntos do DeepSeek V4.1-Flash. O Strata usa partes do
|
||||
[llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Todos os créditos: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits).
|
||||
O Strata é de código aberto sob a [Licença MIT](LICENSE). Algumas partes e todos os modelos têm licenças próprias
|
||||
([quais](docs/HOW_IT_WORKS.md#license)).
|
||||
## Ajuda
|
||||
|
||||
## Contribuindo
|
||||
|
||||
Encontrou um bug, ou quer enviar uma alteração ou um benchmark? Leia primeiro o [CONTRIBUTING.md](CONTRIBUTING.md). Ele diz o que
|
||||
incluir para que outras pessoas possam conferir o seu trabalho.
|
||||
[Todos os guias](docs) · [Solução de problemas](docs/TROUBLESHOOTING.md) · [Relatar um bug](https://github.com/Niko1221/Strata/issues/new/choose) ·
|
||||
[Benchmarks](docs/COMMUNITY_BENCHMARKS.md) · [Como funciona](docs/HOW_IT_WORKS.md) · [Contribuir](CONTRIBUTING.md)
|
||||
|
||||
## Apoie o Strata
|
||||
|
||||
O Strata é gratuito e tem licença MIT. Eu o desenvolvo e testo em todo hardware que consigo arranjar. Se ele roda bem no seu PC, um café me ajuda
|
||||
a continuar testando placas novas e corrigindo o que você relatar. Sem pressão. Usar e relatar o que encontrar ajuda tanto quanto.
|
||||
**Ajude a manter a IA local aberta para todos.** Um café ajuda a financiar o desenvolvimento e os testes em mais hardware.
|
||||
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: apoie o Strata" height="48"></a>
|
||||
|
||||
Quer patrocinar o Strata? Fale comigo pelo [GitHub](https://github.com/Niko1221).
|
||||
[Licença MIT](LICENSE) · [Créditos e licenças dos modelos](docs/HOW_IT_WORKS.md#credits) · [Privacidade](docs/SAFETY.md)
|
||||
|
||||
<a id="option-c-let-your-ai-set-it-up"></a>
|
||||
|
||||
<sub>Para ferramentas de IA e leitores automáticos: <a href="AGENTS.md">referência técnica</a> · <a href="docs/AI_SETUP.md">instruções de instalação</a>.</sub>
|
||||
|
||||
**Patrocínios, parcerias de hardware ou colaborações:** [[email protected]](mailto:[email protected]).
|
||||
+122
-278
@@ -1,344 +1,188 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="340">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="README.md">English</a> · <b>简体中文</b> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<h3 align="center">在你自己的游戏电脑上运行最先进的模型</h3>
|
||||
|
||||
<p align="center">支持 NVIDIA、AMD、Intel · Windows、Linux、Mac · 免费开源</p>
|
||||
<h3 align="center">在你自己的电脑上运行强大的 AI。</h3>
|
||||
<p align="center">本地聊天、写代码、处理图片。</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="最新版本"></a>
|
||||
<a href="#模型">模型</a> · <a href="#安装"><b>安装</b></a> ·
|
||||
<a href="#硬件">硬件</a> · <a href="#工具">工具</a> ·
|
||||
<a href="#帮助">帮助</a> · <a href="#支持-strata">支持</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="下载 Strata"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT 许可证"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub 星标"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=black" alt="请我喝杯咖啡"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="在 Buy Me a Coffee 上支持 Strata"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Strata 的模型写出的体素宝塔花园,在浏览器里运行"></a><br>
|
||||
<sub>一座体素宝塔花园,由一条提示词一次生成,在装有 RTX 5070 的电脑上用 Strata 运行(IQ3_S,128K 上下文) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">完整视频(49 秒)</a></sub></p>
|
||||
<p align="center"><sub><a href="README.md">English</a> · <b>简体中文</b> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a> · <a href="README.pl.md">Polski</a></sub></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#安装"><b>安装</b></a> ·
|
||||
<a href="#模型">模型</a> ·
|
||||
<a href="#速度如何">速度</a> ·
|
||||
<a href="#使用方法">使用方法</a> ·
|
||||
<a href="#工作原理">工作原理</a> ·
|
||||
<a href="#出问题了">帮助</a> ·
|
||||
<a href="#支持-strata">支持</a>
|
||||
</p>
|
||||
**免费、开源,由你自己运行。** 在桌面上使用 Strata,或搭配你喜欢的编程工具。无需账号。
|
||||
|
||||
Strata 运行 **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**。这是一个 1250 亿参数的模型,
|
||||
通常需要服务器才能跑,现在用普通电脑就行。从 0.2.4 起,它在终端版里也能运行 **DeepSeek V4.1-Flash**。它能聊天、写代码、
|
||||
看图片,也能配合你的应用和编程智能体使用。你的聊天记录和文件都留在你自己的电脑上。
|
||||
## 模型
|
||||
|
||||
- **游戏电脑就能跑。** 内存和显存加起来有 32 GB 就可以开始。
|
||||
- **能接入你的工具。** Claude Code、Codex CLI、OpenCode 等智能体通过 OpenAI 和 Anthropic 的 API 连接。
|
||||
- **能看图片**,默认开启。
|
||||
- **免费且隐私。** MIT 许可证,不用注册账号,不走云端,没有遥测([它下载什么、什么时候下载](docs/SAFETY.md))。
|
||||
我们**首先推荐 ISTA-DASLab**,其次是 **Unsloth** 和其他提供方,前提是有内存放得下的兼容模型。
|
||||
|
||||
## 硬件要求
|
||||
<details>
|
||||
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> <b>Qwen3.8-Flash-Next</b></summary>
|
||||
|
||||
**内存和显存(VRAM)加起来至少要有 32 GB。** Mac 和 Ryzen AI Max 上指的是统一内存。32 GB 内存加一张 12-16 GB 显存的显卡,
|
||||
可以跑较小的尺寸;64 GB 可以跑所有尺寸。
|
||||
|
||||
| 适用于 | 指南 |
|
||||
| 提供方 | 可选项 |
|
||||
| --- | --- |
|
||||
| **NVIDIA** GeForce RTX 20、30、40 和 50 系列,12 GB 及以上 | [安装](#安装) |
|
||||
| **AMD** Radeon RX 6800 及更新型号、Radeon AI PRO R9700 | [AMD 显卡](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md) |
|
||||
| **Apple 芯片 Mac**,M1 及更新,macOS 15 及以上 | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max(Strix Halo)** 及其他统一内存的电脑 | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| 较旧的显卡和处理器 | [旧显卡](docs/OLDER_GPUS.md) |
|
||||
| **ISTA-DASLab · 推荐** | Q2\_0, IQ2\_XS, IQ3\_XXS, IQ3\_S。[Coder:IQ1\_M](docs/MODELS.md#coder),适合写代码。 |
|
||||
| **Unsloth** | UD-IQ4\_XS, UD-Q4\_K\_XL, UD-Q5\_K\_XL, UD-Q6\_K\_XL, Q8\_0 |
|
||||
| **Gyro · agentionai** | [Gyro-S, Gyro-M](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)。占用内存更少,但速度更慢。 |
|
||||
| **其他选项** | [Swift 1.5](docs/MODELS.md#swift-15), [NVFP4](docs/NVFP4.md), [OrcaRouter](docs/ORCA.md) |
|
||||
|
||||
你还需要约 80 GB 的可用磁盘空间(固态硬盘)、Windows 10 / 11、Linux 或 macOS,以及较新的显卡驱动。同一品牌的两三张显卡可以共同运行一个模型([多显卡](docs/MULTI_GPU.md))。
|
||||
[比较大小、内存需求和兼容性](docs/MODELS.md)。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> <b>Qwen3.6-35B-A3B</b></summary>
|
||||
|
||||
**Unsloth:** UD-Q4_K_XL, UD-Q6_K_XL, Q8_0。这些是目前文档中收录的该模型的版本。
|
||||
|
||||
[模型指南和兼容性](docs/QWEN36.md)。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><img src="docs/media/models/deepseek.svg" width="22" alt="" align="middle"> <b>DeepSeekV4.1-Flash-1017</b></summary>
|
||||
|
||||
可在**桌面应用**和**终端**中使用。内存放不下的部分会从 SSD 读取,所以需要大量空闲磁盘空间。
|
||||
|
||||
| 版本(IST-DAS) | 下载大小 | 所需空闲磁盘 |
|
||||
| --- | ---: | ---: |
|
||||
| 1.75 bpw | 239 GB | 约245 GB |
|
||||
| 2.00 bpw | 257 GB | 约265 GB |
|
||||
| 2.25 bpw | 274 GB | 约280 GB |
|
||||
|
||||
支持 NVIDIA RTX 30、40 和 50 系列(Windows 和 Linux),以及 AMD Radeon AI PRO R9700(Linux)。安装程序会下载模型文件和 DeepSeek 引擎,然后启动模型。
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
```
|
||||
|
||||
以下为 Strata 0.2.5 上 2.00 bpw 版本的实测结果。“输出”是回答的速度,“提示词”是 Strata 读取你输入内容的速度。
|
||||
|
||||
| 硬件 | 输出 | 提示词 |
|
||||
| --- | ---: | ---: |
|
||||
| RTX 5070 12 GB, 64 GB 内存 | 4 到 6 tokens/s | 约 110 tokens/s(4K) |
|
||||
| RTX 3060 12 GB, 46 GB 内存 | 9 到 11 tokens/s | 约 155 tokens/s(4K) |
|
||||
| 1x Radeon AI PRO R9700, 251 GB 内存 | 21 到 25 tokens/s | 320 到 360 tokens/s (8K) |
|
||||
| 4x Radeon AI PRO R9700 | 74 到 77 tokens/s | 400 到 450 tokens/s (8K) |
|
||||
|
||||
[内存、磁盘和详细信息](docs/DEEPSEEK41.md) · [发行说明](https://github.com/Niko1221/Strata/releases)
|
||||
|
||||
</details>
|
||||
|
||||
<a id="the-other-sizes"></a>
|
||||
|
||||
不确定哪个放得下?[为你的电脑选择模型](docs/MODELS.md#pick-by-memory)。
|
||||
|
||||
## 安装
|
||||
|
||||
### 1. Strata 应用(最简单)
|
||||
<a id="option-a-the-strata-app"></a>
|
||||
|
||||
下载适合你电脑的文件,打开它,然后对推荐的模型点击安装。
|
||||
### 桌面应用
|
||||
|
||||
**Windows 10 / 11**
|
||||
**[下载适用于 Windows、Linux 或 Mac 的 Strata →](https://github.com/Niko1221/Strata/releases/latest)**
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-windows-x64-setup.exe)
|
||||
安装应用,选择模型,就可以开始聊天。每个系统只有一个安装程序。首次启动时,应用会下载适合你显卡的引擎。[安装程序帮助](docs/INSTALL.md#the-app)。
|
||||
|
||||
**Linux**
|
||||
<sub>文件: `Strata-0.2.7-windows-x64-setup.exe` · `Strata-0.2.7-linux-x86_64.AppImage` · `Strata-0.2.7-macos-arm64.dmg`</sub>
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-linux-x86_64.AppImage)
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Strata 桌面应用,聊天和实时代码预览并排显示">
|
||||
<br><sub>聊天、文件和实时预览都在一个窗口里。</sub>
|
||||
</p>
|
||||
|
||||
**Mac**(Apple 芯片,macOS 15 及以上)
|
||||
<a id="option-b-the-terminal-one-line"></a>
|
||||
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-macos-arm64.dmg)
|
||||
### 终端应用
|
||||
|
||||
应用会自己为你的显卡选择合适的引擎。所有文件都在[发布页](https://github.com/Niko1221/Strata/releases/latest)。
|
||||
更喜欢终端?安装 Strata,然后运行 **`strata launch`**。
|
||||
|
||||
| 首次启动 | 怎么做 |
|
||||
| --- | --- |
|
||||
| **Windows** | 运行 `.exe`。不需要管理员权限。如果 Windows 提示“Windows 已保护你的电脑”,点击**更多信息**,再点**仍要运行**。 |
|
||||
| **Linux** | 给文件加上可执行权限(`chmod +x`),然后启动。需要 FUSE 2:`sudo apt install libfuse2t64`(Ubuntu 24.04 及更新)或 `libfuse2`(22.04)。 |
|
||||
| **Mac** | 打开 `.dmg`,把 Strata 拖到“应用程序”。 |
|
||||
<details open>
|
||||
<summary><b>安装命令</b></summary>
|
||||
|
||||
模型下载很大(40-70 GB)。如果中断,会从断点继续。
|
||||
|
||||
### 2. 终端版(一行命令)
|
||||
|
||||
`strata` 命令,适合脚本、服务器和 AI 智能体。运行 DeepSeek 也要用它。
|
||||
|
||||
Linux 和 Mac:
|
||||
**Linux / Mac**
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows(PowerShell):
|
||||
**Windows PowerShell**
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
然后启动它。它会一次问一个问题(模型、上下文、图片、允许谁连接),下载需要的东西,并开始提供服务:
|
||||
在 Windows 上,安装后请打开一个新的终端。然后运行:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
不需要管理员权限。安装程序会校验它下载的所有内容的 SHA-256。在 Windows 上,装完后请打开一个新的终端窗口。
|
||||
所有界面和参数见 [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md)。
|
||||
|
||||
### 3. 让你的 AI 来安装
|
||||
|
||||
把下面这段话粘贴到 Claude Code、Cursor、Codex、GitHub Copilot 或其他编程助手里:
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
它会检查你的显卡、内存和磁盘,选出合适的模型,完成安装和启动,并告诉你怎么连接你的应用。AI 工具也可以通过 Strata 的
|
||||
[MCP 服务器](docs/MCP_SERVER.md)安装、启动和停止 Strata。
|
||||
|
||||
想自己动手编译?请看[从源码构建](#从源码构建)。
|
||||
|
||||
## 模型
|
||||
|
||||
应用和 `strata launch` 会按你的内存推荐尺寸。尺寸小的更快,尺寸大的稍微更聪明一点。
|
||||
|
||||
### 内存不够?试试 Gyro
|
||||
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)(作者 agentionai)是运行 Qwen3.8-Flash-Next 占用最小的方式。
|
||||
它可在 NVIDIA、AMD、Intel Arc 和 Apple 芯片上运行。
|
||||
|
||||
| 尺寸 | 所需内存(RAM + VRAM) | 下载大小 | 适合 |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27.6 GB | 58.5 GB | 其他尺寸都放不下的电脑。32 GB 的 Mac mini 上每秒 9 个 token。 |
|
||||
| **Gyro-M** | 35 GB | 92.0 GB | 质量约相当于 IQ3_XXS,显存占用少 20%。 |
|
||||
|
||||
用 `strata launch --model gyro-s` 启动。详情:[docs/GYRO.md](docs/GYRO.md)。
|
||||
|
||||
### Qwen3.8-Flash-Next 的尺寸
|
||||
|
||||
| 你的内存(RAM + VRAM) | 选择 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Gyro-S**,写代码可选 **[Coder](docs/MODELS.md#coder)** | Coder 保留代码用到的专家 |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS(39.2 GB)或 **Gyro-M** | Swift 回答前思考的时间更短 |
|
||||
| **48 GB** | **IQ3_XXS**(47.0 GB) | 更慢,更好 |
|
||||
| **64 GB** | **IQ3_S**(54.8 GB) | 在公开测试中与完整模型持平 |
|
||||
| **96 GB 及以上** | Unsloth 的 **UD-IQ4_XS** 到 **Q8_0** | 最接近完整模型 |
|
||||
|
||||
内存比这一行写的少,也可能能用,只是更慢:Strata 会自动切换到[低内存模式](docs/MODELS.md#a-big-graphics-card-and-little-ram)。
|
||||
另外还有:给 NVIDIA 显卡用的 [NVFP4](docs/NVFP4.md)、[Qwen3.6-35B-A3B](docs/QWEN36.md)(更小,能同时处理多个请求),
|
||||
以及 [OrcaRouter 的 Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)。所有尺寸,以及各种配置能跑什么:
|
||||
[docs/MODELS.md](docs/MODELS.md)。
|
||||
|
||||
### DeepSeek V4.1-Flash(终端版,0.2.4 起)
|
||||
|
||||
一个大得多的模型,用一块 12 GB 显卡加 46 到 64 GB 内存就能运行。Strata 把最常用的部分留在内存里,其余的从你的固态硬盘读取。
|
||||
这些版本由 [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF) 制作。它可在
|
||||
NVIDIA RTX 30、40、50 系列显卡和 Radeon AI PRO R9700 上运行。请在 `strata launch` 里选择它。桌面应用暂时不提供。
|
||||
|
||||
| 版本 | 下载大小 | 质量 | 写代码速度 |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M**(1.75 位) | 239 GB | 最小,普通文字表现较弱 | 还没在电脑上测量 |
|
||||
| **IQ2_XXS**(2.00 位) | 257 GB | 平衡得不错 | 每秒 10 个 token(RTX 3060),5(RTX 5070),48(4 块 R9700) |
|
||||
| **IQ2_XS**(2.25 位) | 274 GB | 最接近原模型 | 每秒 15 个 token(一块 R9700),80(4 块 R9700) |
|
||||
| **IQ4_NL**(Engram 表) | 已包含在上面每个下载中 | 三个版本共用的查找表 | |
|
||||
|
||||
用 `strata launch --model ist-das-2.00bpw`(或 `ist-das-1.75bpw`、`ist-das-2.25bpw`)启动。质量数据、各台电脑的速度和手动安装:
|
||||
[docs/DEEPSEEK41.md](docs/DEEPSEEK41.md)。
|
||||
|
||||
## 速度如何?
|
||||
|
||||
以下数据基于 12 GB 显卡(RTX 5070、64 GB 内存)。其他显卡的结果见:
|
||||
[各尺寸的速度](docs/MODELS.md#how-fast-is-each-size)和[社区测试结果](docs/COMMUNITY_BENCHMARKS.md)。
|
||||
|
||||
| 模型 | 回答输出速度 | 读取提示词速度 |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████████` 94 tokens/s | `████████████` 2,650 tokens/s |
|
||||
| **IQ2_XS** | `██████████░░` 79 tokens/s | `█████████░░░` 2,090 tokens/s |
|
||||
| **IQ3_XXS** | `████████░░░░` 62 tokens/s | `████████░░░░` 1,750 tokens/s |
|
||||
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1,620 tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2,180 tokens/s |
|
||||
|
||||
在 16 GB 的 AMD 显卡上(RX 9070 XT、47 GB 内存):
|
||||
|
||||
| 模型 | 回答输出速度 | 读取提示词速度 |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████░░░░` 60 tokens/s | `█████░░░░░░░` 1,160 tokens/s |
|
||||
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1,110 tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1,420 tokens/s |
|
||||
|
||||
一个 token 约等于 ¾ 个英文单词,所以每秒 60 个 token 比你阅读的速度还快。“读取提示词”指 Strata 吸收长文档、代码或聊天历史的速度。
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 在应用里
|
||||
|
||||
聊天、显示模型以及 GPU、CPU 和内存状态的**监控**、**模型**页面和**设置**。聊天可以在你电脑上的某个文件夹里工作。
|
||||
在**手动**模式下,修改文件或运行命令之前它会先询问你。它可以搜索网页(DuckDuckGo 或 Bing,不需要账号)。
|
||||
发出电脑的只有搜索词。
|
||||
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="Strata 应用:一次对话,智能体做出了一个小型体素宝塔花园,页面在右侧的预览中打开"><br>
|
||||
<sub>智能体用一个 HTML 文件写出了体素宝塔花园,页面在聊天旁的预览中打开</sub></p>
|
||||
|
||||
### 在终端里
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="模型写回复时,Strata 终端监控的总览界面"><br>
|
||||
<sub>模型回复时的 <code>strata monitor</code>:速度、GPU、内存和最近的请求</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve and print the address
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor # watch speed, GPU, memory and requests
|
||||
strata update # get the newest engine
|
||||
strata doctor # check the installed files
|
||||
```
|
||||
|
||||
要从手机或另一台电脑访问 Strata,请使用 `strata launch --access lan`。它会为你生成一个 API 密钥。
|
||||
`strata --help` 会列出所有命令,[docs/TERMINAL.md](docs/TERMINAL.md) 有详细说明。
|
||||
|
||||
### 配合 AI 智能体和其他应用
|
||||
|
||||
智能体用上面的一行命令安装 Strata,然后运行 `strata launch --model NAME --yes --no-tui`。步骤、参数和退出码见
|
||||
[docs/AI_SETUP.md](docs/AI_SETUP.md)。给智能体看的仓库规则:[AGENTS.md](AGENTS.md)。
|
||||
|
||||
| 你的应用使用 | 基础 URL |
|
||||
| --- | --- |
|
||||
| OpenAI API(大多数应用和智能体) | `http://127.0.0.1:8080/v1`(API 密钥和模型名称随便填) |
|
||||
| Anthropic API(Claude Code) | `http://127.0.0.1:8080`,不带 `/v1`。Claude Code:`ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API(Codex CLI) | `http://127.0.0.1:8080/v1/responses`([设置方法](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` 会打印 OpenCode、Codex CLI、Claude Code、oh-my-pi、pi、Hermes Agent 和 OpenClaw 的具体设置,
|
||||
`strata connect opencode --write` 会帮你写入(并保留备份)。
|
||||
|
||||
### 使用须知
|
||||
|
||||
- **图片**对每个能看图片的模型都是默认开启的。在聊天里附加或粘贴图片,或者从你的应用发送图片。详情:[docs/IMAGES.md](docs/IMAGES.md)。
|
||||
- **思考级别**可以是 off、low、medium 或 high,在聊天菜单或你的应用的“reasoning effort”里设置。off 最快。
|
||||
- **聊天的第一条消息**会被完整读取,每 30,000 个 token 约需 1 分钟。之后的消息几秒钟就开始。
|
||||
- **默认一次处理一个请求。** 要处理更多,请使用 `--parallel N`([BATCHING.md](docs/BATCHING.md))。在 12 GB 显卡上,这会让每个回答变慢。
|
||||
|
||||
## 从源码构建
|
||||
|
||||
<details>
|
||||
<summary>从仓库构建 Windows、Linux 和 Mac 版本</summary>
|
||||
|
||||
[下载 Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip)并解压,或者用 `git clone` 获取。
|
||||
**Windows:** 双击 `START-HERE.bat`。**Linux:** 在 Strata 文件夹里运行 `./setup.sh`。
|
||||
|
||||
**Mac(Apple 芯片,macOS 15 及以上):** 先用 `xcode-select --install` 安装一次 Apple 的命令行工具(不需要完整的 Xcode)。
|
||||
在 Strata 文件夹里打开终端,运行 `./setup.sh`。它会在你的 Mac 上构建 Mac 引擎(约 7 分钟),问同样的问题,下载模型并启动。
|
||||
下次运行 `./setup.sh` 会直接启动。你的 Mac 适合哪个尺寸:[docs/MAC.md](docs/MAC.md)。
|
||||
|
||||
安装程序会找到你的显卡,并设置合适的引擎。它会问你用哪个模型和尺寸、上下文多长(模型能记住多少文字),以及是否要读取图片。
|
||||
直接按回车就是推荐的选择。然后它会下载模型(约 70 GB)并启动。如果下载中断,再运行一次就会继续。浏览器会打开
|
||||
`http://127.0.0.1:8080` 上的 Strata 页面。
|
||||
|
||||
> 模型启动期间,你的电脑可能会变慢,或者在 1-3 分钟内没有响应(第一次最久)。Strata 会把 35-55 GB 加载到内存里,并锁定其中一部分
|
||||
> 给显卡使用。请等待,不要关闭窗口。
|
||||
|
||||
下次再运行 `START-HERE.bat`(或 `./setup.sh`)会立即启动,不会重复下载。关闭它的窗口就能停止模型。`UPDATE.bat`
|
||||
(`./update.sh`)只更新 Strata,不启动。更新、Docker、多显卡、文件存放位置和所有选项:[docs/INSTALL.md](docs/INSTALL.md)。
|
||||
|
||||
要测试你的电脑,运行 `BENCH.bat`(`./bench.sh`)。需要 5-10 分钟,并生成一份可以分享的报告
|
||||
([方法](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark))。
|
||||
|
||||
**可选:整个模型都放得进内存时,让单卡更快。** 这是 RubenTorney 提供的两个需要手动开启的设置(#1904、#1905)。
|
||||
当你只用一张显卡,并且内存装得下所有专家时,它们会有帮助,例如在 64 GB 的电脑上跑 Q2_0 或 IQ3_XXS。打开 Strata 数据文件夹里的
|
||||
`strata-<model>.json`([位置](docs/INSTALL.md#where-things-are-stored)),然后添加:
|
||||
|
||||
- 在 `"args"` 列表中加入 `"--adapt-async", "1"`。这样专家缓存会在辅助线程上更新,不会让回答暂停。在 RTX 5070
|
||||
(Windows,Q2_0)上测得:解码快 4-5%,代码和长文档快 7-10%。回答可能与默认设置略有不同,但每次运行的结果保持一致。
|
||||
- 在 `"env"` 块中加入 `"STRATA_PIN_BY_NEED": "1"`(Windows)。Windows 只能为显卡锁定大约一半的内存,
|
||||
这个设置会优先锁定显卡最先需要的专家。在 RTX 5070 上测得:读取提示词快 4.4%。
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
修改后请重启 Strata。要撤销,删除这两项即可。
|
||||
按提示下载并启动模型。[终端指南](docs/TERMINAL_LAUNCH.md)。
|
||||
|
||||
</details>
|
||||
|
||||
## 工作原理
|
||||
<p align="center">
|
||||
<img src="docs/media/terminal-monitor.png" width="900" alt="Strata 终端应用,显示模型活动、速度、内存和最近的请求">
|
||||
<br><sub>模型运行时,随时查看模型和硬件状态。</sub>
|
||||
</p>
|
||||
|
||||
这类模型通常运行在有几百 GB 显存的服务器上。你的显卡只有 12-24 GB。Strata 把工作分给整台电脑,让模型装得下。
|
||||
可以把它想成一座图书馆。
|
||||
<a id="build-from-source"></a>
|
||||
|
||||
- 模型是一个由 24,576 位小专家(“experts”)组成的团队,好比 24,576 本参考书。每个词只需要其中 10 本。
|
||||
- **你的显卡是书桌。** 它小而快。最常用的大约 4,000 本书摊开放在上面。
|
||||
- **你的内存是阅览室的书架。** 24,576 本书都在这里。某个词需要一本不在书桌上的书时,处理器会立刻从书架上取来读,显卡则继续工作。
|
||||
- **你的固态硬盘是档案室。** 里面存着一张 29 GB 的查找表。模型每个词只读其中几行,所以不用常驻内存。
|
||||
[从源码构建](docs/SOURCE_INSTALL.md) · [Docker](docs/DOCKER.md) · [完整安装指南](docs/INSTALL.md)
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="模型的 24,576 位专家:最忙的在显卡上,全部在内存里,查找表在固态硬盘上"></p>
|
||||
<a id="what-you-need"></a>
|
||||
|
||||
另外有一个小助手会猜接下来的几个词。大模型一次检查所有猜测,保留正确的部分。得到的答案不变,但快了 1.6-1.8 倍。
|
||||
长文本按大块读取,一次最多 8,192 个 token,速度超过每秒 1,000 个 token。
|
||||
## 硬件
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="小助手猜接下来的几个词;大模型一次检查全部猜测,保留正确的部分"></p>
|
||||
| 支持的硬件 | 指南 |
|
||||
| --- | --- |
|
||||
| **NVIDIA GeForce RTX 20 / 30 / 40 / 50 系列** | [NVIDIA 安装](docs/INSTALL.md#what-you-need) |
|
||||
| **AMD Radeon RX 7700 / 7800 / 7900、RX 9060 / 9070、AI PRO R9700** | [AMD 安装](docs/AMD_HIP.md) |
|
||||
| **AMD Radeon RX 6800 / 6900 · 实验性** | [RX 6000 安装](docs/AMD_HIP.md#rdna2-gfx1030) |
|
||||
| **Intel Arc · Linux** | [Intel 安装](docs/INTEL_ARC.md) |
|
||||
| **Apple Silicon · M1 或更新** | [Mac 安装](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max · Strix Halo** | [Strix Halo 安装](docs/STRIX_HALO.md) |
|
||||
|
||||
更详细的解释:[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)。每个部分和它的数据:
|
||||
[详细说明](docs/DETAILS.md#how-it-works)和[论文](docs/paper/Strata-Paper.pdf)。
|
||||
<a id="using-it"></a>
|
||||
|
||||
## 出问题了?
|
||||
## 工具
|
||||
|
||||
- **第一次启动 Strata 时我的电脑卡住了。** 它在把模型加载到内存。请等待,不要关闭窗口。10 分钟后还是卡住?重启电脑,
|
||||
关闭其他程序后再试,或者选一个更小的尺寸。
|
||||
- **下载或安装时中断了。** 重新启动应用或 `strata launch`。它会从中断的地方继续。
|
||||
- **非常慢,或者提示“the engine stopped unexpectedly”。** 你的电脑可用内存不够。关闭其他程序(浏览器很占内存),
|
||||
或者选一个更小的尺寸,比如 Q2_0 或 IQ2_XS。
|
||||
- **提示这台电脑没有可用的引擎。** 在应用里打开**设置,引擎**,点击**更新引擎**。在终端里运行 `strata update`。
|
||||
- **提示 8080 端口已被占用。** Strata 已经在运行了。找一下它的窗口。
|
||||
**Claude Code · Codex CLI · OpenCode · oh-my-pi · pi.dev · Hermes Agent · OpenClaw**,也可以通过兼容 OpenAI 和 Anthropic 的 API 使用更多工具。
|
||||
|
||||
更多解决办法:[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)。还是解决不了?请提交
|
||||
[issue](https://github.com/Niko1221/Strata/issues) 并附上日志:在应用里是**设置,引擎,诊断,保存日志**,在终端里是
|
||||
`strata doctor` 的输出。如果是安全问题,请私下报告:[SECURITY.md](SECURITY.md)。
|
||||
运行 **`strata connect`**,查看你所用工具的设置方法。[连接指南](docs/TERMINAL_LAUNCH.md#connect-your-apps-strata-connect)。
|
||||
|
||||
## 致谢与许可证
|
||||
<a id="guides"></a>
|
||||
<a id="how-fast-is-it"></a>
|
||||
<a id="how-does-it-work"></a>
|
||||
<a id="something-went-wrong"></a>
|
||||
|
||||
模型是 Qwen 团队的 [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)。压缩工作由
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF)、UkisAI(Swift 1.5)、agentionai(Gyro)
|
||||
和 Unsloth 完成。DeepSeek V4.1-Flash 的各个版本也由 ISTA-DASLab 制作。Strata 使用了
|
||||
[llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) 的部分代码。全部致谢:
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits)。Strata 以 [MIT 许可证](LICENSE)开源。少数部分和所有模型有各自的许可证
|
||||
([具体是哪些](docs/HOW_IT_WORKS.md#license))。
|
||||
## 帮助
|
||||
|
||||
## 参与贡献
|
||||
|
||||
发现了 bug,或者想提交修改或基准测试结果?请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。其中说明了需要附上哪些内容,
|
||||
方便其他人核实你的工作。
|
||||
[全部指南](docs) · [故障排查](docs/TROUBLESHOOTING.md) · [报告问题](https://github.com/Niko1221/Strata/issues/new/choose) ·
|
||||
[基准测试](docs/COMMUNITY_BENCHMARKS.md) · [工作原理](docs/HOW_IT_WORKS.md) · [参与贡献](CONTRIBUTING.md)
|
||||
|
||||
## 支持 Strata
|
||||
|
||||
Strata 免费,采用 MIT 许可证。我在能拿到的各种硬件上开发和测试它。如果它在你的电脑上跑得不错,请我喝杯咖啡,能帮我继续测试新显卡、
|
||||
修复你们反馈的问题。不用有压力。使用它并反馈你的发现,同样很有帮助。
|
||||
**帮助本地 AI 保持开放。** 一杯咖啡能帮助我们在更多硬件上开发和测试。
|
||||
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee:支持 Strata" height="48"></a>
|
||||
|
||||
有兴趣赞助 Strata?请通过 [GitHub](https://github.com/Niko1221) 联系我。
|
||||
[MIT 许可证](LICENSE) · [致谢和模型许可证](docs/HOW_IT_WORKS.md#credits) · [隐私](docs/SAFETY.md)
|
||||
|
||||
<a id="option-c-let-your-ai-set-it-up"></a>
|
||||
|
||||
<sub>面向 AI 工具和自动阅读器: <a href="AGENTS.md">技术参考</a> · <a href="docs/AI_SETUP.md">安装说明</a></sub>
|
||||
|
||||
**赞助、硬件合作或其他合作:** [[email protected]](mailto:[email protected]).
|
||||
+3
-2
@@ -23,8 +23,9 @@ this PC only. The details and every setting are in [docs/DETAILS.md](docs/DETAIL
|
||||
- **Where it listens.** `127.0.0.1` by default. `--host 0.0.0.0` (or `"host"` in `strata-<model>.json`) opens it to
|
||||
your network, and the server then warns when no API key is set.
|
||||
- **API key.** `"api_key"` in the run config (or `STRATA_API_KEY`) is required on `/v1/*` and on every endpoint
|
||||
that shows the model's state, requests or answers (`/status`, `/metrics`, `/settings`, `/mcp`, `/props`, `/slots`,
|
||||
`/api/requests`, `/config`) and on every `POST`. It is compared in constant time. Set one before you open the
|
||||
that shows the model's state, requests or answers (`/status`, `/metrics`, `/metrics/prometheus`, `/settings`,
|
||||
`/mcp`, `/props`, `/slots`, `/api/requests`, `/config`) and on every `POST`. It is compared in constant time.
|
||||
Set one before you open the
|
||||
server to your network or put a tunnel in front of it.
|
||||
- **Host check (DNS rebinding, 0.1.38).** Without an API key the server answers only requests whose `Host` is a name
|
||||
it knows (`localhost`, an IP address, the address it listens on and, when it listens beyond this PC, this PC's
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "strata-desktop",
|
||||
"productName": "Strata",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.7",
|
||||
"private": true,
|
||||
"description": "Strata Desktop - local AI, tuned to one model.",
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/** Strata's output-budget correction for the staged pi-ai copy. */
|
||||
import { createHash } from 'node:crypto';
|
||||
import { lstatSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
|
||||
export const HARNESS_BUDGET_PATCH = Object.freeze({
|
||||
id: 'strata-pi-ai-context-safety-v2',
|
||||
package: '@earendil-works/pi-ai',
|
||||
version: '1.1.0',
|
||||
file: 'node_modules/@earendil-works/pi-ai/dist/api/simple-options.js',
|
||||
beforeSha256: '9b3cc0ae6daa86fdc050bc0cb0f3f986401e64ccddbde6757183c9f8ad411be2',
|
||||
});
|
||||
|
||||
const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
|
||||
const oldClamp = 'const available = model.contextWindow - estimateContextTokens(context).tokens - CONTEXT_SAFETY_TOKENS;';
|
||||
// Small windows also need room after tool results; a fixed margin must not
|
||||
// consume all remaining estimated space. Keep the old 64K+ calculation.
|
||||
const newClamp = `const remaining = model.contextWindow - estimateContextTokens(context).tokens;
|
||||
const safety = model.contextWindow >= 65536 ? CONTEXT_SAFETY_TOKENS
|
||||
: Math.min(CONTEXT_SAFETY_TOKENS, Math.floor(model.contextWindow / 16), Math.max(0, Math.floor(remaining / 2)));
|
||||
const available = remaining - safety;`;
|
||||
|
||||
/**
|
||||
* Patch only a flattened, disposable staged Harness. Returns the BUNDLE.json
|
||||
* provenance entry; the caller records it when writing the bundle manifest.
|
||||
* A repeat application or any unknown package/source fails before writing.
|
||||
*/
|
||||
export function patchStagedHarnessBudget(stagedRoot) {
|
||||
const fail = message => { throw new Error(`Harness output-budget patch: ${message}`); };
|
||||
const root = realpathSync(resolve(stagedRoot));
|
||||
const packageDir = join(root, 'node_modules', '@earendil-works', 'pi-ai');
|
||||
const target = join(root, HARNESS_BUDGET_PATCH.file);
|
||||
let manifest, before;
|
||||
try {
|
||||
manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
|
||||
// Flattening must have removed links; never write through a shared source file.
|
||||
if (realpathSync(target) !== target || !lstatSync(target).isFile() || lstatSync(target).nlink !== 1)
|
||||
fail('expected an unlinked staged module');
|
||||
before = readFileSync(target);
|
||||
} catch (error) {
|
||||
fail(`cannot read a standalone staged pi-ai dependency (${error.message})`);
|
||||
}
|
||||
if (manifest?.name !== HARNESS_BUDGET_PATCH.package || manifest?.version !== HARNESS_BUDGET_PATCH.version)
|
||||
fail('expected @earendil-works/pi-ai 1.1.0; review the patch before staging a different dependency');
|
||||
if (sha256(before) !== HARNESS_BUDGET_PATCH.beforeSha256)
|
||||
fail('unexpected simple-options.js SHA-256; review the dependency source before staging');
|
||||
const source = before.toString('utf8');
|
||||
if (source.split(oldClamp).length !== 2) fail('expected exactly one known context clamp');
|
||||
const after = Buffer.from(source.replace(oldClamp, newClamp));
|
||||
writeFileSync(target, after);
|
||||
return { ...HARNESS_BUDGET_PATCH, afterSha256: sha256(after) };
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { join, relative, resolve, sep } from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { flattenPnpm } from './flatten-pnpm.mjs';
|
||||
import { isOtherOsBinaryFolder, missingHarnessImports } from './harness-prune.mjs';
|
||||
import { patchStagedHarnessBudget } from './harness-budget.mjs';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const opt = (name, fallback) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : fallback; };
|
||||
@@ -144,6 +145,9 @@ function prune(dir) {
|
||||
}
|
||||
prune(out);
|
||||
|
||||
// Strata's provenance-recorded output-budget correction applies only to this copy.
|
||||
const patches = [patchStagedHarnessBudget(out)];
|
||||
|
||||
// --- licence: the MIT text and the third-party notices travel with the files ---------------------------------------------
|
||||
for (const name of ['LICENSE', 'THIRD_PARTY_NOTICES.md']) {
|
||||
if (existsSync(join(src, name))) cpSync(join(src, name), join(out, name));
|
||||
@@ -156,6 +160,7 @@ const total = size(out);
|
||||
writeFileSync(join(out, 'BUNDLE.json'), JSON.stringify({
|
||||
component: 'deepseek-harness', version: cli.version, commit, license: 'MIT', platform, node: process.version,
|
||||
bytes: total.bytes, files: total.files, entry: 'apps/cli/lib/bin.js', builtAt: new Date().toISOString(),
|
||||
patches,
|
||||
note: 'Pruned production install (pnpm deploy --prod). No package manager runs on the user\'s PC.',
|
||||
}, null, 2));
|
||||
|
||||
|
||||
Generated
+155
-1
@@ -349,6 +349,12 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.1"
|
||||
@@ -1755,6 +1761,19 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png 0.18.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
@@ -2097,6 +2116,20 @@ version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "mac-notification-sys"
|
||||
version = "0.6.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"time",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.39.0"
|
||||
@@ -2166,6 +2199,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.20.0"
|
||||
@@ -2229,6 +2272,20 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "notify-rust"
|
||||
version = "4.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4587364a9a0074333429b3df75a30a205340c56a536ca3eb6ca0e59b87bbf8af"
|
||||
dependencies = [
|
||||
"futures-lite",
|
||||
"log",
|
||||
"mac-notification-sys",
|
||||
"serde",
|
||||
"tauri-winrt-notification",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.2"
|
||||
@@ -2757,6 +2814,15 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "precomputed-hash"
|
||||
version = "0.1.1"
|
||||
@@ -2825,6 +2891,12 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.42.0"
|
||||
@@ -2855,6 +2927,35 @@ version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.6.2"
|
||||
@@ -3524,13 +3625,14 @@ checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7"
|
||||
|
||||
[[package]]
|
||||
name = "strata-desktop"
|
||||
version = "0.2.5"
|
||||
version = "0.2.7"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-updater",
|
||||
@@ -3755,6 +3857,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"image",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -3869,6 +3972,26 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-notification"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b939d7b19c8af1381a72ef8474b3aba2b56c1ddac2c23f0c8e96abbcf86b198b"
|
||||
dependencies = [
|
||||
"log",
|
||||
"notify-rust",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.21",
|
||||
"time",
|
||||
"url",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-shell"
|
||||
version = "2.5.0"
|
||||
@@ -4042,6 +4165,17 @@ dependencies = [
|
||||
"toml 1.1.8+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-winrt-notification"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f37a6c354fd28fc9e322ed9bd47e3959576dad28c9d58ea1cf888cce1c7ccb36"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
"windows",
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
@@ -5332,6 +5466,26 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.8"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "strata-desktop"
|
||||
version = "0.2.5"
|
||||
version = "0.2.7"
|
||||
description = "Strata Desktop — local AI, tuned to one model."
|
||||
edition = "2021"
|
||||
rust-version = "1.77"
|
||||
@@ -15,8 +15,10 @@ webkit2gtk = "2.0"
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri = { version = "2", features = ["tray-icon", "image-png"] }
|
||||
tauri-plugin-shell = "2"
|
||||
# Desktop notifications for an approval that waits (#3477); the tray icon above is Tauri's own.
|
||||
tauri-plugin-notification = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-single-instance = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-exit":{"identifier":"allow-exit","description":"Enables the exit command without any pre-configured scope.","commands":{"allow":["exit"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-exit":{"identifier":"deny-exit","description":"Denies the exit command without any pre-configured scope.","commands":{"allow":[],"deny":["exit"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"DenLine truncated
|
||||
{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-exit":{"identifier":"allow-exit","description":"Enables the exit command without any pre-configured scope.","commands":{"allow":["exit"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-exit":{"identifier":"deny-exit","description":"Denies the exit command without any pre-configured scope.","commands":{"allow":[],"deny":["exit"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"DenLine truncated
|
||||
@@ -2426,6 +2426,216 @@
|
||||
"const": "core:window:deny-unminimize",
|
||||
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
|
||||
"type": "string",
|
||||
"const": "notification:default",
|
||||
"markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the batch command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-batch",
|
||||
"markdownDescription": "Enables the batch command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the cancel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-cancel",
|
||||
"markdownDescription": "Enables the cancel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the check_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-check-permissions",
|
||||
"markdownDescription": "Enables the check_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the create_channel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-create-channel",
|
||||
"markdownDescription": "Enables the create_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the delete_channel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-delete-channel",
|
||||
"markdownDescription": "Enables the delete_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-get-active",
|
||||
"markdownDescription": "Enables the get_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_pending command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-get-pending",
|
||||
"markdownDescription": "Enables the get_pending command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the is_permission_granted command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-is-permission-granted",
|
||||
"markdownDescription": "Enables the is_permission_granted command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the list_channels command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-list-channels",
|
||||
"markdownDescription": "Enables the list_channels command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the notify command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-notify",
|
||||
"markdownDescription": "Enables the notify command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the permission_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-permission-state",
|
||||
"markdownDescription": "Enables the permission_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register_action_types command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-register-action-types",
|
||||
"markdownDescription": "Enables the register_action_types command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-register-listener",
|
||||
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-remove-active",
|
||||
"markdownDescription": "Enables the remove_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-remove-listener",
|
||||
"markdownDescription": "Enables the remove_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the request_permission command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-request-permission",
|
||||
"markdownDescription": "Enables the request_permission command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the show command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:allow-show",
|
||||
"markdownDescription": "Enables the show command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the batch command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-batch",
|
||||
"markdownDescription": "Denies the batch command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the cancel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-cancel",
|
||||
"markdownDescription": "Denies the cancel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the check_permissions command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-check-permissions",
|
||||
"markdownDescription": "Denies the check_permissions command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the create_channel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-create-channel",
|
||||
"markdownDescription": "Denies the create_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the delete_channel command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-delete-channel",
|
||||
"markdownDescription": "Denies the delete_channel command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-get-active",
|
||||
"markdownDescription": "Denies the get_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_pending command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-get-pending",
|
||||
"markdownDescription": "Denies the get_pending command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the is_permission_granted command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-is-permission-granted",
|
||||
"markdownDescription": "Denies the is_permission_granted command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the list_channels command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-list-channels",
|
||||
"markdownDescription": "Denies the list_channels command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the notify command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-notify",
|
||||
"markdownDescription": "Denies the notify command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the permission_state command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-permission-state",
|
||||
"markdownDescription": "Denies the permission_state command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register_action_types command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-register-action-types",
|
||||
"markdownDescription": "Denies the register_action_types command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-register-listener",
|
||||
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_active command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-remove-active",
|
||||
"markdownDescription": "Denies the remove_active command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove_listener command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-remove-listener",
|
||||
"markdownDescription": "Denies the remove_listener command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the request_permission command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-request-permission",
|
||||
"markdownDescription": "Denies the request_permission command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the show command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "notification:deny-show",
|
||||
"markdownDescription": "Denies the show command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
|
||||
"type": "string",
|
||||
@@ -2491,6 +2701,60 @@
|
||||
"type": "string",
|
||||
"const": "shell:deny-stdin-write",
|
||||
"markdownDescription": "Denies the stdin_write command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`",
|
||||
"type": "string",
|
||||
"const": "updater:default",
|
||||
"markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the check command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:allow-check",
|
||||
"markdownDescription": "Enables the check command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the download command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:allow-download",
|
||||
"markdownDescription": "Enables the download command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the download_and_install command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:allow-download-and-install",
|
||||
"markdownDescription": "Enables the download_and_install command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the install command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:allow-install",
|
||||
"markdownDescription": "Enables the install command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the check command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:deny-check",
|
||||
"markdownDescription": "Denies the check command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the download command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:deny-download",
|
||||
"markdownDescription": "Denies the download command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the download_and_install command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:deny-download-and-install",
|
||||
"markdownDescription": "Denies the download_and_install command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the install command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "updater:deny-install",
|
||||
"markdownDescription": "Denies the install command without any pre-configured scope."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -701,6 +701,35 @@ SectionEnd
|
||||
!macroend
|
||||
; STRATA-MIGRATE-END
|
||||
|
||||
; STRATA-STOP-BEGIN
|
||||
; Stop what Strata itself started from the install folder (the app, its host, the node runtime, the engine) before files are removed:
|
||||
; a running copy keeps its files open, so the folder stayed behind after use. Only processes whose image path is inside INSTDIR are
|
||||
; stopped; this uninstaller's own process and any program elsewhere are left alone.
|
||||
!macro StrataStopOwnProcesses INSTALLDIR
|
||||
System::Call 'kernel32::SetEnvironmentVariableW(w "STRATA_INSTDIR", w "${INSTALLDIR}")'
|
||||
System::Call 'kernel32::GetCurrentProcessId()i.r9'
|
||||
System::Call 'kernel32::SetEnvironmentVariableW(w "STRATA_SELFPID", w "$9")'
|
||||
; the installer is a 32-bit program, so plain powershell.exe is the 32-bit one, which cannot read the image path of a 64-bit process: use the 64-bit one
|
||||
StrCpy $8 "$WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe"
|
||||
IfFileExists $8 +2 0
|
||||
StrCpy $8 "powershell.exe"
|
||||
nsExec::Exec '"$8" -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "$$d = [IO.Path]::GetFullPath($$env:STRATA_INSTDIR).TrimEnd([char]92) + [char]92; $$ids = @(); $$ids += Get-Process -ErrorAction SilentlyContinue | Where-Object { $$_.Path -and [IO.Path]::GetFullPath($$_.Path).StartsWith($$d, [StringComparison]::OrdinalIgnoreCase) } | ForEach-Object { $$_.Id }; try { $$ids += Get-CimInstance Win32_Process -ErrorAction Stop | Where-Object { $$_.ExecutablePath -and [IO.Path]::GetFullPath($$_.ExecutablePath).StartsWith($$d, [StringComparison]::OrdinalIgnoreCase) } | ForEach-Object { $$_.ProcessId } } catch {}; $$ids | Sort-Object -Unique | Where-Object { $$_ -ne [int]$$env:STRATA_SELFPID } | ForEach-Object { Stop-Process -Id $$_ -Force -ErrorAction SilentlyContinue }; Start-Sleep -Milliseconds 1500"'
|
||||
Pop $9
|
||||
!macroend
|
||||
; STRATA-STOP-END
|
||||
|
||||
; STRATA-CLEAN-BEGIN
|
||||
; The app writes into its own folders while it runs (Python bytecode caches under control\, for one), and the file-by-file delete below does not know them,
|
||||
; so those folders stayed behind with a few dozen files. These four folders hold only what the installer put there and what the app made there;
|
||||
; user data is never in them (it lives in the data folder), and the install folder itself is still removed with a plain RMDir.
|
||||
!macro StrataRemoveAppFolders INSTALLDIR
|
||||
RMDir /r "${INSTALLDIR}\control"
|
||||
RMDir /r "${INSTALLDIR}\host"
|
||||
RMDir /r "${INSTALLDIR}\node"
|
||||
RMDir /r "${INSTALLDIR}\harness"
|
||||
!macroend
|
||||
; STRATA-CLEAN-END
|
||||
|
||||
Section Install
|
||||
SetOutPath $INSTDIR
|
||||
|
||||
@@ -847,6 +876,7 @@ Section Uninstall
|
||||
!insertmacro NSIS_HOOK_PREUNINSTALL
|
||||
!endif
|
||||
|
||||
!insertmacro StrataStopOwnProcesses "$INSTDIR"
|
||||
!insertmacro CheckIfAppIsRunning "$INSTDIR\${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
||||
|
||||
; Delete the app directory and its content from disk
|
||||
@@ -879,6 +909,8 @@ Section Uninstall
|
||||
{{/each}}
|
||||
|
||||
|
||||
!insertmacro StrataRemoveAppFolders "$INSTDIR"
|
||||
|
||||
; Delete uninstaller
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ use tauri::{Manager, WebviewUrl, WebviewWindowBuilder};
|
||||
|
||||
mod updater;
|
||||
mod desktoplog;
|
||||
mod shell;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod supervisor;
|
||||
|
||||
@@ -252,13 +253,24 @@ fn pick_port() -> Result<u16, String> {
|
||||
/// launch starts with empty storage, as before).
|
||||
const PREFERRED_PORT: u16 = 47831;
|
||||
|
||||
/// Set when the port the person (or the app) wanted was busy: "busy:<wanted>:<used>". The host gets it as STRATA_PORT_NOTICE and tells the
|
||||
/// person (the chats and settings saved on the page belong to the port, so a different port starts empty) (#3402).
|
||||
static PORT_NOTICE: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
|
||||
|
||||
fn pick_stable_port() -> Result<u16, String> {
|
||||
match TcpListener::bind(("127.0.0.1", PREFERRED_PORT)) {
|
||||
// The person may have chosen a port in Settings (shell-prefs.json); otherwise the app's usual one.
|
||||
let wanted = shell::wanted_port(&shell::read_prefs(), PREFERRED_PORT);
|
||||
*PORT_NOTICE.lock().unwrap() = None;
|
||||
match TcpListener::bind(("127.0.0.1", wanted)) {
|
||||
Ok(listener) => {
|
||||
drop(listener);
|
||||
Ok(PREFERRED_PORT)
|
||||
Ok(wanted)
|
||||
}
|
||||
Err(_) => {
|
||||
let used = pick_port()?;
|
||||
*PORT_NOTICE.lock().unwrap() = Some(shell::busy_notice(wanted, used));
|
||||
Ok(used)
|
||||
}
|
||||
Err(_) => pick_port(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,6 +481,9 @@ fn host_command(res: &Path, port: u16, shutdown_token: &str) -> Result<Command,
|
||||
.stderr(Stdio::inherit());
|
||||
apply_runtime_policy(&mut cmd, res);
|
||||
hide_console(&mut cmd);
|
||||
if let Some(notice) = PORT_NOTICE.lock().unwrap().clone() {
|
||||
cmd.env("STRATA_PORT_NOTICE", notice);
|
||||
}
|
||||
// A user with the Strata API worker on a different port sets this; unset means the worker's
|
||||
// documented loopback default.
|
||||
if let Ok(url) = std::env::var("STRATA_SERVER_URL") {
|
||||
@@ -875,6 +890,7 @@ fn run_embedded() {
|
||||
}))
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.manage(HostProcess(Mutex::new(None)))
|
||||
.manage(HostPort(Mutex::new(None)))
|
||||
.manage(ShutdownToken(Mutex::new(None)))
|
||||
@@ -901,6 +917,20 @@ fn run_embedded() {
|
||||
Ok(())
|
||||
})
|
||||
.on_window_event(|window, event| {
|
||||
// "Minimise to tray" (Settings, off by default): closing the main window hides it and keeps Strata running in the tray; Quit in the
|
||||
// tray menu ends it (#3477). If no tray could be made the window closes as before.
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
if window.label() == "main" {
|
||||
let prefs = shell::read_prefs();
|
||||
if prefs.tray {
|
||||
api.prevent_close();
|
||||
shell::hide_to_tray(&window.app_handle().clone(), &prefs.language);
|
||||
if !shell::tray_exists(&window.app_handle().clone()) {
|
||||
let _ = window.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let tauri::WindowEvent::Destroyed = event {
|
||||
stop_host(&window.app_handle().clone());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
//! The shell's own choices and the two things the host asks of it besides updates (#3402, #3477).
|
||||
//!
|
||||
//! `<Strata data folder>/shell-prefs.json` is written by the App Host when the person changes a setting
|
||||
//! (`apps/host/server/shell-prefs.mjs`) and read here when Strata starts, before any page exists:
|
||||
//!
|
||||
//! { "port": 0 | 1024..65535, "tray": false, "language": "en" }
|
||||
//!
|
||||
//! * `port`: the port the window's origin uses. 0 is the default, 47831. When the port is busy the shell takes a free one and tells the
|
||||
//! host (`STRATA_PORT_NOTICE=busy:<wanted>:<used>`), which shows the person a warning: the chats and settings of the page are kept per
|
||||
//! port, so a different port starts empty.
|
||||
//! * `tray`: closing the window hides it and keeps Strata running in the tray (Windows, macOS). The tray menu follows `language`.
|
||||
//! * `/shell/notify` on the loopback bridge shows a desktop notification (an approval is waiting) whether the window is open or hidden.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Prefs {
|
||||
pub port: u16,
|
||||
pub tray: bool,
|
||||
pub language: String,
|
||||
}
|
||||
|
||||
impl Default for Prefs {
|
||||
fn default() -> Self {
|
||||
Prefs { port: 0, tray: false, language: "en".into() }
|
||||
}
|
||||
}
|
||||
|
||||
pub const LANGUAGES: [&str; 8] = ["en", "zh", "ja", "de", "es", "ru", "pt", "ko"];
|
||||
|
||||
pub fn prefs_path() -> PathBuf {
|
||||
crate::desktoplog::data_dir().join("shell-prefs.json")
|
||||
}
|
||||
|
||||
/// The file's text as choices; anything missing or out of range is the default.
|
||||
pub fn parse_prefs(text: &str) -> Prefs {
|
||||
let value: Value = serde_json::from_str(text.trim_start_matches('\u{feff}')).unwrap_or(Value::Null);
|
||||
let port = value.get("port").and_then(Value::as_u64).filter(|p| (1024..=65535).contains(p)).unwrap_or(0) as u16;
|
||||
let tray = value.get("tray").and_then(Value::as_bool).unwrap_or(false);
|
||||
let language = value
|
||||
.get("language")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|l| LANGUAGES.contains(l))
|
||||
.unwrap_or("en")
|
||||
.to_string();
|
||||
Prefs { port, tray, language }
|
||||
}
|
||||
|
||||
pub fn read_prefs() -> Prefs {
|
||||
std::fs::read_to_string(prefs_path()).map(|t| parse_prefs(&t)).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The port to ask for: the person's choice, else the app's usual one.
|
||||
pub fn wanted_port(prefs: &Prefs, usual: u16) -> u16 {
|
||||
if prefs.port != 0 { prefs.port } else { usual }
|
||||
}
|
||||
|
||||
/// The notice the host shows when the wanted port was taken.
|
||||
pub fn busy_notice(wanted: u16, used: u16) -> String {
|
||||
format!("busy:{wanted}:{used}")
|
||||
}
|
||||
|
||||
/// `%41%20b` -> "A b" (a query value from the host). Bad escapes are kept as typed.
|
||||
pub fn percent_decode(text: &str) -> String {
|
||||
let bytes = text.as_bytes();
|
||||
let mut out = Vec::with_capacity(bytes.len());
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
if bytes[i] == b'%' && i + 2 < bytes.len() {
|
||||
if let Some(v) = std::str::from_utf8(&bytes[i + 1..i + 3]).ok().and_then(|h| u8::from_str_radix(h, 16).ok()) {
|
||||
out.push(v);
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.push(if bytes[i] == b'+' { b' ' } else { bytes[i] });
|
||||
i += 1;
|
||||
}
|
||||
String::from_utf8_lossy(&out).into_owned()
|
||||
}
|
||||
|
||||
/// The value of `key` in the query of a request path (`/shell/notify?title=..&body=..`).
|
||||
pub fn query_value(path: &str, key: &str) -> Option<String> {
|
||||
let query = path.split_once('?')?.1;
|
||||
query.split('&').find_map(|pair| {
|
||||
let (k, v) = pair.split_once('=')?;
|
||||
(k == key).then(|| percent_decode(v))
|
||||
})
|
||||
}
|
||||
|
||||
/// A short single-line text for a notification: no control characters, bounded.
|
||||
pub fn clean_text(text: &str, max: usize) -> String {
|
||||
text.chars().filter(|c| !c.is_control()).take(max).collect::<String>().trim().to_string()
|
||||
}
|
||||
|
||||
/// The tray menu: (open, quit, tooltip), in the app's language.
|
||||
pub fn tray_labels(language: &str) -> (&'static str, &'static str, &'static str) {
|
||||
match language {
|
||||
"zh" => ("打开 Strata", "退出 Strata", "Strata"),
|
||||
"ja" => ("Strata を開く", "Strata を終了", "Strata"),
|
||||
"de" => ("Strata öffnen", "Strata beenden", "Strata"),
|
||||
"es" => ("Abrir Strata", "Salir de Strata", "Strata"),
|
||||
"ru" => ("Открыть Strata", "Выйти из Strata", "Strata"),
|
||||
"pt" => ("Abrir o Strata", "Sair do Strata", "Strata"),
|
||||
"ko" => ("Strata 열기", "Strata 종료", "Strata"),
|
||||
_ => ("Open Strata", "Quit Strata", "Strata"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
mod native {
|
||||
use tauri::menu::{Menu, MenuItem};
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
|
||||
const TRAY_ID: &str = "strata-tray";
|
||||
|
||||
fn show_main(app: &AppHandle) {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
|
||||
/// Hide the window and keep Strata running in the tray (made on the first need, with the menu in the app's language).
|
||||
pub fn hide_to_tray(app: &AppHandle, language: &str) {
|
||||
if app.tray_by_id(TRAY_ID).is_none() {
|
||||
let (open, quit, tip) = super::tray_labels(language);
|
||||
let built = (|| -> tauri::Result<()> {
|
||||
let open_item = MenuItem::with_id(app, "open", open, true, None::<&str>)?;
|
||||
let quit_item = MenuItem::with_id(app, "quit", quit, true, None::<&str>)?;
|
||||
let menu = Menu::with_items(app, &[&open_item, &quit_item])?;
|
||||
let mut builder = TrayIconBuilder::with_id(TRAY_ID)
|
||||
.tooltip(tip)
|
||||
.menu(&menu)
|
||||
.show_menu_on_left_click(false)
|
||||
.on_menu_event(|app, event| match event.id().as_ref() {
|
||||
"open" => show_main(app),
|
||||
"quit" => app.exit(0),
|
||||
_ => {}
|
||||
})
|
||||
.on_tray_icon_event(|tray, event| {
|
||||
if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = event {
|
||||
show_main(tray.app_handle());
|
||||
}
|
||||
});
|
||||
if let Some(icon) = app.default_window_icon() {
|
||||
builder = builder.icon(icon.clone());
|
||||
}
|
||||
builder.build(app)?;
|
||||
Ok(())
|
||||
})();
|
||||
if built.is_err() {
|
||||
// No tray could be made (a desktop without one): do not hide a window nobody could bring back.
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.hide();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tray_exists(app: &AppHandle) -> bool {
|
||||
app.tray_by_id(TRAY_ID).is_some()
|
||||
}
|
||||
|
||||
/// Show a desktop notification; a hidden or minimised window also asks for attention (the taskbar button flashes).
|
||||
pub fn notify(app: &AppHandle, title: &str, body: &str) -> bool {
|
||||
if title.is_empty() {
|
||||
return false;
|
||||
}
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let hidden = !window.is_visible().unwrap_or(true) || window.is_minimized().unwrap_or(false);
|
||||
if hidden {
|
||||
let _ = window.request_user_attention(Some(tauri::UserAttentionType::Informational));
|
||||
}
|
||||
}
|
||||
app.notification().builder().title(title).body(body).show().is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub use native::{hide_to_tray, notify, tray_exists};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_file_gives_choices_and_anything_wrong_is_the_default() {
|
||||
assert_eq!(parse_prefs(""), Prefs::default());
|
||||
assert_eq!(parse_prefs("{ not json"), Prefs::default());
|
||||
let p = parse_prefs(r#"{"port":47900,"tray":true,"language":"de"}"#);
|
||||
assert_eq!((p.port, p.tray, p.language.as_str()), (47900, true, "de"));
|
||||
assert_eq!(parse_prefs("\u{feff}{\"port\":47900}").port, 47900);
|
||||
assert_eq!(parse_prefs(r#"{"port":80}"#).port, 0, "a port below 1024 is not used");
|
||||
assert_eq!(parse_prefs(r#"{"port":70000}"#).port, 0);
|
||||
assert_eq!(parse_prefs(r#"{"port":"47831"}"#).port, 0, "a text is not a number");
|
||||
assert_eq!(parse_prefs(r#"{"tray":"yes","language":"xx"}"#), Prefs::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_wanted_port_is_the_choice_or_the_usual_one() {
|
||||
assert_eq!(wanted_port(&Prefs::default(), 47831), 47831);
|
||||
assert_eq!(wanted_port(&Prefs { port: 47900, ..Prefs::default() }, 47831), 47900);
|
||||
assert_eq!(busy_notice(47831, 51234), "busy:47831:51234");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_values_are_decoded_and_bounded() {
|
||||
let path = "/shell/notify?title=Strata%20needs%20your%20answer&body=a%20%26%20b%3F+ok";
|
||||
assert_eq!(query_value(path, "title").as_deref(), Some("Strata needs your answer"));
|
||||
assert_eq!(query_value(path, "body").as_deref(), Some("a & b? ok"));
|
||||
assert_eq!(query_value(path, "missing"), None);
|
||||
assert_eq!(query_value("/shell/notify", "title"), None);
|
||||
assert_eq!(percent_decode("100%"), "100%");
|
||||
assert_eq!(percent_decode("%zz%41"), "%zzA");
|
||||
assert_eq!(percent_decode("%C3%A4"), "ä");
|
||||
assert_eq!(clean_text("a\nb\u{7}c", 10), "abc");
|
||||
assert_eq!(clean_text("abcdef", 3), "abc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_language_has_tray_labels() {
|
||||
for language in LANGUAGES {
|
||||
let (open, quit, tip) = tray_labels(language);
|
||||
assert!(!open.is_empty() && !quit.is_empty() && !tip.is_empty(), "{language}");
|
||||
if language != "en" {
|
||||
assert_ne!(open, tray_labels("en").0, "{language}");
|
||||
}
|
||||
}
|
||||
assert_eq!(tray_labels("xx"), tray_labels("en"));
|
||||
}
|
||||
}
|
||||
@@ -210,6 +210,16 @@ fn handle(mut stream: TcpStream, app: &AppHandle, token: &str, progress: &Arc<Mu
|
||||
respond(&mut stream, "202 Accepted", &progress.lock().unwrap().json());
|
||||
}
|
||||
("GET", "/app-update/status") => respond(&mut stream, "200 OK", &progress.lock().unwrap().json()),
|
||||
// The host asks for a desktop notification (an approval is waiting): title and body are in the query, bounded and cleaned here.
|
||||
("POST", p) if p.starts_with("/shell/notify") => {
|
||||
let title = crate::shell::clean_text(&crate::shell::query_value(p, "title").unwrap_or_default(), 120);
|
||||
let body = crate::shell::clean_text(&crate::shell::query_value(p, "body").unwrap_or_default(), 300);
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let shown = crate::shell::notify(app, &title, &body);
|
||||
#[cfg(target_os = "linux")]
|
||||
let shown = false;
|
||||
respond(&mut stream, "200 OK", &serde_json::json!({ "shown": shown }))
|
||||
}
|
||||
_ => respond(&mut stream, "404 Not Found", &serde_json::json!({ "error": "unknown route" })),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,3 +10,15 @@ npm run preview:ui
|
||||
The built UI is included. To rebuild after source edits, run `npm run build`. `preview:ui` uses isolated simulated data; `npm run dev` connects the normal host.
|
||||
|
||||
The previous project README is retained in [docs/README-historical.md](docs/README-historical.md). It contains older implementation status and is historical reference.
|
||||
|
||||
## Languages
|
||||
|
||||
The app speaks English, Chinese, Japanese, German, French, Spanish, Russian, Portuguese and Korean. The English text is the key: `src/locales/en.json`
|
||||
(with `fix-en.json`, `tune-en.json` and `tools-en.json`) holds every string, and each language has the same four files (`fr.json`, `fix-fr.json`, ...).
|
||||
|
||||
- **A new string.** Write it with `tr("...")`, then add it to the English catalog (`node scripts/append-localization.mjs`, or the next `fx` number in
|
||||
`scripts/make-fix-locales.py`). The language tests fail until every language has it.
|
||||
- **French.** `python scripts/fr_sync.py missing todo.tsv` lists the English strings that have no French yet, one per line (`id`, a tab, the text).
|
||||
Translate the second column ("vous", short plain words, one next step; a lone `=` keeps the English), then `python scripts/fr_sync.py apply todo.tsv` merges it
|
||||
(it also sets the French spacing before `: ; ? !` and the typographic apostrophe) and `python scripts/fr_sync.py check` confirms nothing is missing.
|
||||
- **Screenshots in a language.** `npm run build`, then `node scripts/language-shots.mjs fr <folder> models settings setup` (hermetic preview host, scratch profile, headless Chrome).
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "strata",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "strata",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.7",
|
||||
"dependencies": {
|
||||
"html-to-image": "^1.11.13",
|
||||
"katex": "^0.19.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "strata",
|
||||
"private": true,
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.7",
|
||||
"type": "module",
|
||||
"description": "Strata \u2014 local AI, tuned to one model. Web shell + engine host.",
|
||||
"scripts": {
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,156 @@
|
||||
"""French (fr) catalog tooling for the desktop app (#3970).
|
||||
|
||||
The French texts live in src/locales/fr.json, fix-fr.json, tune-fr.json and tools-fr.json, next to the other languages.
|
||||
They are edited by hand (they are the translations, not generated), so a string added to an English catalog later has no
|
||||
French yet. This script lists and merges those. From apps/host:
|
||||
|
||||
python scripts/fr_sync.py missing [out.tsv] write every English string that has no French yet
|
||||
python scripts/fr_sync.py apply done.tsv merge translations, rewrite the four fr files in English key order
|
||||
python scripts/fr_sync.py check exit 1 if a French string is missing, empty or breaks a placeholder / tag
|
||||
|
||||
The TSV has one string per line: <id><TAB><text>. In the text, a newline is written \\n and a backslash \\\\.
|
||||
In the "done" file a lone = means "keep the English text as it is" (class names, product names, file names).
|
||||
The id prefix picks the file: s0001 -> fr.json, fx01 -> fix-fr.json, tune01 -> tune-fr.json, tl01 -> tools-fr.json.
|
||||
Use "vous", plain short words, a next step, and a no-break space (U+00A0) before : ; ? ! and inside guillemets.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
LOCALES = Path(__file__).resolve().parent.parent / "src" / "locales"
|
||||
PAIRS = [("en.json", "fr.json"), ("fix-en.json", "fix-fr.json"), ("tune-en.json", "tune-fr.json"), ("tools-en.json", "tools-fr.json")]
|
||||
SLOT = re.compile(r"\{\d+\}")
|
||||
TAG = re.compile(r"</?[A-Za-z][^>]*>")
|
||||
|
||||
|
||||
def load(name):
|
||||
path = LOCALES / name
|
||||
return json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}
|
||||
|
||||
|
||||
def file_of(identifier):
|
||||
for en_name, fr_name in PAIRS:
|
||||
if identifier in load(en_name):
|
||||
return fr_name
|
||||
raise SystemExit(f"unknown id {identifier}: it is in no English catalog")
|
||||
|
||||
|
||||
def escape(text):
|
||||
return text.replace("\\", "\\\\").replace("\n", "\\n")
|
||||
|
||||
|
||||
def unescape(text):
|
||||
out, i = [], 0
|
||||
while i < len(text):
|
||||
if text[i] == "\\" and i + 1 < len(text):
|
||||
out.append({"n": "\n", "\\": "\\"}.get(text[i + 1], text[i + 1]))
|
||||
i += 2
|
||||
else:
|
||||
out.append(text[i])
|
||||
i += 1
|
||||
return "".join(out)
|
||||
|
||||
|
||||
NBSP = " "
|
||||
|
||||
|
||||
def typography(text):
|
||||
"""French spacing and apostrophes, applied to every translated (not kept) string.
|
||||
l'image -> l'image with a typographic apostrophe (also before a {0} slot); a no-break space before : ; ? ! % and inside the guillemets."""
|
||||
text = re.sub(r"(?<=[A-Za-zÀ-ſ])'(?=[A-Za-zÀ-ſ{])", "’", text)
|
||||
text = re.sub(r"(?<=\S) ([:;?!])", NBSP + r"\1", text)
|
||||
text = re.sub(r"(?<=[\d}]) %", NBSP + "%", text)
|
||||
text = re.sub("« ?", "«" + NBSP, text)
|
||||
text = re.sub(" ?»", NBSP + "»", text)
|
||||
return text
|
||||
|
||||
|
||||
def problems(identifier, source, text):
|
||||
found = []
|
||||
if not text.strip():
|
||||
found.append("empty")
|
||||
expected = [slot for slot in SLOT.findall(source) if not (identifier == "s2463" and slot == "{1}")] # the tests allow this one to drop {1} (the plural suffix)
|
||||
if sorted(expected) != sorted(SLOT.findall(text)):
|
||||
found.append("placeholders differ")
|
||||
if sorted(TAG.findall(source)) != sorted(TAG.findall(text)):
|
||||
found.append("markup differs")
|
||||
return found
|
||||
|
||||
|
||||
def missing():
|
||||
todo = {}
|
||||
for en_name, fr_name in PAIRS:
|
||||
en, fr = load(en_name), load(fr_name)
|
||||
for identifier, text in en.items():
|
||||
if identifier not in fr:
|
||||
todo[identifier] = text
|
||||
return todo
|
||||
|
||||
|
||||
def main(argv):
|
||||
command = argv[1] if len(argv) > 1 else ""
|
||||
if command == "missing":
|
||||
todo = missing()
|
||||
lines = [f"{identifier}\t{escape(text)}" for identifier, text in todo.items()]
|
||||
if len(argv) > 2:
|
||||
Path(argv[2]).write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
|
||||
print(f"{len(lines)} strings without French written to {argv[2]}")
|
||||
else:
|
||||
print("\n".join(lines))
|
||||
print(f"{len(lines)} strings without French", file=sys.stderr)
|
||||
return 0
|
||||
if command == "apply" and len(argv) > 2:
|
||||
incoming = {}
|
||||
for number, line in enumerate(Path(argv[2]).read_text(encoding="utf-8").splitlines(), 1):
|
||||
if not line.strip():
|
||||
continue
|
||||
if "\t" not in line:
|
||||
raise SystemExit(f"{argv[2]}:{number}: no tab")
|
||||
identifier, text = line.split("\t", 1)
|
||||
incoming[identifier.strip()] = text
|
||||
bad = 0
|
||||
for en_name, fr_name in PAIRS:
|
||||
en, fr = load(en_name), load(fr_name)
|
||||
changed = False
|
||||
for identifier, source in en.items():
|
||||
if identifier in incoming:
|
||||
text = source if incoming[identifier] == "=" else typography(unescape(incoming[identifier]))
|
||||
for why in problems(identifier, source, text):
|
||||
print(f"{identifier}: {why}: {source!r} -> {text!r}")
|
||||
bad += 1
|
||||
fr[identifier] = text
|
||||
changed = True
|
||||
if changed:
|
||||
ordered = {identifier: fr[identifier] for identifier in en if identifier in fr}
|
||||
(LOCALES / fr_name).write_text(json.dumps(ordered, ensure_ascii=False, indent=1 if en_name != "en.json" else 2) + "\n", encoding="utf-8")
|
||||
unknown = [identifier for identifier in incoming if not any(identifier in load(en_name) for en_name, _ in PAIRS)]
|
||||
for identifier in unknown:
|
||||
print(f"{identifier}: not in any English catalog")
|
||||
bad += 1
|
||||
print(f"applied {len(incoming)} strings, {bad} problems")
|
||||
return 1 if bad else 0
|
||||
if command == "check":
|
||||
bad = 0
|
||||
for en_name, fr_name in PAIRS:
|
||||
en, fr = load(en_name), load(fr_name)
|
||||
for identifier, source in en.items():
|
||||
if identifier not in fr:
|
||||
print(f"{fr_name}: {identifier} has no French yet: {source!r}")
|
||||
bad += 1
|
||||
continue
|
||||
for why in problems(identifier, source, fr[identifier]):
|
||||
print(f"{fr_name}: {identifier}: {why}")
|
||||
bad += 1
|
||||
for identifier in fr:
|
||||
if identifier not in en:
|
||||
print(f"{fr_name}: {identifier} is not in {en_name}")
|
||||
bad += 1
|
||||
print("French catalogs complete" if not bad else f"{bad} problems")
|
||||
return 1 if bad else 0
|
||||
print(__doc__)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv))
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Screenshots of the app screens in one interface language, from a hermetic preview host (simulated hardware and models,
|
||||
* scratch profile, no engine, no weights). The browser is a throw-away headless Chrome profile.
|
||||
*
|
||||
* node scripts/language-shots.mjs <lang> <outDir> [route ...] (needs `npm run build` first)
|
||||
* example: node scripts/language-shots.mjs fr C:/Users/me/Desktop/fr-screens models settings setup
|
||||
*
|
||||
* Env: CHROME (path to Chrome/Edge), WIDTH (default 1280), HEIGHT (default 800).
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import { chromium } from 'playwright-core';
|
||||
import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
|
||||
const root = resolve(import.meta.dirname, '..');
|
||||
const [, , language = 'fr', outDir = 'language-shots', ...routes] = process.argv;
|
||||
const screens = routes.length ? routes : ['models', 'settings', 'setup'];
|
||||
if (!existsSync(join(root, 'dist/index.html'))) { console.error('Run npm run build first.'); process.exit(1); }
|
||||
const chrome = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', '/usr/bin/google-chrome', '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'].find(existsSync);
|
||||
if (!chrome) { console.error('No Chrome or Edge found; set CHROME.'); process.exit(1); }
|
||||
|
||||
const home = mkdtempSync(join(tmpdir(), 'strata-language-shots-'));
|
||||
const hostPort = 8830 + Math.floor(Math.random() * 40), webPort = hostPort + 100;
|
||||
const scratch = { STRATA_HOME: home, STRATA_MODELS_DIR: join(home, 'models'), STRATA_WORKSPACE: home, STRATA_REAL_ENGINE: '0', STRATA_NATIVE_REPO: join(home, 'no-native-installation'),
|
||||
STRATA_SETTINGS_FILE: join(home, 'app-settings.json'), STRATA_COMPONENTS_DIR: join(home, 'components'), LOCALAPPDATA: join(home, 'Local'), APPDATA: join(home, 'Roaming'),
|
||||
XDG_DATA_HOME: join(home, 'xdg-data'), XDG_STATE_HOME: join(home, 'xdg-state'), XDG_CONFIG_HOME: join(home, 'xdg-config'), STRATA_PORT: String(hostPort) };
|
||||
const children = [];
|
||||
const run = (args, extra = {}) => { const child = spawn(process.execPath, args, { cwd: root, env: { ...process.env, ...scratch, ...extra }, stdio: 'ignore' }); children.push(child); return child; };
|
||||
const stop = () => { for (const child of children) { try { child.kill(); } catch {} } try { rmSync(home, { recursive: true, force: true }); } catch {} };
|
||||
process.on('exit', stop);
|
||||
|
||||
run(['server/index.mjs']);
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try { if ((await fetch(`http://127.0.0.1:${hostPort}/api/health`)).ok) break; } catch {}
|
||||
if (attempt > 200) { console.error('The preview host did not start.'); process.exit(1); }
|
||||
await new Promise(done => setTimeout(done, 150));
|
||||
}
|
||||
run(['tests/fixtures/onboarding-ui-host.mjs'], { STRATA_UI_PREVIEW: '1', STRATA_ONBOARDING_UPSTREAM: `http://127.0.0.1:${hostPort}`, STRATA_ONBOARDING_PORT: String(webPort), STRATA_ONBOARDING_START: 'ready', STRATA_ONBOARDING_SCENARIO: 'telemetry', STRATA_ONBOARDING_PREVIEW: '1' });
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try { if ((await fetch(`http://127.0.0.1:${webPort}/`)).ok) break; } catch {}
|
||||
if (attempt > 200) { console.error('The preview page did not start.'); process.exit(1); }
|
||||
await new Promise(done => setTimeout(done, 150));
|
||||
}
|
||||
|
||||
mkdirSync(resolve(outDir), { recursive: true });
|
||||
const browser = await chromium.launch({ executablePath: chrome, headless: true, args: ['--no-sandbox', '--disable-gpu', '--force-device-scale-factor=1'] });
|
||||
const page = await browser.newPage({ viewport: { width: Number(process.env.WIDTH ?? 1280), height: Number(process.env.HEIGHT ?? 800) }, deviceScaleFactor: 1 });
|
||||
await page.addInitScript(code => { try { localStorage.setItem('strata.prefs.v2', JSON.stringify({ language: code, onboardingSeen: true })); } catch {} }, language);
|
||||
const problems = [];
|
||||
page.on('pageerror', error => problems.push(`pageerror: ${error.message}`));
|
||||
for (const screen of screens) {
|
||||
await page.goto(`http://127.0.0.1:${webPort}/#/${screen}`, { waitUntil: 'load' });
|
||||
await page.reload({ waitUntil: 'load' });
|
||||
await page.waitForTimeout(2500);
|
||||
const file = join(resolve(outDir), `${language}-${screen}.png`);
|
||||
await page.screenshot({ path: file });
|
||||
console.log(`${screen}: ${file} (lang=${await page.evaluate(() => document.documentElement.lang)})`);
|
||||
}
|
||||
await browser.close();
|
||||
if (problems.length) console.log(problems.join('\n'));
|
||||
stop();
|
||||
process.exit(0);
|
||||
@@ -0,0 +1,365 @@
|
||||
"""Writes src/locales/gh-<lang>.json: the interface strings of the 0.2.7 GitHub items done in f-027-gh-app (the Monitor's Power card #3628,
|
||||
approval notifications and the tray #3477, the finished-task sound #3586, the pinned port #3402, the interface size #3970).
|
||||
Run from apps/host: python scripts/make-gh-locales.py
|
||||
The keys are gh01.. in this order; gh-en.json is the English source. Placeholders ({0}, {1}) must be kept in every language.
|
||||
A separate file keeps this work from colliding with the other catalogs (the same reason tune-, tools- and fix- exist)."""
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
EN = [
|
||||
"Energy since start",
|
||||
"Cost since start",
|
||||
"GPU fan",
|
||||
"GPU clock",
|
||||
"CPU temperature",
|
||||
"SSD temperature",
|
||||
"Electricity price",
|
||||
"Price of one kilowatt-hour. The Monitor then shows what your graphics card has cost since Strata started. Leave it empty to hide the cost.",
|
||||
"Price per kWh",
|
||||
"Sound when work finishes",
|
||||
"Plays a short sound when an answer or task that took a while is finished, or when Strata is in the background.",
|
||||
"Play sound",
|
||||
"Notify me when Strata needs an answer",
|
||||
"In Manual mode, shows a desktop notification when the assistant waits for your approval and Strata is in the background.",
|
||||
"Strata needs your answer",
|
||||
"The assistant wants to use {0}. Open Strata to approve or deny.",
|
||||
"a tool",
|
||||
"Minimise to tray",
|
||||
"Closing the window keeps Strata running in the tray, next to the clock. Choose Quit in the tray menu to stop it. Applies the next time Strata starts.",
|
||||
"App port",
|
||||
"The port Strata’s window and local address use. Leave it empty to let Strata choose (47831 when it is free). Applies the next time Strata starts.",
|
||||
"Port (empty = automatic)",
|
||||
"Using port {0} now.",
|
||||
"Strata could not use its port",
|
||||
"Port {0} is used by another program, so Strata started on port {1}. Chats and settings saved on port {0} are not shown until Strata can use it again. Close the other program and restart Strata.",
|
||||
"Choose a port between 1024 and 65535, or leave it empty.",
|
||||
"Makes all text, buttons and menus in Strata larger or smaller.",
|
||||
"Energy and temperature readings",
|
||||
"per kWh, in {0}",
|
||||
"per kWh",
|
||||
"Change currency",
|
||||
"Automatic ({0})",
|
||||
"Currency",
|
||||
"less than {0}",
|
||||
]
|
||||
|
||||
T = {
|
||||
"zh": [
|
||||
"启动以来的能耗",
|
||||
"启动以来的费用",
|
||||
"GPU 风扇",
|
||||
"GPU 频率",
|
||||
"CPU 温度",
|
||||
"SSD 温度",
|
||||
"电价",
|
||||
"每千瓦时的价格。监控页会据此显示 Strata 启动以来显卡用电的费用。留空则不显示费用。",
|
||||
"每千瓦时价格",
|
||||
"工作完成时的提示音",
|
||||
"回答或耗时较长的任务完成时,或 Strata 在后台时,播放一段简短的提示音。",
|
||||
"播放提示音",
|
||||
"Strata 需要您答复时通知我",
|
||||
"在手动模式下,当助手等待您批准而 Strata 在后台时,显示桌面通知。",
|
||||
"Strata 需要您的答复",
|
||||
"助手想使用 {0}。打开 Strata 以批准或拒绝。",
|
||||
"一个工具",
|
||||
"最小化到托盘",
|
||||
"关闭窗口后,Strata 会继续在托盘(时钟旁边)运行。要停止它,请在托盘菜单中选择“退出”。下次启动 Strata 时生效。",
|
||||
"应用端口",
|
||||
"Strata 窗口和本地地址使用的端口。留空则由 Strata 自行选择(空闲时为 47831)。下次启动 Strata 时生效。",
|
||||
"端口(留空 = 自动)",
|
||||
"当前使用端口 {0}。",
|
||||
"Strata 无法使用它的端口",
|
||||
"端口 {0} 已被其他程序占用,因此 Strata 改用端口 {1} 启动。在端口 {0} 上保存的聊天和设置在 Strata 能再次使用该端口之前不会显示。请关闭其他程序并重新启动 Strata。",
|
||||
"请选择 1024 到 65535 之间的端口,或留空。",
|
||||
"让 Strata 中的所有文字、按钮和菜单变大或变小。",
|
||||
"能耗和温度读数",
|
||||
"每千瓦时,币种 {0}",
|
||||
"每千瓦时",
|
||||
"更改货币",
|
||||
"自动({0})",
|
||||
"货币",
|
||||
"不足 {0}",
|
||||
],
|
||||
"ja": [
|
||||
"起動後の消費電力量",
|
||||
"起動後の電気代",
|
||||
"GPU ファン",
|
||||
"GPU クロック",
|
||||
"CPU 温度",
|
||||
"SSD 温度",
|
||||
"電気料金",
|
||||
"1キロワット時あたりの料金です。モニターに、Strata の起動後にグラフィックスカードが使った電気代が表示されます。空欄のままなら電気代は表示されません。",
|
||||
"1 kWh あたりの料金",
|
||||
"作業完了時の音",
|
||||
"回答や時間のかかった作業が終わったとき、または Strata がバックグラウンドにあるときに、短い音を鳴らします。",
|
||||
"音を鳴らす",
|
||||
"Strata の返答が必要なときに通知する",
|
||||
"手動モードで、アシスタントが承認を待っていて Strata がバックグラウンドにあるときに、デスクトップ通知を表示します。",
|
||||
"Strata があなたの返答を待っています",
|
||||
"アシスタントが {0} を使おうとしています。Strata を開いて承認または拒否してください。",
|
||||
"ツール",
|
||||
"トレイに最小化",
|
||||
"ウィンドウを閉じても、Strata は時計の横のトレイで動き続けます。止めるには、トレイのメニューで「終了」を選びます。次に Strata を起動したときから有効になります。",
|
||||
"アプリのポート",
|
||||
"Strata のウィンドウとローカルアドレスが使うポートです。空欄なら Strata が選びます(空いていれば 47831)。次に Strata を起動したときから有効になります。",
|
||||
"ポート(空欄 = 自動)",
|
||||
"現在のポートは {0} です。",
|
||||
"Strata は指定のポートを使えませんでした",
|
||||
"ポート {0} は別のプログラムが使っているため、Strata はポート {1} で起動しました。ポート {0} に保存されたチャットと設定は、Strata が再びそのポートを使えるようになるまで表示されません。他のプログラムを閉じて Strata を再起動してください。",
|
||||
"1024 から 65535 の間のポートを選ぶか、空欄にしてください。",
|
||||
"Strata のすべての文字、ボタン、メニューを大きくしたり小さくしたりします。",
|
||||
"消費電力量と温度の値",
|
||||
"1 kWh あたり、通貨は {0}",
|
||||
"1 kWh あたり",
|
||||
"通貨を変更",
|
||||
"自動({0})",
|
||||
"通貨",
|
||||
"{0} 未満",
|
||||
],
|
||||
"de": [
|
||||
"Energie seit dem Start",
|
||||
"Kosten seit dem Start",
|
||||
"GPU-Lüfter",
|
||||
"GPU-Takt",
|
||||
"CPU-Temperatur",
|
||||
"SSD-Temperatur",
|
||||
"Strompreis",
|
||||
"Preis einer Kilowattstunde. Der Monitor zeigt dann, was Ihre Grafikkarte seit dem Start von Strata gekostet hat. Leer lassen, um die Kosten auszublenden.",
|
||||
"Preis pro kWh",
|
||||
"Ton, wenn Arbeit fertig ist",
|
||||
"Spielt einen kurzen Ton, wenn eine Antwort oder Aufgabe, die länger gedauert hat, fertig ist oder wenn Strata im Hintergrund läuft.",
|
||||
"Ton abspielen",
|
||||
"Benachrichtigen, wenn Strata eine Antwort braucht",
|
||||
"Zeigt im manuellen Modus eine Desktop-Benachrichtigung, wenn der Assistent auf Ihre Freigabe wartet und Strata im Hintergrund läuft.",
|
||||
"Strata braucht Ihre Antwort",
|
||||
"Der Assistent möchte {0} verwenden. Öffnen Sie Strata, um zu erlauben oder abzulehnen.",
|
||||
"ein Werkzeug",
|
||||
"In den Infobereich minimieren",
|
||||
"Beim Schließen des Fensters läuft Strata im Infobereich neben der Uhr weiter. Wählen Sie im Menü des Symbols „Beenden“, um es zu stoppen. Gilt ab dem nächsten Start von Strata.",
|
||||
"App-Port",
|
||||
"Der Port, den das Fenster und die lokale Adresse von Strata verwenden. Leer lassen, damit Strata ihn wählt (47831, wenn frei). Gilt ab dem nächsten Start von Strata.",
|
||||
"Port (leer = automatisch)",
|
||||
"Aktuell wird Port {0} verwendet.",
|
||||
"Strata konnte seinen Port nicht verwenden",
|
||||
"Port {0} wird von einem anderen Programm verwendet, daher wurde Strata auf Port {1} gestartet. Unter Port {0} gespeicherte Chats und Einstellungen werden nicht angezeigt, bis Strata ihn wieder verwenden kann. Schließen Sie das andere Programm und starten Sie Strata neu.",
|
||||
"Wählen Sie einen Port zwischen 1024 und 65535 oder lassen Sie das Feld leer.",
|
||||
"Macht alle Texte, Schaltflächen und Menüs in Strata größer oder kleiner.",
|
||||
"Energie- und Temperaturwerte",
|
||||
"pro kWh, in {0}",
|
||||
"pro kWh",
|
||||
"Währung ändern",
|
||||
"Automatisch ({0})",
|
||||
"Währung",
|
||||
"weniger als {0}",
|
||||
],
|
||||
"es": [
|
||||
"Energía desde el inicio",
|
||||
"Coste desde el inicio",
|
||||
"Ventilador de la GPU",
|
||||
"Reloj de la GPU",
|
||||
"Temperatura de la CPU",
|
||||
"Temperatura del SSD",
|
||||
"Precio de la electricidad",
|
||||
"Precio de un kilovatio hora. El Monitor mostrará entonces lo que ha costado tu tarjeta gráfica desde que se inició Strata. Déjalo vacío para ocultar el coste.",
|
||||
"Precio por kWh",
|
||||
"Sonido al terminar el trabajo",
|
||||
"Reproduce un sonido breve cuando termina una respuesta o tarea que tardó un rato, o cuando Strata está en segundo plano.",
|
||||
"Reproducir sonido",
|
||||
"Avisarme cuando Strata necesite una respuesta",
|
||||
"En el modo manual, muestra una notificación de escritorio cuando el asistente espera tu aprobación y Strata está en segundo plano.",
|
||||
"Strata necesita tu respuesta",
|
||||
"El asistente quiere usar {0}. Abre Strata para aprobar o denegar.",
|
||||
"una herramienta",
|
||||
"Minimizar a la bandeja",
|
||||
"Al cerrar la ventana, Strata sigue funcionando en la bandeja, junto al reloj. Elige Salir en el menú de la bandeja para detenerlo. Se aplica la próxima vez que se inicie Strata.",
|
||||
"Puerto de la aplicación",
|
||||
"El puerto que usan la ventana y la dirección local de Strata. Déjalo vacío para que Strata elija (47831 si está libre). Se aplica la próxima vez que se inicie Strata.",
|
||||
"Puerto (vacío = automático)",
|
||||
"Ahora se usa el puerto {0}.",
|
||||
"Strata no pudo usar su puerto",
|
||||
"El puerto {0} lo usa otro programa, así que Strata se inició en el puerto {1}. Los chats y ajustes guardados en el puerto {0} no se muestran hasta que Strata pueda volver a usarlo. Cierra el otro programa y reinicia Strata.",
|
||||
"Elige un puerto entre 1024 y 65535, o déjalo vacío.",
|
||||
"Hace más grandes o más pequeños todos los textos, botones y menús de Strata.",
|
||||
"Lecturas de energía y temperatura",
|
||||
"por kWh, en {0}",
|
||||
"por kWh",
|
||||
"Cambiar moneda",
|
||||
"Automática ({0})",
|
||||
"Moneda",
|
||||
"menos de {0}",
|
||||
],
|
||||
"ru": [
|
||||
"Энергия с запуска",
|
||||
"Стоимость с запуска",
|
||||
"Вентилятор GPU",
|
||||
"Частота GPU",
|
||||
"Температура CPU",
|
||||
"Температура SSD",
|
||||
"Цена электричества",
|
||||
"Цена одного киловатт-часа. Тогда монитор покажет, во сколько видеокарта обошлась с запуска Strata. Оставьте поле пустым, чтобы скрыть стоимость.",
|
||||
"Цена за кВт·ч",
|
||||
"Звук по окончании работы",
|
||||
"Проигрывает короткий звук, когда закончился долгий ответ или задача, а также когда Strata работает в фоне.",
|
||||
"Проиграть звук",
|
||||
"Уведомлять, когда Strata нужен ответ",
|
||||
"В ручном режиме показывает уведомление на рабочем столе, когда помощник ждёт вашего подтверждения, а Strata работает в фоне.",
|
||||
"Strata нужен ваш ответ",
|
||||
"Помощник хочет использовать {0}. Откройте Strata, чтобы разрешить или отклонить.",
|
||||
"инструмент",
|
||||
"Сворачивать в трей",
|
||||
"При закрытии окна Strata продолжает работать в трее рядом с часами. Чтобы остановить её, выберите «Выход» в меню значка. Действует со следующего запуска Strata.",
|
||||
"Порт приложения",
|
||||
"Порт, который используют окно и локальный адрес Strata. Оставьте поле пустым, чтобы Strata выбрала сама (47831, если свободен). Действует со следующего запуска Strata.",
|
||||
"Порт (пусто = автоматически)",
|
||||
"Сейчас используется порт {0}.",
|
||||
"Strata не смогла использовать свой порт",
|
||||
"Порт {0} занят другой программой, поэтому Strata запустилась на порту {1}. Чаты и настройки, сохранённые на порту {0}, не показываются, пока Strata не сможет снова его использовать. Закройте другую программу и перезапустите Strata.",
|
||||
"Выберите порт от 1024 до 65535 или оставьте поле пустым.",
|
||||
"Увеличивает или уменьшает весь текст, кнопки и меню в Strata.",
|
||||
"Показания энергии и температуры",
|
||||
"за кВт·ч, в валюте {0}",
|
||||
"за кВт·ч",
|
||||
"Сменить валюту",
|
||||
"Автоматически ({0})",
|
||||
"Валюта",
|
||||
"меньше {0}",
|
||||
],
|
||||
"pt": [
|
||||
"Energia desde o início",
|
||||
"Custo desde o início",
|
||||
"Ventoinha da GPU",
|
||||
"Clock da GPU",
|
||||
"Temperatura da CPU",
|
||||
"Temperatura do SSD",
|
||||
"Preço da eletricidade",
|
||||
"Preço de um quilowatt-hora. O Monitor passa a mostrar quanto a sua placa de vídeo custou desde que o Strata iniciou. Deixe vazio para ocultar o custo.",
|
||||
"Preço por kWh",
|
||||
"Som ao terminar o trabalho",
|
||||
"Toca um som curto quando uma resposta ou tarefa que demorou termina, ou quando o Strata está em segundo plano.",
|
||||
"Tocar som",
|
||||
"Avisar quando o Strata precisar de uma resposta",
|
||||
"No modo manual, mostra uma notificação na área de trabalho quando o assistente espera sua aprovação e o Strata está em segundo plano.",
|
||||
"O Strata precisa da sua resposta",
|
||||
"O assistente quer usar {0}. Abra o Strata para aprovar ou negar.",
|
||||
"uma ferramenta",
|
||||
"Minimizar para a bandeja",
|
||||
"Ao fechar a janela, o Strata continua em execução na bandeja, ao lado do relógio. Escolha Sair no menu da bandeja para encerrá-lo. Vale a partir da próxima vez que o Strata iniciar.",
|
||||
"Porta do aplicativo",
|
||||
"A porta usada pela janela e pelo endereço local do Strata. Deixe vazio para o Strata escolher (47831 quando estiver livre). Vale a partir da próxima vez que o Strata iniciar.",
|
||||
"Porta (vazio = automático)",
|
||||
"Usando a porta {0} agora.",
|
||||
"O Strata não conseguiu usar a sua porta",
|
||||
"A porta {0} está em uso por outro programa, então o Strata iniciou na porta {1}. Os chats e as configurações salvos na porta {0} não aparecem até o Strata poder usá-la de novo. Feche o outro programa e reinicie o Strata.",
|
||||
"Escolha uma porta entre 1024 e 65535, ou deixe vazio.",
|
||||
"Deixa todos os textos, botões e menus do Strata maiores ou menores.",
|
||||
"Leituras de energia e temperatura",
|
||||
"por kWh, em {0}",
|
||||
"por kWh",
|
||||
"Alterar moeda",
|
||||
"Automática ({0})",
|
||||
"Moeda",
|
||||
"menos de {0}",
|
||||
],
|
||||
"ko": [
|
||||
"시작 이후 에너지",
|
||||
"시작 이후 비용",
|
||||
"GPU 팬",
|
||||
"GPU 클럭",
|
||||
"CPU 온도",
|
||||
"SSD 온도",
|
||||
"전기 요금",
|
||||
"1킬로와트시의 요금입니다. 모니터에 Strata를 시작한 뒤 그래픽 카드에 든 전기 요금이 표시됩니다. 비워 두면 비용을 표시하지 않습니다.",
|
||||
"kWh당 요금",
|
||||
"작업이 끝났을 때 소리",
|
||||
"답변이나 오래 걸린 작업이 끝났을 때, 또는 Strata가 백그라운드에 있을 때 짧은 소리를 재생합니다.",
|
||||
"소리 재생",
|
||||
"Strata가 답을 필요로 할 때 알림",
|
||||
"수동 모드에서 어시스턴트가 승인을 기다리고 Strata가 백그라운드에 있을 때 데스크톱 알림을 표시합니다.",
|
||||
"Strata가 답을 기다리고 있습니다",
|
||||
"어시스턴트가 {0}을(를) 사용하려고 합니다. Strata를 열어 승인하거나 거부하세요.",
|
||||
"도구",
|
||||
"트레이로 최소화",
|
||||
"창을 닫아도 Strata는 시계 옆 트레이에서 계속 실행됩니다. 끝내려면 트레이 메뉴에서 '종료'를 선택하세요. 다음에 Strata를 시작할 때부터 적용됩니다.",
|
||||
"앱 포트",
|
||||
"Strata의 창과 로컬 주소가 사용하는 포트입니다. 비워 두면 Strata가 고릅니다(비어 있으면 47831). 다음에 Strata를 시작할 때부터 적용됩니다.",
|
||||
"포트(비움 = 자동)",
|
||||
"지금은 포트 {0}을(를) 사용 중입니다.",
|
||||
"Strata가 지정한 포트를 사용할 수 없었습니다",
|
||||
"포트 {0}을(를) 다른 프로그램이 사용 중이어서 Strata는 포트 {1}에서 시작했습니다. 포트 {0}에 저장된 채팅과 설정은 Strata가 다시 그 포트를 쓸 수 있을 때까지 표시되지 않습니다. 다른 프로그램을 닫고 Strata를 다시 시작하세요.",
|
||||
"1024에서 65535 사이의 포트를 고르거나 비워 두세요.",
|
||||
"Strata의 모든 글자, 버튼, 메뉴를 더 크게 또는 더 작게 만듭니다.",
|
||||
"에너지 및 온도 측정값",
|
||||
"kWh당, 통화 {0}",
|
||||
"kWh당",
|
||||
"통화 변경",
|
||||
"자동({0})",
|
||||
"통화",
|
||||
"{0} 미만",
|
||||
],
|
||||
"fr": [
|
||||
"Énergie depuis le démarrage",
|
||||
"Coût depuis le démarrage",
|
||||
"Ventilateur du GPU",
|
||||
"Fréquence du GPU",
|
||||
"Température du CPU",
|
||||
"Température du SSD",
|
||||
"Prix de l'électricité",
|
||||
"Prix d'un kilowattheure. Le Moniteur affiche alors ce que votre carte graphique a coûté depuis le démarrage de Strata. Laissez vide pour masquer le coût.",
|
||||
"Prix par kWh",
|
||||
"Son à la fin du travail",
|
||||
"Joue un court son quand une réponse ou une tâche qui a pris du temps est terminée, ou quand Strata est en arrière-plan.",
|
||||
"Jouer le son",
|
||||
"Me prévenir quand Strata a besoin d'une réponse",
|
||||
"En mode manuel, affiche une notification sur le bureau quand l'assistant attend votre accord et que Strata est en arrière-plan.",
|
||||
"Strata a besoin de votre réponse",
|
||||
"L'assistant veut utiliser {0}. Ouvrez Strata pour accepter ou refuser.",
|
||||
"un outil",
|
||||
"Réduire dans la zone de notification",
|
||||
"Fermer la fenêtre garde Strata actif dans la zone de notification, près de l'horloge. Choisissez Quitter dans le menu de l'icône pour l'arrêter. S'applique au prochain démarrage de Strata.",
|
||||
"Port de l'application",
|
||||
"Le port que la fenêtre et l'adresse locale de Strata utilisent. Laissez vide pour que Strata choisisse (47831 s'il est libre). S'applique au prochain démarrage de Strata.",
|
||||
"Port (vide = automatique)",
|
||||
"Port {0} utilisé en ce moment.",
|
||||
"Strata n'a pas pu utiliser son port",
|
||||
"Le port {0} est utilisé par un autre programme, donc Strata a démarré sur le port {1}. Les conversations et réglages enregistrés sur le port {0} ne s'affichent pas tant que Strata ne peut pas le réutiliser. Fermez l'autre programme et relancez Strata.",
|
||||
"Choisissez un port entre 1024 et 65535, ou laissez vide.",
|
||||
"Agrandit ou réduit tous les textes, boutons et menus de Strata.",
|
||||
"Relevés d'énergie et de température",
|
||||
"par kWh, en {0}",
|
||||
"par kWh",
|
||||
"Changer de devise",
|
||||
"Automatique ({0})",
|
||||
"Devise",
|
||||
"moins de {0}",
|
||||
],
|
||||
}
|
||||
|
||||
here = Path(__file__).resolve().parent.parent / "src" / "locales"
|
||||
|
||||
|
||||
NBSP = "\u00a0"
|
||||
|
||||
|
||||
def french(text):
|
||||
"""The same French spacing as scripts/fr_sync.py: a typographic apostrophe, a no-break space before : ; ? ! % and inside the guillemets."""
|
||||
text = re.sub(r"(?<=[A-Za-z\u00c0-\u017f])'(?=[A-Za-z\u00c0-\u017f{])", "\u2019", text)
|
||||
text = re.sub(r"(?<=\S) ([:;?!])", NBSP + r"\1", text)
|
||||
text = re.sub(r"(?<=[\d}]) %", NBSP + "%", text)
|
||||
return text
|
||||
|
||||
|
||||
def write(lang, values):
|
||||
assert len(values) == len(EN), (lang, len(values), len(EN))
|
||||
for source, text in zip(EN, values):
|
||||
assert sorted(re.findall(r"\{\d+\}", source)) == sorted(re.findall(r"\{\d+\}", text)), (lang, source, text)
|
||||
if lang == "fr":
|
||||
values = [french(v) for v in values]
|
||||
data = {f"gh{i + 1:02d}": text for i, text in enumerate(values)}
|
||||
(here / f"gh-{lang}.json").write_text(json.dumps(data, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
write("en", EN)
|
||||
for lang, values in T.items():
|
||||
write(lang, values)
|
||||
print("written", len(EN), "strings in", len(T) + 1, "languages")
|
||||
@@ -1,5 +1,8 @@
|
||||
# Agent bridge (Strata chats as DeepSeek Harness sessions)
|
||||
|
||||
This page is for developers who work on the app host's agent code. It lists the files, the frames and routes, and the rules the
|
||||
bridge enforces. Tests are listed at the end.
|
||||
|
||||
The Harness is a separate, pinned component (`DSH 0.2.1-alpha.3`, commit `57190202`, MIT). The host finds it through
|
||||
`STRATA_HARNESS_DIR` (or `<STRATA_COMPONENTS_DIR>/harness`), starts one `dsh --profile web` process on a loopback port,
|
||||
and talks to its Gateway. Nothing here downloads or builds anything.
|
||||
@@ -14,12 +17,13 @@ and talks to its Gateway. Nothing here downloads or builds anything.
|
||||
| `children.mjs` | pure projection of one helper agent's events into its row and its approval card |
|
||||
| `preset.mjs` | the agent preset (which tools the chat has) |
|
||||
| `plugins/strata-gate.mjs` | the Manual-mode approval gate (a Harness plugin, loaded by path) |
|
||||
| `plugins/strata-search.mjs`, `plugins/strata-models.mjs` | the app's web search registered with the Harness; helper agents on a named model |
|
||||
| `plugins/strata-context.mjs`, `context-budget.mjs`, `context-fit.mjs` | measured context pressure, summaries that fit the window, the check before a message goes out |
|
||||
| `agent-bridge.mjs` | the chat-to-session map, turns, approvals, Stop, status |
|
||||
|
||||
## Frames sent on `/api/chat/stream` for an agent turn
|
||||
|
||||
`queued`, `start` (`agent:true`), `reasoning`, `delta`, `agent-tool` (new: upserts a tool card by call id with
|
||||
`queued`, `start` (`agent:true`), `reasoning`, `delta`, `agent-tool` (upserts a tool card by call id with
|
||||
`running | proposed | done | denied | error`), `done` (usage, `finishReason`, `cancelled`), `error`, and `notice`
|
||||
(why a chat is not using the agent). The chat UI renders them with its existing components.
|
||||
|
||||
@@ -57,7 +61,7 @@ The chat's Mode menu is real Harness behaviour. Each turn the page sends the cha
|
||||
| Bypass | `strata-bypass` | `danger-full-access` | `never` | Full access without prompts, as before. |
|
||||
|
||||
- Manual relies on the gate, so a Manual turn does not start unless the bridge has confirmed the gate is loaded (`/strata-gate` command): without it Manual would be an open sandbox that nobody asks about.
|
||||
- The gate listens to `tools/pre-execute` (the seam the Harness's own Auto-review layer uses) and returns an `ask` decision; the Harness's approval service then asks, runs the tool only after `allowed-once`, and records the answer. Read-only tools and a short allowlist of look-only shell commands (Get-ChildItem, git status, ...) run unasked; MCP and unknown tools ask.
|
||||
- The gate listens to `tools/pre-execute` (the seam the Harness's own Auto-review layer uses) and returns an `ask` decision; the Harness's approval service then asks, runs the tool only after `allowed-once`, and records the answer. Read-only tools and a short allowlist of look-only shell commands (Get-ChildItem, git status, ...) run unasked, each only with the options listed for it in `strata-gate.mjs` (an unknown option, or one that writes, runs a program, sets something or never ends, asks); MCP and unknown tools ask.
|
||||
- Web search and web pages (`web_search`, `web_fetch`) ask once per chat in Manual, not once per call. The gate asks through the approval service itself (`webDecision`: "The assistant wants to search the web for: ... Search words go to <service>." / "... open the web page ..."), remembers a yes for the chat (a helper agent shares its chat's answer, and two searches started together share one card), and on a no gives the model `WEB_REFUSAL` ("The person did not allow web access ... Go on without it and say so."). Plan and Bypass are unchanged. Settings -> Tools has a switch, "Let the assistant search the web without asking" (off by default, `webWithoutAsking` in `tools-settings.mjs`, passed to the gate as `config.web.skipAsk`), that skips the question in Manual. A scheduled task has nobody at the card, so its web call is answered no unless that switch is on. Tests: `tests/web-ask.test.mjs`.
|
||||
- The mode is read once per turn; an unknown or missing value is Manual.
|
||||
|
||||
@@ -70,7 +74,7 @@ The chat's Mode menu is real Harness behaviour. Each turn the page sends the cha
|
||||
What the person chose is saved by `tools-settings.mjs` (inside the host's settings file) and shown by `src/components/SettingsTools.tsx` (`GET/PUT /api/agent/tools`, `POST/PATCH/DELETE /api/agent/tools/custom[/id]`). A change reaches the agent from the next message: the profile carries a `toolsSignature`, and the bridge restarts the Harness (after the running turns end) when it differs.
|
||||
|
||||
- Tool groups (`tools-catalog.mjs`): each is one switch in plain words and is one or more preset plugins (`withoutDisabled`); `fetch` and `search` are the two halves of the Harness's `tool-web` (both off drops it). Goals off: `/goal` answers that goals are turned off.
|
||||
- Web search needs no account. The default is DuckDuckGo's HTML result page (`../web-search-ddg.mjs`), with Bing's plain result page (`../web-search-bing.mjs`) as the second source. Manners, because DuckDuckGo refuses a program that asks fast (a real box: two searches in 1.2 s answered, the third got HTTP 202 and every DuckDuckGo page refused for many minutes): a browser User-Agent and the page's own request shape (Referer, Origin, Sec-Fetch-*), one request at a time per source, 2.5 to 4 s apart (random), at most 8 a minute, a 30 minute session cache per question, a cooldown after a refusal that doubles (1, 2, 4 ... 10 minutes, `Retry-After` honoured) during which that source gets no request, no lite-page follow-up after a refusal, one retry for a timeout or a 5xx, a 15 s limit. When every source refuses, the tool answers one plain sentence (`LIMITED_SENTENCE`: "Web search is limited right now. Try again in a minute, or add a search key (Exa, Perplexity or DeepSeek) in Settings -> Tools -> Web search.") plus an instruction to the model not to retry; the turn guard in `agent-bridge.mjs` (see Turn guard) stops a model that retries anyway. **The results come from DuckDuckGo or Bing**; they are not ranked or filtered here. The person may switch to Exa, Perplexity or DeepSeek with their own key (`../web-search-keyed.mjs`; DeepSeek uses the Harness's own provider): the key is saved in the settings file, reaches the agent only as the environment variable `STRATA_SEARCH_KEY`, never the profile file and never the screen (it only learns `keySet`). A keyed service without its key falls back to DuckDuckGo. The profile pins `web.searchProvider` to the chosen id so two usable backends cannot make the choice ambiguous.
|
||||
- Web search needs no account. The default is DuckDuckGo's HTML result page (`../web-search-ddg.mjs`), with Bing's plain result page (`../web-search-bing.mjs`) as the second source. Manners, because DuckDuckGo refuses a program that asks fast: a browser User-Agent and the page's own request shape (Referer, Origin, Sec-Fetch-*), one request at a time per source, 2.5 to 4 s apart (random), at most 8 a minute, a 30 minute session cache per question, a cooldown after a refusal that doubles (1, 2, 4 ... 10 minutes, `Retry-After` honoured) during which that source gets no request, no lite-page follow-up after a refusal, one retry for a timeout or a 5xx, a 15 s limit. When every source refuses, the tool answers one plain sentence (`LIMITED_SENTENCE`: "Web search is limited right now. Try again in a minute, or add a search key (Exa, Perplexity or DeepSeek) in Settings -> Tools -> Web search.") plus an instruction to the model not to retry; the turn guard in `agent-bridge.mjs` (see Turn guard) stops a model that retries anyway. **The results come from DuckDuckGo or Bing**; they are not ranked or filtered here. The person may switch to Exa, Perplexity or DeepSeek with their own key (`../web-search-keyed.mjs`; DeepSeek uses the Harness's own provider): the key is saved in the settings file, reaches the agent only as the environment variable `STRATA_SEARCH_KEY`, never the profile file and never the screen (it only learns `keySet`). A keyed service without its key falls back to DuckDuckGo. The profile pins `web.searchProvider` to the chosen id so two usable backends cannot make the choice ambiguous.
|
||||
- The browser: Playwright's MCP server (`@playwright/mcp`, staged next to the Harness by `apps/desktop/scripts/stage-harness.mjs`) on the person's own Chrome, Edge or Chromium (`browser-probe.mjs`), `--isolated` so it never sees their profile. Tools appear as `mcp__browser__browser_*`. In Manual, opening a page, clicking, typing, uploading, running a script in the page and closing tabs ask (plain reasons: "Open the web page ...", "Click on the page: ..."); looking (snapshot, screenshot, console, network, waiting, listing tabs) does not. Plan refuses every connected tool. The screen says plainly when no browser, or no tool server, is found.
|
||||
- The person's own tools: `instructions` (the tool returns their text; look-only, never asks), `command` (runs their command with the assistant's text in the `STRATA_INPUT` environment variable, never spliced into the command line; asks in Manual, refused in Plan) and `server` (a web address or a program, added as `mcp__custom-<name>__*`; asks in Manual). Instruction and command tools are served by `custom-tools-mcp.mjs` (stdio MCP) from a JSON file the settings module rewrites on every change, so a switch in Settings is seen without a restart.
|
||||
- `strata-gate.mjs` implements the Manual/Plan rules above (`planDecision`, `browserDecision`, the look-only instruction server).
|
||||
@@ -81,7 +85,7 @@ What the person chose is saved by `tools-settings.mjs` (inside the host's settin
|
||||
- A helper's request for approval is the same card (state `proposed`, id `<helper session>:<call>`), answered with the same `/api/chat/approval`; a chat can only answer its own helpers.
|
||||
- The run (the chat's one NDJSON response) stays open until the parent is idle, every helper has finished and reported back, and no armed goal is waiting; `done` is sent then.
|
||||
- Stop: pauses an active goal, interrupts every helper (`subagents/interruptByParent`; `session/cancel` on a helper session is refused by the Harness), cancels the parent, waits; a report that would wake the parent afterwards is cancelled too.
|
||||
- The number of helpers at once equals the engine's request slots (`maxActiveSubagents` in the profile); the Harness refuses more with a message the model reads.
|
||||
- The number of helpers at once is the engine's request slots, with a floor of 4 for a local model and 8 for an API model (`maxActiveSubagents` in the profile). The Harness refuses more with a message the model reads.
|
||||
|
||||
## /goal
|
||||
|
||||
@@ -89,8 +93,15 @@ What the person chose is saved by `tools-settings.mjs` (inside the host's settin
|
||||
|
||||
## Thinking and the output budget
|
||||
|
||||
The Harness staging script (`apps/desktop/scripts/stage-harness.mjs`) applies Strata's guarded pi-ai 1.1.0 patch to its staged copy
|
||||
(`harness-budget.mjs`, #3684): below 64K the context safety room is at most `min(4096, floor(contextWindow / 16))` and never more
|
||||
than half the estimated remaining room, so an 8K window keeps output room after a tool result instead of ending with a one-token
|
||||
cap. The caller's output cap and the one-token floor remain; windows of 64K or more keep the previous calculation. `BUNDLE.json.patches`
|
||||
records the patch identity, the dependency version and the before/after SHA-256. Unknown dependency bytes stop staging for review.
|
||||
Source and already-installed Harness copies are not changed.
|
||||
|
||||
- A local route is reasoning-capable (`reasoning: true`) with `compat.thinkingFormat: 'chat-template'`: the request carries `chat_template_kwargs {enable_thinking, reasoning_effort}`, which the Strata server reads (a top-level `enable_thinking` is ignored by it, which is why pi-ai's `qwen` format left the model thinking). The chat's Reasoning menu travels as `thinking` in the stream body; the bridge sends it with `session/selectModel` (`reasoningEffort`) whenever the model or the level changed; a level a route does not have is dropped (never an error). API routes keep the provider default (the menu is disabled for them in the UI).
|
||||
- `localOutputBudget(context)` = context minus 4096 (it was context/4 capped at 8192, which cut big answers off at exactly 8192 tokens); pi-ai sends the window minus the prompt. App-loaded models get `fit_max_tokens: true` so the engine clamps instead of answering 400. Sampling is not set here: the Harness sends none, so the server's own defaults apply.
|
||||
- `localOutputBudget(context)` is 30 percent of the window, at least 4096 and at most 32768 (9,830 tokens at 32K, 19,660 at 64K). The Harness's automatic compaction reads it as the room a reply reserves. pi-ai sends the window minus the prompt. App-loaded models get `fit_max_tokens: true` so the engine clamps instead of answering 400. Sampling is not set here: the Harness sends none, so the server's own defaults apply.
|
||||
|
||||
## Tasks
|
||||
|
||||
@@ -108,7 +119,7 @@ Failures the person sees are plain sentences with a next step (`AgentUnavailable
|
||||
|
||||
## Helper agents on a named model (`plugins/strata-models.mjs`, `model-routes.mjs`)
|
||||
|
||||
The Harness's `subagent` tool takes an exact `provider` route key and model id, and the app had its model choice switched off, so every helper ran on the chat's model. The profile now enables the Harness's `subagent-model-selection-settings` with every route the app can reach (local instances and saved API models), and the `strata-models` plugin wraps the two tool definitions the Harness registers for each chat (`subagent`, `list_subagent_models`) in place:
|
||||
The Harness's `subagent` tool takes an exact `provider` route key and model id. The profile enables the Harness's `subagent-model-selection-settings` with every route the app can reach (local instances and saved API models), and the `strata-models` plugin wraps the two tool definitions the Harness registers for each chat (`subagent`, `list_subagent_models`) in place:
|
||||
|
||||
- the model is found from its id, `provider/id`, the name in the model picker, `connection name`, or "local" (any letters). A close guess is not started: the tool answers "Did you mean ...?" and nothing runs until the call is made again with that model. Several fits are listed; nothing fitting lists the models that can be used and says they can be added in Models.
|
||||
- the tool's own text says when to use it ("ask a subagent", "ask model X"), not to look for the model with shell commands, and not to wait with sleep. The system prompt (`preset.mjs`) says the same, so it reaches every model, local or hosted.
|
||||
@@ -126,7 +137,7 @@ Pictures are on by default for every model that can read them: an API model read
|
||||
|
||||
## Turn guard (`turn-guard.mjs`)
|
||||
|
||||
A turn that keeps failing the same way is stopped by the app, never left to the model. Seen on a search-heavy `/plan`: every web search was refused, the model searched again and rewrote its plan after each one until the context reached 100%, then the turn ended with no answer and no message.
|
||||
A turn that keeps failing the same way is stopped by the app, never left to the model.
|
||||
|
||||
- **Counted, from what the turn shows:** web searches that failed in a row (3), searches in one turn (10 in a plan turn, 25 otherwise), a plan turn that starts its numbered list from `1.` again (4 times), and a context of 93% or more (the Harness's own compaction did not keep up).
|
||||
- **Stopped the way Stop stops** (`session/cancel`, helpers first) but ended as a normal turn (`done`, not cancelled, no error card), so the plan the model had written is still proposed. The plan card carries the reason (`plan.caution`); any other turn ends with one `delta` frame that has `own: true` and `key` (the English sentence, translated by the page; `GUARD_NOTES` are in the interface catalog in all eight languages). Each sentence says what happened and gives one next step.
|
||||
@@ -134,9 +145,7 @@ A turn that keeps failing the same way is stopped by the app, never left to the
|
||||
- **Never silent:** a turn that ends (`completed` or `max-tokens`) with no answer text gets "I ran out of room in this chat..." when the last request used 85% of the window or more, otherwise "I stopped without writing an answer..." (not when tool cards were shown, not after Stop, not after an error, which says why by itself). Goal runs have their own ending lines and are not guarded.
|
||||
- Tests: `tests/agent-turn-guard.test.mjs` (counting, always; end to end with the real Harness, the scripted model's `searchloop` and `bloat` scripts and `searchMock: 'limited'` on the bridge, which runs the real search code on a pretend web that refuses, no network).
|
||||
|
||||
## Context: measured pressure, summaries that fit, the check before a message goes out (2026-10-22)
|
||||
|
||||
Found by the owner's hands-on test of the installed 0.2.1 (Gyro-S, 64K): a 118 KB log paste was 66,500 tokens to the engine and about 29,500 to the Harness (4 characters a token; logs and code are about 2); the engine refused it with a 400, the refusal stayed in the history, and then neither the next message nor `/compact` could go through, because the summary request carried the same history and was refused for its size too. `/compact` itself was never a command of the app: it went to the model as a question and the model answered "nothing to compact".
|
||||
## Context: measured pressure, summaries that fit, the check before a message goes out
|
||||
|
||||
- **`plugins/strata-context.mjs`** (inserted by the profile, like the gate) wraps `tokenMeter.measure` and `llm.stream` in place, once each. Pressure = the last prompt size the model server reported for an answer, plus what changed since priced at the density that measurement showed (`context-budget.mjs`: `pressureNow`, `densityOf`); never lower than the Harness's own price. Node prices are scaled the same way, so the retained tail is in real tokens. A summary request (`purpose: 'compaction'`) that would not fit the window is asked for in pieces: a message bigger than a piece is cut for the request only, each piece builds on the summary before it, a piece the server refuses is halved and tried again. `/strata-context` (a Harness command, like `/strata-gate`) answers `{tokens, window}` for a session so the host can tell the person true figures.
|
||||
- **`context-fit.mjs`**: before a message is sent the model server counts it (`POST /v1/messages/count_tokens`, its own tokenizer). `decideFit`: send; summarise first and then send (the message fits an otherwise empty chat); or never fits. The last one is not sent: the error frame has `code: 'CONTEXT_MESSAGE_TOO_LARGE'` and the page offers "Attach as a file" (`oversize.mjs`, `/api/chat/oversize`: the text is saved in the chat's folder, the assistant reads it in parts) or "Send a shortened preview". An engine that cannot count lets the message through; the Harness's own compact-and-retry is the net.
|
||||
@@ -144,3 +153,15 @@ Found by the owner's hands-on test of the installed 0.2.1 (Gyro-S, 64K): a 118 K
|
||||
- **Visible:** `compaction/start` shows "Summarizing earlier messages..." in the status line; `compaction/end` adds one plain line to the reply. The projector also sends a `context` frame after every model request, so the meter follows the running turn.
|
||||
- **A refusal that is left** (`CONTEXT_WINDOW_EXCEEDED`) is a plain sentence with the engine's text under Details and the buttons "Summarize and continue" and "Start a new chat". "Try sending the message again" is for other errors only.
|
||||
- Tests: `tests/agent-context.test.mjs` (arithmetic; the real Harness against the scripted worker with `window`, `charsPerToken` and `noCount`), `tests/oversize.test.mjs`, `tests/context-ui.test.mjs`. The run against a real engine is `scripts/context-e2e.mjs`.
|
||||
|
||||
## History / older measurements
|
||||
|
||||
- **Search refusals (2026-10):** on a real box two DuckDuckGo searches in 1.2 s answered, the third got HTTP 202, and every DuckDuckGo page refused for many minutes.
|
||||
The pacing, cooldown and cache rules above come from that test.
|
||||
- **Search-heavy `/plan` (2026-10):** every web search was refused, the model searched again and rewrote its plan after each one until the context reached 100%.
|
||||
The turn ended with no answer and no message. The turn guard comes from that run.
|
||||
- **Large log paste (2026-10-22, installed 0.2.1, Gyro-S, 64K):** a 118 KB log was 66,500 tokens to the engine and about 29,500 to the Harness
|
||||
(4 characters a token; logs and code are about 2). The engine refused it with a 400. The refusal stayed in the history, so neither the next message nor `/compact` could
|
||||
go through, because the summary request carried the same history. `/compact` was also not a command of the app yet: it went to the model as a question. This
|
||||
produced `strata-context.mjs`, `context-fit.mjs` and the host's own `/compact`.
|
||||
- **Output budget:** an earlier local budget of a quarter of the window, capped at 8192, cut big answers off at exactly 8192 tokens.
|
||||
@@ -58,7 +58,7 @@ export async function countTokens({ baseUrl, key = '', model = '', text, fetchIm
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
/** The plain sentences (also in the interface catalog in all eight languages). `{0}` slots are filled by the page. */
|
||||
/** The plain sentences (also in the interface catalog in all nine languages). `{0}` slots are filled by the page. */
|
||||
export const FIT_TEXT = Object.freeze({
|
||||
tooLarge: 'This message is about {0} tokens long, and this model can read about {1} at a time. It is too large to send as it is.',
|
||||
tooLargeNext: 'Attach it as a file for the assistant to read in parts, or send a shortened preview.',
|
||||
|
||||
@@ -48,25 +48,196 @@ export const READ_ONLY_TOOLS = new Set([
|
||||
|
||||
const SHELL_TOOLS = new Set(['pwsh', 'bash']);
|
||||
|
||||
/** Commands that only look. Matched against the first word of every pipeline segment. */
|
||||
const SAFE_COMMANDS = new Set([
|
||||
// PowerShell
|
||||
'get-childitem', 'gci', 'ls', 'dir', 'get-content', 'gc', 'cat', 'type', 'get-item', 'gi', 'get-location', 'pwd', 'gl',
|
||||
'select-string', 'sls', 'measure-object', 'measure', 'select-object', 'select', 'where-object', 'where', 'sort-object', 'sort',
|
||||
'format-table', 'ft', 'format-list', 'fl', 'out-string', 'test-path', 'resolve-path', 'get-filehash', 'get-date', 'get-command',
|
||||
'get-process', 'get-itemproperty', 'write-output', 'echo', 'hostname', 'whoami', 'get-host', 'get-psdrive', 'get-help',
|
||||
'group-object', 'group', 'convertto-json', 'convertfrom-json', 'select-xml', 'compare-object', 'get-unique', 'tee-object-none',
|
||||
// POSIX
|
||||
'head', 'tail', 'wc', 'grep', 'rg', 'egrep', 'fgrep', 'stat', 'file', 'du', 'df', 'uname', 'which', 'basename', 'dirname',
|
||||
'realpath', 'readlink', 'sort', 'uniq', 'cut', 'tr', 'less', 'more', 'date', 'id', 'env-none', 'tree',
|
||||
]);
|
||||
/**
|
||||
* Which shell commands run without a card in Manual mode, and with which options. Safe by construction (#3975): a command is on the list WITH the
|
||||
* options that are known to only look. Every other option, long or short, abbreviated or attached, makes the command ask. So a write mode
|
||||
* (`sort -o`, `tree -o`, `uniq in out`), a program-execution mode (`sort --compress-program`, `rg --pre`, `git grep -O`), a setting mode
|
||||
* (`date -s`) or one that never ends (`tail -f`) cannot slip in, and a word added to the list later starts with no options at all.
|
||||
*
|
||||
* A spec says what one command accepts:
|
||||
* f short options without a value (letters, so `-la` is `-l -a`) v short options that need a value (`-k2`, `-k 2`)
|
||||
* vn the same, and the value must be a number o short options with an optional value that must be attached (`-Iseconds`)
|
||||
* n a bare `-5` is allowed l long options (`--name`, `--name=value`), spelled in full
|
||||
* lv long options that need a value (`--name=value`, `--name value`) ps PowerShell parameter names, lower case, spelled in full
|
||||
* max how many operands (files, patterns) are allowed op a check for each operand
|
||||
* post a last check from the options seen and the operand count
|
||||
*/
|
||||
const words = (text) => (text ? text.trim().split(/\s+/) : []);
|
||||
const spec = ({ f = '', v = '', vn = '', o = '', n = false, l = '', lv = '', ps = '', max = Infinity, op = null, post = null }) =>
|
||||
({ f, v, vn, o, n, l: new Set(words(l)), lv: new Set(words(lv)), ps: ps ? new Set([...words(ps), ...PS_COMMON]) : null, max, op, post });
|
||||
/** Parameters every cmdlet has that only change how errors and warnings show. (-Debug asks a question and -OutVariable sets one: not listed.) */
|
||||
const PS_COMMON = ['erroraction', 'ea', 'warningaction', 'wa', 'informationaction', 'ia', 'verbose', 'vb'];
|
||||
const NUMBER = /^[+-]?\d+[A-Za-z]{0,3}$/;
|
||||
|
||||
/** What `ls`, `dir`, `gci` and `Get-ChildItem` take. */
|
||||
const LS = { f: 'aAbBcdDFgGhHiklLmnNopqQrRsStuUvxX1CZ', v: 'ITw', ps: 'path literalpath filter include exclude recurse depth name force file directory hidden readonly system attributes',
|
||||
l: 'all almost-all human-readable recursive directory classify reverse size inode dereference color group-directories-first sort time time-style block-size indicator-style si numeric-uid-gid no-group literal hide ignore quote-name escape author file-type full-time width tabsize' };
|
||||
/** What `cat`, `type`, `gc` and `Get-Content` take. (-Wait never ends, so it is not here.) */
|
||||
const CAT = { f: 'AbeEnstTuv', ps: 'path literalpath totalcount tail head raw encoding readcount force filter include exclude delimiter stream asbytestream',
|
||||
l: 'show-all number-nonblank show-ends number squeeze-blank show-tabs show-nonprinting' };
|
||||
const MEASURE = { ps: 'property sum average maximum minimum line word character ignorewhitespace inputobject standarddeviation allstats' };
|
||||
const SELECT = { ps: 'first last skip skiplast property expandproperty unique index excludeproperty inputobject' };
|
||||
const WHERE = { ps: 'property value inputobject eq ne gt lt ge le like notlike match notmatch contains notcontains in notin is isnot ceq cne cgt clt cge cle clike cnotlike cmatch cnotmatch ccontains cnotcontains cin cnotin' };
|
||||
const GROUP = { ps: 'property noelement ashashtable asstring casesensitive inputobject culture' };
|
||||
const FT = { ps: 'property autosize wrap hidetableheaders groupby view force expand repeatheader displayerror showerror inputobject' };
|
||||
const FL = { ps: 'property view groupby expand force displayerror showerror inputobject' };
|
||||
const SLS = { ps: 'pattern path literalpath simplematch casesensitive context list notmatch quiet allmatches include exclude encoding raw inputobject culture noemphasis' };
|
||||
const GETITEM = { ps: 'path literalpath name filter include exclude force stream' };
|
||||
const GETLOC = { ps: 'psprovider psdrive stack stackname' };
|
||||
const ECHO = { f: 'neE', ps: 'inputobject noenumerate' };
|
||||
/** The GNU/BSD grep family. */
|
||||
const GREP = { f: 'aiIlLcnvwHhEFGPqrRsTUyzZbxo', v: 'efdD', vn: 'ABCm', n: true,
|
||||
l: 'extended-regexp fixed-strings basic-regexp perl-regexp ignore-case no-ignore-case word-regexp line-regexp invert-match count color colour files-with-matches files-without-match max-count only-matching quiet silent no-messages byte-offset with-filename no-filename label line-buffered initial-tab null after-context before-context context text recursive dereference-recursive include exclude exclude-dir binary-files line-number null-data no-group-separator group-separator regexp file directories devices binary' };
|
||||
|
||||
/** Commands that only look, each with the options that are known to be harmless. Matched against the first word of every pipeline segment. */
|
||||
const SPECS = new Map();
|
||||
const add = (names, s) => { for (const name of words(names)) SPECS.set(name, spec(s)); };
|
||||
// PowerShell
|
||||
add('get-childitem gci', { ps: LS.ps });
|
||||
add('ls dir', LS);
|
||||
add('get-content gc', { ps: CAT.ps });
|
||||
add('cat type', CAT);
|
||||
add('get-item gi get-itemproperty', GETITEM);
|
||||
add('get-location gl', GETLOC);
|
||||
add('pwd', { ...GETLOC, f: 'LP' });
|
||||
add('select-string sls', SLS);
|
||||
add('measure-object measure', MEASURE);
|
||||
add('select-object select', SELECT);
|
||||
add('where-object where', WHERE);
|
||||
add('sort-object', { ps: 'property descending unique top bottom casesensitive culture stable inputobject' });
|
||||
add('format-table ft', FT);
|
||||
add('format-list fl', FL);
|
||||
add('out-string', { ps: 'stream width inputobject' });
|
||||
add('test-path', { ps: 'path literalpath pathtype isvalid filter include exclude olderthan newerthan' });
|
||||
add('resolve-path', { ps: 'path literalpath relative relativebasepath force' });
|
||||
add('get-filehash', { ps: 'path literalpath algorithm' });
|
||||
add('get-date', { ps: 'format uformat date asutc displayhint year month day hour minute second millisecond' });
|
||||
add('get-command', { ps: 'name commandtype module all syntax listimported totalcount noun verb fullyqualifiedmodule parametername parametertype' });
|
||||
add('get-process', { ps: 'name id includeusername module fileversioninfo inputobject' });
|
||||
add('write-output', { ps: ECHO.ps });
|
||||
add('echo', ECHO);
|
||||
add('get-host', { ps: '' });
|
||||
add('get-psdrive', { ps: 'name psprovider scope literalname' });
|
||||
add('get-help', { ps: 'name full detailed examples parameter category component functionality role' });
|
||||
add('group-object group', GROUP);
|
||||
add('convertto-json', { ps: 'depth compress enumsasstrings inputobject asarray escapehandling' });
|
||||
add('convertfrom-json', { ps: 'ashashtable depth inputobject noenumerate' });
|
||||
add('select-xml', { ps: 'xml path literalpath xpath content namespace' });
|
||||
add('compare-object', { ps: 'referenceobject differenceobject property includeequal excludedifferent passthru syncwindow casesensitive culture' });
|
||||
add('get-unique', { ps: 'inputobject asstring caseinsensitive onetype' });
|
||||
// POSIX. `sort` and `date` take no PowerShell names here: `sort -o FILE` writes, `date -s` sets the clock, and a PowerShell-looking `-Second` or `-Top`
|
||||
// must not be read by a real sort or date as one of those. (Sort-Object and Get-Date above are the PowerShell forms.)
|
||||
add('head', { f: 'qvz', vn: 'nc', n: true, l: 'quiet silent verbose zero-terminated lines bytes' });
|
||||
add('tail', { f: 'qvz', vn: 'nc', n: true, l: 'quiet silent verbose zero-terminated lines bytes' }); // no -f, -F, --follow, --retry, --pid, -s: those never end
|
||||
add('wc', { f: 'lwcmL', l: 'lines words bytes chars max-line-length' });
|
||||
add('grep egrep fgrep', GREP);
|
||||
add('rg', { // no --pre, --pre-glob, --hostname-bin (they run a program) and no -z/--search-zip (it runs decompressors)
|
||||
f: 'iIsSnNHlcwFxvUoLaquhVpbP', v: 'efgtTr', vn: 'ABCjdMm',
|
||||
l: 'ignore-case no-ignore-case smart-case case-sensitive fixed-strings word-regexp line-regexp invert-match count count-matches files-with-matches files-without-match files type-list hidden no-ignore no-ignore-vcs no-ignore-parent no-ignore-dot no-ignore-global no-ignore-messages follow line-number no-line-number with-filename no-filename column heading no-heading null only-matching quiet text multiline multiline-dotall pcre2 no-messages json stats trim unrestricted binary no-config vimgrep pretty byte-offset passthru crlf one-file-system no-unicode regexp file glob iglob type type-not max-count max-depth maxdepth context after-context before-context color replace threads sort sortr max-filesize max-columns path-separator encoding engine ignore-file glob-case-insensitive no-glob-case-insensitive' });
|
||||
add('stat', { f: 'LtxlnqrsFf', v: 'c', l: 'dereference file-system terse format printf' });
|
||||
add('file', { f: 'bhikLnNrvI', l: 'brief dereference no-dereference mime mime-type mime-encoding keep-going no-pad no-buffer raw' }); // no -C, -m, -f, -P (they write or read other files), no -z/-s/-p (they run decompressors, read devices, reset times)
|
||||
add('du', { f: 'abchHklmsxLPSD0', v: 'tB', vn: 'd', l: 'all apparent-size bytes total dereference-args human-readable si summarize one-file-system dereference no-dereference separate-dirs count-links inodes null max-depth threshold block-size time time-style exclude' });
|
||||
add('df', { f: 'ahHiklmPTv', v: 'txB', l: 'all human-readable si inodes local portability print-type total sync no-sync output block-size type exclude-type' });
|
||||
add('uname', { f: 'asnrvmpio', max: 0, l: 'all kernel-name nodename kernel-release kernel-version machine processor hardware-platform operating-system' });
|
||||
add('which', { f: 'as', l: 'all' });
|
||||
add('basename', { f: 'az', v: 's', l: 'multiple zero suffix' });
|
||||
add('dirname', { f: 'z', l: 'zero' });
|
||||
add('realpath', { f: 'emsqzLP', l: 'canonicalize-existing canonicalize-missing no-symlinks quiet zero strip logical physical relative-to relative-base' });
|
||||
add('readlink', { f: 'femnqsvz', l: 'canonicalize canonicalize-existing canonicalize-missing no-newline quiet silent verbose zero' });
|
||||
add('sort', { f: 'bdfghiMnRrsuVzmcC', v: 'kt', l: 'ignore-leading-blanks dictionary-order ignore-case general-numeric-sort human-numeric-sort ignore-nonprinting month-sort numeric-sort random-sort reverse stable unique version-sort zero-terminated check merge sort field-separator key' });
|
||||
// uniq's second operand is its OUTPUT file, so it takes at most one.
|
||||
add('uniq', { f: 'cdDuiz', vn: 'fsw', max: 1, l: 'count repeated all-repeated unique ignore-case skip-fields skip-chars check-chars zero-terminated group' });
|
||||
add('cut', { f: 'snz', v: 'bcfd', l: 'complement only-delimited zero-terminated bytes characters fields delimiter output-delimiter' });
|
||||
add('tr', { f: 'cCdst', max: 2 });
|
||||
add('date', { f: 'uR', o: 'I', v: 'dr', lv: 'date reference', l: 'utc universal rfc-email rfc-2822 iso-8601 rfc-3339',
|
||||
op: (word) => word.startsWith('+') }); // a bare operand like 1026120026 SETS the clock; only +FORMAT is a format
|
||||
add('id', { f: 'ugGnrazZ', l: 'user group groups name real context zero' });
|
||||
add('hostname', { f: 'sfiIdaAyv', max: 0 }); // `hostname NAME` renames the machine
|
||||
add('whoami', { l: 'help version', op: (word) => /^\/(all|user|groups|priv|upn|fqdn|logonid|nh)$/i.test(word) });
|
||||
add('tree', { f: 'adfilpsugDFqNQrtvxCnASUchJX', v: 'LPIHT', vn: '', l: 'help version filelimit charset sort timefmt noreport dirsfirst filesfirst prune du si gitignore matchdirs ignore-case inodes device nolinks' }); // no -o, no -R
|
||||
// `less` and `more` are not here: they wait for keys, and `less` can run commands.
|
||||
|
||||
/** The names that run without a card (for the tests). */
|
||||
export const readOnlyCommandNames = () => [...SPECS.keys()];
|
||||
|
||||
/** git subcommands that only read, with the options they may take. */
|
||||
const GIT_VIEW = 'oneline graph decorate no-decorate all branches tags remotes stat shortstat numstat dirstat name-only name-status patch no-patch raw summary compact-summary abbrev-commit no-abbrev-commit abbrev reverse follow no-merges merges first-parent full-history simplify-by-decoration simplify-merges ancestry-path left-right left-only right-only cherry-pick cherry-mark cherry topo-order date-order author-date-order relative-date all-match invert-grep regexp-ignore-case extended-regexp fixed-strings perl-regexp no-color color color-words word-diff word-diff-regex minimal patience histogram diff-algorithm check exit-code quiet cached staged merge-base no-index no-ext-diff no-textconv find-renames find-copies find-copies-harder no-renames ignore-space-change ignore-all-space ignore-space-at-eol ignore-blank-lines ignore-cr-at-eol function-context show-function unified inter-hunk-context src-prefix dst-prefix no-prefix full-index binary text source use-mailmap mailmap no-mailmap log-size notes no-notes show-signature diff-merges boundary children parents no-walk sparse date format pretty author committer grep since until after before max-count skip min-parents max-parents diff-filter encoding stat-width stat-name-width stat-graph-width stat-count count';
|
||||
const LISTING = ['l', 'list', 'contains', 'no-contains', 'merged', 'no-merged', 'points-at'];
|
||||
const GIT = new Map(Object.entries({
|
||||
status: spec({ f: 'sbvz', o: 'u', l: 'short branch porcelain long ignored untracked-files ignore-submodules renames no-renames find-renames show-stash ahead-behind no-ahead-behind column no-column verbose null' }),
|
||||
log: spec({ f: 'pswbMCizuacmrtqEFPR', vn: 'nU', v: 'SGL', n: true, l: GIT_VIEW }),
|
||||
diff: spec({ f: 'pswbMCizuacmrtqEFPR', vn: 'nU', v: 'SGL', n: true, l: GIT_VIEW }),
|
||||
show: spec({ f: 'pswbMCizuacmrtqEFPR', vn: 'nU', v: 'SGL', n: true, l: GIT_VIEW }),
|
||||
'rev-list': spec({ f: 'pswbMCizuacmrtqEFPR', vn: 'nU', v: 'SGL', n: true, l: GIT_VIEW }),
|
||||
shortlog: spec({ f: 'sneciw', n: false, l: `${GIT_VIEW} summary numbered email committer group` }),
|
||||
// Listing forms only. A name with no list option would CREATE a branch or a tag, and the options that change things are not here.
|
||||
branch: spec({ f: 'avri', l: 'list all remotes verbose show-current ignore-case contains no-contains merged no-merged points-at sort format abbrev no-abbrev color no-color column no-column omit-empty',
|
||||
post: (seen, count) => count === 0 || LISTING.some(name => seen.has(name)) }),
|
||||
tag: spec({ f: 'li', o: 'n', l: 'list sort contains no-contains merged no-merged points-at format column no-column ignore-case omit-empty color no-color',
|
||||
post: (seen, count) => count === 0 || LISTING.some(name => seen.has(name)) }),
|
||||
remote: spec({ f: 'v', l: 'verbose', max: 0 }),
|
||||
// `git config KEY` reads and `git config KEY VALUE` writes, so a key is only taken with --get; no --edit, --add, --unset, --file.
|
||||
config: spec({ f: 'lz', l: 'list get get-all get-regexp show-origin show-scope local global system worktree bool int bool-or-int path expiry-date type null name-only includes no-includes', lv: 'default',
|
||||
post: (seen, count) => (count > 0 ? ['get', 'get-all', 'get-regexp'].some(name => seen.has(name)) : seen.has('list') || seen.has('l')) }),
|
||||
'rev-parse': spec({ f: 'q', l: 'show-toplevel git-dir git-common-dir absolute-git-dir abbrev-ref symbolic symbolic-full-name short verify is-inside-work-tree is-inside-git-dir is-bare-repository is-shallow-repository show-prefix show-cdup show-superproject-working-tree all branches tags remotes quiet revs-only no-revs flags no-flags local-env-vars show-object-format path-format', lv: 'default git-path' }),
|
||||
'ls-files': spec({ f: 'cdmoiskuzvtfD', l: 'cached deleted modified others ignored stage directory unmerged killed no-empty-directory error-unmatch exclude-standard exclude full-name eol deduplicate recurse-submodules sparse abbrev resolve-undo format' }),
|
||||
'ls-tree': spec({ f: 'rtdlz', l: 'name-only name-status long full-name full-tree object-only abbrev format' }),
|
||||
blame: spec({ f: 'wMCespltbfnc', v: 'L', l: 'porcelain line-porcelain incremental show-email show-name show-number date root abbrev' }),
|
||||
describe: spec({ l: 'all tags always long dirty broken contains abbrev match exclude first-parent exact-match candidates' }),
|
||||
'cat-file': spec({ f: 'tsep', l: 'batch batch-check batch-all-objects allow-unknown-type buffer unordered follow-symlinks mailmap use-mailmap' }), // no --textconv, --filters
|
||||
// No -O / --open-files-in-pager: they run a program on the matches.
|
||||
grep: spec({ f: 'aiIlLcnvwHhEFGPqpWrzUo', v: 'e', vn: 'ABCm', n: true, l: 'no-index untracked cached no-exclude-standard exclude-standard recurse-submodules text ignore-case word-regexp invert-match full-name extended-regexp basic-regexp perl-regexp fixed-strings line-number column files-with-matches name-only files-without-match count null only-matching quiet no-color color break heading show-function function-context all-match and or not max-depth max-count threads after-context before-context context recursive no-recursive' }),
|
||||
}));
|
||||
|
||||
/**
|
||||
* One command's words against its spec: every option has to be one the spec knows (anything else is not understood, so it asks).
|
||||
* @param {ReturnType<typeof spec>} sp
|
||||
* @param {{t:string,q:boolean}[]} args the words after the command; q marks a quoted text, which is data and never an option
|
||||
*/
|
||||
function argumentsAreSafe(sp, args) {
|
||||
const seen = new Set();
|
||||
let operands = 0, ended = false;
|
||||
const operand = (arg) => { operands++; return operands <= sp.max && (!sp.op || sp.op(arg.t, arg.q)); };
|
||||
const valueOf = (i, numeric) => { // the next word as an option's value; it can be a quoted text, a number, or a word that is not an option
|
||||
const arg = args[i + 1];
|
||||
if (!arg) return false;
|
||||
return numeric ? NUMBER.test(arg.t) : arg.q || !arg.t.startsWith('-') || NUMBER.test(arg.t);
|
||||
};
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const { t, q } = args[i];
|
||||
if (q || ended || t === '-' || t[0] !== '-') { if (!operand(args[i])) return false; continue; }
|
||||
if (t === '--') { ended = true; continue; }
|
||||
if (t.startsWith('--')) {
|
||||
const eq = t.indexOf('=');
|
||||
const name = eq < 0 ? t.slice(2) : t.slice(2, eq);
|
||||
if (sp.l.has(name)) { seen.add(name); continue; }
|
||||
if (sp.lv.has(name)) { seen.add(name); if (eq < 0) { if (!valueOf(i, false)) return false; i++; } continue; }
|
||||
return false;
|
||||
}
|
||||
if (sp.ps) {
|
||||
const named = /^-([A-Za-z][A-Za-z0-9]*)(?::|$)/.exec(t);
|
||||
if (named && sp.ps.has(named[1].toLowerCase())) { seen.add(named[1].toLowerCase()); continue; }
|
||||
}
|
||||
if (sp.n && /^-\d+$/.test(t)) continue;
|
||||
for (let j = 1; j < t.length; j++) {
|
||||
const c = t[j];
|
||||
if (sp.f.includes(c)) { seen.add(c); continue; }
|
||||
if (sp.o.includes(c)) { seen.add(c); break; }
|
||||
const numeric = sp.vn.includes(c);
|
||||
if (!numeric && !sp.v.includes(c)) return false;
|
||||
seen.add(c);
|
||||
const rest = t.slice(j + 1);
|
||||
if (rest) { if (numeric && !NUMBER.test(rest)) return false; } else { if (!valueOf(i, numeric)) return false; i++; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sp.post ? sp.post(seen, operands) : true;
|
||||
}
|
||||
|
||||
/** git subcommands that only read. */
|
||||
const SAFE_GIT = new Set(['status', 'log', 'diff', 'show', 'branch', 'rev-parse', 'ls-files', 'blame', 'describe', 'remote', 'tag', 'shortlog', 'rev-list', 'cat-file', 'grep', 'ls-tree', 'config']);
|
||||
const SAFE_VERSION = /^(node|python|python3|py|npm|pnpm|git|dotnet|cargo|rustc|go|java|pip|pip3)\s+(--version|-v|-version|version)$/i;
|
||||
|
||||
/** Anything that can chain, redirect, expand or start something else is not read-only. */
|
||||
const UNSAFE_SYNTAX = /[;&<>`${}()]|\r|\n|--%|\bout-file\b|\bset-content\b|\badd-content\b|\binvoke-|\biex\b|\bstart-|\bnew-|\bremove-|\bcopy-|\bmove-|\brename-|\bclear-|\bset-|\bpush-|\bexpand-|\bsave-|\binstall-|\bcurl\b|\bwget\b|\bsudo\b|\bsh\b|\bbash\b|\bpwsh\b|\bpowershell\b|\bcmd\b|-exec\b|-delete\b|-execdir\b/i;
|
||||
/** Anything that can chain, redirect, expand or start something else is not read-only. (`sh` as a word, not inside an option like `du -sh`.) */
|
||||
const UNSAFE_SYNTAX = /[;&<>`${}()]|\r|\n|--%|\bout-file\b|\bset-content\b|\badd-content\b|\binvoke-|\biex\b|\bstart-|\bnew-|\bremove-|\bcopy-|\bmove-|\brename-|\bclear-|\bset-|\bpush-|\bexpand-|\bsave-|\binstall-|\bcurl\b|\bwget\b|\bsudo\b|(?<![-\w])sh(?![-\w])|\bbash\b|\bpwsh\b|\bpowershell\b|\bcmd\b|-exec\b|-delete\b|-execdir\b/i;
|
||||
|
||||
/**
|
||||
* The command with every quoted text replaced by a plain stand-in, or null when it cannot be read safely. What is inside quotes is data
|
||||
@@ -74,9 +245,10 @@ const UNSAFE_SYNTAX = /[;&<>`${}()]|\r|\n|--%|\bout-file\b|\bset-content\b|\badd
|
||||
* inside double quotes a $ or a backtick expands (a variable, a sub-command), and an unterminated quote means the text was not understood.
|
||||
* Either one gives null, and the command asks as before.
|
||||
* @param {string} text
|
||||
* @param {string[]} [quoted] when given, receives the text inside each quote, in order (the checks on operands read it)
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function maskQuotes(text) {
|
||||
export function maskQuotes(text, quoted) {
|
||||
let out = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
@@ -91,13 +263,14 @@ export function maskQuotes(text) {
|
||||
const before = text[i - 1], after = text[end + 1];
|
||||
if ((before !== undefined && !/[\s|]/.test(before)) || (after !== undefined && !/[\s|]/.test(after)) || text[i + 1] === '-') return null;
|
||||
out += '"_"';
|
||||
quoted?.push(text.slice(i + 1, end));
|
||||
i = end;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a shell command only reads. Conservative by design: one unrecognised word and the answer is no.
|
||||
* Whether a shell command only reads. Conservative by design: one unrecognised word or option and the answer is no.
|
||||
* Quoted text is data and is looked at after it has been set aside (maskQuotes): Get-ChildItem "C:\Users\x\Q&A" only lists.
|
||||
* @param {string} command
|
||||
* @returns {boolean}
|
||||
@@ -106,24 +279,21 @@ export function isReadOnlyShell(command) {
|
||||
const text = String(command ?? '').trim();
|
||||
if (!text || text.length > 600) return false;
|
||||
if (SAFE_VERSION.test(text)) return true;
|
||||
const masked = maskQuotes(text);
|
||||
const quoted = [];
|
||||
const masked = maskQuotes(text, quoted);
|
||||
if (masked === null || UNSAFE_SYNTAX.test(masked)) return false;
|
||||
const segments = masked.split('|').map(s => s.trim());
|
||||
return segments.every((segment) => {
|
||||
if (!segment) return false;
|
||||
const words = segment.split(/\s+/);
|
||||
const first = words[0].toLowerCase().replace(/^\.\\/, '');
|
||||
let next = 0;
|
||||
const tokens = masked.split('|').map(segment => segment.trim()).map(segment => words(segment).map(w => (w === '"_"' ? { t: quoted[next++] ?? '', q: true } : { t: w, q: false })));
|
||||
return tokens.every((segment) => {
|
||||
if (!segment.length || segment[0].q) return false;
|
||||
const first = segment[0].t.toLowerCase().replace(/^\.\\/, '');
|
||||
if (first === 'git') {
|
||||
// `git config` and `git tag`/`branch`/`remote` can write when given a value; only the bare listing forms are safe.
|
||||
const sub = (words[1] ?? '').toLowerCase();
|
||||
if (!SAFE_GIT.has(sub)) return false;
|
||||
if (['config', 'tag', 'branch', 'remote'].includes(sub)) {
|
||||
const rest = words.slice(2).filter(w => !w.startsWith('-') || /^(-l|-a|-v|-vv|--list|--all|--show-current|--get|--get-all)$/i.test(w));
|
||||
return rest.length === 0 || (sub === 'config' && words[2] === '--get') || (sub === 'remote' && words[2] === '-v');
|
||||
}
|
||||
return !words.some(w => /^--(output|ext-diff|textconv)\b/i.test(w));
|
||||
const sub = segment[1];
|
||||
const sp = sub && !sub.q ? GIT.get(sub.t) : null;
|
||||
return !!sp && argumentsAreSafe(sp, segment.slice(2));
|
||||
}
|
||||
return SAFE_COMMANDS.has(first) && !words.some(w => /^-(delete|exec|execdir|fprint|fls|ok)$/i.test(w));
|
||||
const sp = SPECS.get(first);
|
||||
return !!sp && argumentsAreSafe(sp, segment.slice(1));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* rewrites a plan turn wrote its numbered plan from "1." again this many times
|
||||
* context the context is nearly full: the Harness compaction did not keep up
|
||||
*
|
||||
* What the person is told is `guardNote`: fixed English sentences that are also in the interface catalog in all eight languages (the page
|
||||
* What the person is told is `guardNote`: fixed English sentences that are also in the interface catalog in all nine languages (the page
|
||||
* translates a `delta` frame that carries `key`). Each says what happened and gives one next step.
|
||||
*/
|
||||
|
||||
|
||||
@@ -67,40 +67,70 @@ export function nvidiaDriverVersion() {
|
||||
/** Where the builds are published; the wrong-make sentence points here. */
|
||||
export const RELEASES_URL = 'https://github.com/Niko1221/Strata/releases';
|
||||
|
||||
/** The PCI vendor ids (`10de`, `1002`, `8086`) of the Windows display adapters; empty off Windows or when the registry cannot be read. */
|
||||
export function windowsDisplayVendors({ platform = process.platform, run = execFileSync } = {}) {
|
||||
/** The PCI vendor ids of the display adapters Windows reports as present right now (PowerShell, Win32_VideoController); empty when that cannot be read. */
|
||||
export function windowsPresentVendors({ platform = process.platform, run = execFileSync } = {}) {
|
||||
const found = new Set();
|
||||
if (platform !== 'win32') return found;
|
||||
try {
|
||||
const out = run('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', 'Get-CimInstance Win32_VideoController | ForEach-Object { $_.PNPDeviceID }'], { encoding: 'utf8', timeout: 15000, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] });
|
||||
for (const match of String(out).matchAll(/VEN_([0-9A-F]{4})/gi)) found.add(match[1].toLowerCase());
|
||||
} catch { /* no PowerShell or a limited account: the registry answer stands */ }
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* The PCI vendor ids (`10de`, `1002`, `8086`) of the Windows display adapters; empty off Windows or when the registry cannot be read.
|
||||
* The registry keeps the entry of a card that was taken out (an NVIDIA card swapped for a Radeon, #3892): when NVIDIA and AMD are both
|
||||
* listed, only the makes Windows reports as present now count, the same rule Strata Control uses (engine_component.windows_display_adapters).
|
||||
*/
|
||||
export function windowsDisplayVendors({ platform = process.platform, run = execFileSync, present = windowsPresentVendors } = {}) {
|
||||
const found = new Set();
|
||||
if (platform !== 'win32') return found;
|
||||
try {
|
||||
const out = run('reg', ['query', 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}', '/s', '/v', 'MatchingDeviceId'], { encoding: 'utf8', timeout: 8000, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] });
|
||||
for (const match of String(out).matchAll(/VEN_([0-9A-F]{4})/gi)) found.add(match[1].toLowerCase());
|
||||
} catch { /* no registry access: the tools decide */ }
|
||||
if (found.has('10de') && found.has('1002')) {
|
||||
const now = present({ platform, run });
|
||||
const kept = new Set([...found].filter(vendor => now.has(vendor)));
|
||||
if (kept.size) return kept;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
const BACKEND_OF_VENDOR = [['10de', 'cuda'], ['1002', 'hip'], ['8086', 'sycl']];
|
||||
|
||||
/**
|
||||
* Every engine family this PC's graphics cards can run, in the order NVIDIA, AMD, Intel; STRATA_ENGINE_BACKEND wins. A PC with two
|
||||
* makes lists both, so a build for either make is accepted (recommend, never force).
|
||||
*/
|
||||
export function gpuBackendsHere(env = process.env, { vendors = windowsDisplayVendors() } = {}) {
|
||||
const forced = String(env.STRATA_ENGINE_BACKEND ?? '').trim().toLowerCase();
|
||||
if (forced) return ['cuda', 'hip', 'sycl'].includes(forced) ? [forced] : [];
|
||||
if (vendors.size) {
|
||||
const list = BACKEND_OF_VENDOR.filter(([vendor]) => vendors.has(vendor)).map(([, backend]) => backend);
|
||||
if (list.length) return list;
|
||||
}
|
||||
if (nvidiaDriverVersion()) return ['cuda'];
|
||||
try {
|
||||
const base = '/sys/class/drm';
|
||||
const seen = new Set();
|
||||
for (const card of readdirSync(base)) {
|
||||
if (!/^card\d+$/.test(card)) continue;
|
||||
try { seen.add(readFileSync(`${base}/${card}/device/vendor`, 'utf8').trim().toLowerCase()); } catch { /* no vendor file */ }
|
||||
}
|
||||
const list = [['0x10de', 'cuda'], ['0x1002', 'hip'], ['0x8086', 'sycl']].filter(([vendor]) => seen.has(vendor)).map(([, backend]) => backend);
|
||||
if (list.length) return list;
|
||||
} catch { /* not Linux */ }
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* The engine family this PC's graphics card needs ('cuda', 'hip', 'sycl'), or null when it cannot be told. The same order as Control
|
||||
* (NVIDIA first, then AMD, then Intel); STRATA_ENGINE_BACKEND wins. Reads only the driver and (Linux) the PCI vendor of the display adapters.
|
||||
*/
|
||||
export function gpuBackendHere(env = process.env, { vendors = windowsDisplayVendors() } = {}) {
|
||||
const forced = String(env.STRATA_ENGINE_BACKEND ?? '').trim().toLowerCase();
|
||||
if (forced) return ['cuda', 'hip', 'sycl'].includes(forced) ? forced : null;
|
||||
// Windows: the display adapters are the machine's own statement of which cards it has; a leftover nvidia-smi from a card that was
|
||||
// taken out must not outvote them (#3688). Same order as Control: NVIDIA, then AMD, then Intel.
|
||||
if (vendors.size) {
|
||||
for (const [vendor, backend] of [['10de', 'cuda'], ['1002', 'hip'], ['8086', 'sycl']]) if (vendors.has(vendor)) return backend;
|
||||
}
|
||||
if (nvidiaDriverVersion()) return 'cuda';
|
||||
try {
|
||||
const base = '/sys/class/drm';
|
||||
const vendors = new Set();
|
||||
for (const card of readdirSync(base)) {
|
||||
if (!/^card\d+$/.test(card)) continue;
|
||||
try { vendors.add(readFileSync(`${base}/${card}/device/vendor`, 'utf8').trim().toLowerCase()); } catch { /* no vendor file */ }
|
||||
}
|
||||
for (const [vendor, backend] of [['0x10de', 'cuda'], ['0x1002', 'hip'], ['0x8086', 'sycl']]) if (vendors.has(vendor)) return backend;
|
||||
} catch { /* not Linux */ }
|
||||
return null;
|
||||
export function gpuBackendHere(env = process.env, options = {}) {
|
||||
return gpuBackendsHere(env, options)[0] ?? null;
|
||||
}
|
||||
|
||||
const HIP_FAMILIES = [['RX 9000', ['gfx1200', 'gfx1201']], ['RX 7000', ['gfx1100', 'gfx1101', 'gfx1102']], ['RX 6000', ['gfx1030', 'gfx1031']], ['Ryzen AI Max', ['gfx1151']]];
|
||||
@@ -147,10 +177,12 @@ export async function installBundledEngine({ bundleDir, components, platform = n
|
||||
if (platform) packages = packages.filter(p => p.manifest.platform === platform);
|
||||
if (!packages.length) return { action: 'none' };
|
||||
// A build for another make of graphics card than this PC has can never run here: it is not unpacked, and the caller says so in plain words.
|
||||
const wanted = backend === undefined ? gpuBackendHere() : backend;
|
||||
// A PC with two makes (an AMD card for the model, an NVIDIA card for the desktop) accepts a build for either (#3892).
|
||||
const wantedList = backend === undefined ? gpuBackendsHere() : (Array.isArray(backend) ? backend : backend ? [backend] : []);
|
||||
const wanted = wantedList[0] ?? null;
|
||||
if (wanted && ['cuda', 'hip', 'sycl'].includes(wanted)) {
|
||||
const gpu = packages.filter(p => ['cuda', 'hip', 'sycl'].includes(p.manifest.backend));
|
||||
const fits = packages.filter(p => !gpu.includes(p) || p.manifest.backend === wanted);
|
||||
const fits = packages.filter(p => !gpu.includes(p) || wantedList.includes(p.manifest.backend));
|
||||
if (gpu.length && !fits.length) {
|
||||
const first = gpu.sort((a, b) => compareVersions(b.manifest.version, a.manifest.version))[0].manifest;
|
||||
return { action: 'wrong-make', built: first.backend, here: wanted, notice: wrongMakeNotice(first, wanted) };
|
||||
|
||||
@@ -74,6 +74,17 @@ export function findControl({ env = process.env, platform = process.platform, ex
|
||||
return control;
|
||||
}
|
||||
|
||||
/**
|
||||
* The environment of the server the host starts from a source checkout (#3851): the key travels in the run configuration, so a
|
||||
* STRATA_API_KEY the host itself was started with is removed. Setting it to an empty string made serve/server.py stop with
|
||||
* "an API key was given but it is empty".
|
||||
*/
|
||||
export function sourceServerEnvironment(env = process.env) {
|
||||
const next = { ...env, PYTHONUNBUFFERED: '1', PYTHONUTF8: '1', PYTHONIOENCODING: 'utf-8' };
|
||||
delete next.STRATA_API_KEY;
|
||||
return next;
|
||||
}
|
||||
|
||||
/** The environment Control runs in: nothing from the user's Python, and acquiring forbidden. */
|
||||
export function controlEnvironment(control, { env = process.env, components = componentsDirectory({ env }), acquire = false } = {}) {
|
||||
const next = { ...env };
|
||||
|
||||
@@ -40,13 +40,15 @@ export function parseNvidiaDevices(raw) {
|
||||
return [{id:uuid,index:Number(index),sensorIndex:Number(index),busId:bus?.toLowerCase(),vendor:'nvidia',name,vramGb:round((number(memory)??0)/1024,1),unified:false,backend:'cuda',driver}];
|
||||
});
|
||||
}
|
||||
/** A percentage reading; "[N/A]" and out-of-range values are no reading. */
|
||||
const percentOf = v => { const n=number(v); return n!==null&&n>=0&&n<=100?n:null; };
|
||||
export function parseNvidiaReadings(raw) {
|
||||
return raw.split('\n').flatMap(line=>{
|
||||
const [id,temp,load,memory,power,limit,gen,maxGen,width]=line.split(',').map(s=>s.trim());
|
||||
const [id,temp,load,memory,power,limit,gen,maxGen,width,fan,clock]=line.split(',').map(s=>s.trim());
|
||||
if (!id?.startsWith('GPU-')) return [];
|
||||
const t=number(temp),u=number(load),m=number(memory);
|
||||
const positive = v => { const n=number(v); return n!==null&&n>=0?n:null; };
|
||||
return [{id,temperatureC:t!==null&&t>=0&&t<=150?t:null,utilizationPct:u!==null&&u>=0&&u<=100?u:null,memoryUsedGb:m!==null&&m>=0?round(m/1024,2):null,powerW:positive(power),powerLimitW:positive(limit),pcieGen:positive(gen),pcieGenMax:positive(maxGen),pcieWidth:positive(width),pcieRxMbs:null}];
|
||||
return [{id,temperatureC:t!==null&&t>=0&&t<=150?t:null,utilizationPct:u!==null&&u>=0&&u<=100?u:null,memoryUsedGb:m!==null&&m>=0?round(m/1024,2):null,powerW:positive(power),powerLimitW:positive(limit),pcieGen:positive(gen),pcieGenMax:positive(maxGen),pcieWidth:positive(width),pcieRxMbs:null,fanPct:percentOf(fan),clockMhz:positive(clock)}];
|
||||
});
|
||||
}
|
||||
export function parsePcieThroughput(raw) {
|
||||
@@ -73,14 +75,15 @@ export async function detectDrmGpus() {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export async function readGpuTelemetry(gpus) {
|
||||
/** `transfer: false` skips the PCIe throughput sample (a second nvidia-smi); it only means something while a model runs. */
|
||||
export async function readGpuTelemetry(gpus, { transfer: wantTransfer = true, run = probe } = {}) {
|
||||
const now=new Date().toISOString();
|
||||
const hasNvidia=gpus.some(g=>g.vendor==='nvidia');
|
||||
let raw=hasNvidia?await probe('nvidia-smi',['--query-gpu=uuid,temperature.gpu,utilization.gpu,memory.used,power.draw,power.limit,pcie.link.gen.current,pcie.link.gen.max,pcie.link.width.current','--format=csv,noheader,nounits'],2000):'';
|
||||
let raw=hasNvidia?await run('nvidia-smi',['--query-gpu=uuid,temperature.gpu,utilization.gpu,memory.used,power.draw,power.limit,pcie.link.gen.current,pcie.link.gen.max,pcie.link.width.current,fan.speed,clocks.current.graphics','--format=csv,noheader,nounits'],2000):'';
|
||||
// Older drivers can reject a field; retain their basic sensors in that case.
|
||||
if(hasNvidia&&!raw)raw=await probe('nvidia-smi',['--query-gpu=uuid,temperature.gpu,utilization.gpu,memory.used','--format=csv,noheader,nounits'],2000);
|
||||
if(hasNvidia&&!raw)raw=await run('nvidia-smi',['--query-gpu=uuid,temperature.gpu,utilization.gpu,memory.used','--format=csv,noheader,nounits'],2000);
|
||||
const nvidia=new Map(parseNvidiaReadings(raw).map(r=>[r.id,r]));
|
||||
const transfer=hasNvidia?parsePcieThroughput(await probe('nvidia-smi',['dmon','-s','t','-c','1'],1500)):new Map();
|
||||
const transfer=hasNvidia&&wantTransfer?parsePcieThroughput(await run('nvidia-smi',['dmon','-s','t','-c','1'],1500)):new Map();
|
||||
return Promise.all(gpus.map(async gpu=>{
|
||||
let reading=nvidia.get(gpu.id)??{id:gpu.id??`${gpu.vendor}:${gpu.index}`,temperatureC:null,utilizationPct:null,memoryUsedGb:null,powerW:null,powerLimitW:null,pcieGen:null,pcieGenMax:null,pcieWidth:null,pcieRxMbs:null};
|
||||
if(gpu.vendor==='nvidia')reading={...reading,pcieRxMbs:transfer.get(gpu.sensorIndex??gpu.index)??null};
|
||||
@@ -89,11 +92,13 @@ export async function readGpuTelemetry(gpus) {
|
||||
if(counters)reading={...reading,utilizationPct:counters.utilizationPct,memoryUsedGb:round(counters.memoryUsedGb,2)};
|
||||
}
|
||||
if(gpu.sensorPath&&!nvidia.has(gpu.id)) {
|
||||
let temperature=null,power=null,powerLimit=null;
|
||||
let temperature=null,power=null,powerLimit=null,fanPwm=null,clockHz=null;
|
||||
for(const hwmon of await list(`${gpu.sensorPath}/hwmon`)) {
|
||||
const folder=`${gpu.sensorPath}/hwmon/${hwmon}`,inputs=(await list(folder)).filter(n=>/^temp\d+_input$/.test(n));
|
||||
power ??= number(await read(`${folder}/power1_average`)) ?? number(await read(`${folder}/power1_input`));
|
||||
powerLimit ??= number(await read(`${folder}/power1_cap`));
|
||||
fanPwm ??= number(await read(`${folder}/pwm1`)); // 0-255, the fan's duty as the amdgpu driver reports it
|
||||
clockHz ??= number(await read(`${folder}/freq1_input`)); // the graphics clock in Hz
|
||||
for(const input of inputs) {
|
||||
const label=await read(`${folder}/${input.replace('_input','_label')}`);
|
||||
if(label&&!/edge|gpu/i.test(label))continue;
|
||||
@@ -105,7 +110,7 @@ export async function readGpuTelemetry(gpus) {
|
||||
const utilization=number(await read(`${gpu.sensorPath}/gpu_busy_percent`)),bytes=number(await read(`${gpu.sensorPath}/mem_info_vram_used`));
|
||||
const gen=path=>read(path).then(raw=>({2.5:1,5:2,8:3,16:4,32:5,64:6}[Number.parseFloat(raw)]??null));
|
||||
const [pcieGen,pcieGenMax,width]=await Promise.all([gen(`${gpu.sensorPath}/current_link_speed`),gen(`${gpu.sensorPath}/max_link_speed`),read(`${gpu.sensorPath}/current_link_width`)]);
|
||||
reading={...reading,temperatureC:temperature,utilizationPct:utilization!==null&&utilization>=0&&utilization<=100?utilization:null,memoryUsedGb:bytes!==null&&bytes>=0?round(bytes/1024**3,2):null,powerW:power!==null&&power>=0?power/1e6:null,powerLimitW:powerLimit!==null&&powerLimit>=0?powerLimit/1e6:null,pcieGen,pcieGenMax,pcieWidth:number(width)};
|
||||
reading={...reading,fanPct:fanPwm!==null&&fanPwm>=0&&fanPwm<=255?Math.round(fanPwm/255*100):null,clockMhz:clockHz!==null&&clockHz>0?Math.round(clockHz/1e6):null,temperatureC:temperature,utilizationPct:utilization!==null&&utilization>=0&&utilization<=100?utilization:null,memoryUsedGb:bytes!==null&&bytes>=0?round(bytes/1024**3,2):null,powerW:power!==null&&power>=0?power/1e6:null,powerLimitW:powerLimit!==null&&powerLimit>=0?powerLimit/1e6:null,pcieGen,pcieGenMax,pcieWidth:number(width)};
|
||||
}
|
||||
return {...reading,sampledAt:now,source:Object.entries(reading).some(([key,value])=>key!=='id'&&typeof value==='number')?'sensor':'none'};
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// DeepSeek-V4.1-Flash's own context limit, read from its pack (the app offers sizes up to it: src/lib/deepseek-context.ts).
|
||||
// Project Maya v1.1.28 (#442, @needmorevram, MIT) did the same in its dashboard. strata_control/dsv41_context.py reads the same file for setup and the Terminal.
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
/** The pack's own limit: `max_position_embeddings` in <pack>/ds4.txt. Null when there is no pack, no file or no such line (the choices then stay as before). */
|
||||
export function packContextLimit(pack) {
|
||||
if (!pack) return null;
|
||||
try {
|
||||
const match = /^max_position_embeddings\s+(\d+)\s*$/m.exec(readFileSync(join(pack, 'ds4.txt'), 'utf8'));
|
||||
const value = match ? Number(match[1]) : 0;
|
||||
return Number.isSafeInteger(value) && value >= 1024 ? value : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
/** The limit of the pack a DeepSeek run config names (`--pack DIR` in its arguments). */
|
||||
export function configContextLimit(config) {
|
||||
const args = (config?.args ?? []).map(String), at = args.indexOf('--pack');
|
||||
return at >= 0 && at + 1 < args.length ? packContextLimit(args[at + 1]) : null;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* GPU energy since the host started, in Wh (#3628): the power reading held for the time since the last tick, added up.
|
||||
* A long gap (the PC slept, the host was paused) counts for at most 5 s, so a sleep never adds a made-up hour of use.
|
||||
*/
|
||||
const meter = { wh: 0, at: 0 };
|
||||
|
||||
export function addEnergy(powerW, at) {
|
||||
const dt = meter.at ? Math.min((at - meter.at) / 1000, 5) : 0;
|
||||
meter.at = at;
|
||||
if (typeof powerW === 'number' && Number.isFinite(powerW) && powerW >= 0 && dt > 0) meter.wh += powerW * dt / 3600;
|
||||
return meter.wh;
|
||||
}
|
||||
|
||||
export const energyWh = () => meter.wh;
|
||||
@@ -71,6 +71,8 @@ import { createRuntimeModels } from './runtime-models.mjs';
|
||||
import { createNativeControl } from './native-control.mjs';
|
||||
import { nativeUnloadPlan } from './storage-check.mjs';
|
||||
import { installBundledEngine, cleanupAfterInstall } from './bundled-engine.mjs';
|
||||
import { systemLocale } from './region.mjs';
|
||||
import { createShellNotifier, DEFAULT_PORT, parsePortNotice, readShellPrefs, writeShellPrefs } from './shell-prefs.mjs';
|
||||
import { findControl, createControl, componentsDirectory } from './control-runtime.mjs';
|
||||
import { forbidden as acquisitionForbidden } from './acquisition.mjs';
|
||||
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
||||
@@ -115,7 +117,7 @@ const WORKSPACE_NAME = process.env.STRATA_WORKSPACE ? basename(WORKSPACE) : 'Str
|
||||
const insideApp = path => { const rel = relative(ROOT, resolve(path)); return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel)); };
|
||||
|
||||
const hub = new EventHub();
|
||||
const modelProviders = createModelProviders({ directory: store.DATA_DIR, changed: value => hub.send('providers', withPictureAnswers(value)) });
|
||||
const modelProviders = createModelProviders({ directory: store.DATA_DIR, knownThinks: (connection, modelId) => visionProbe.knownThinks(connection, modelId), changed: value => hub.send('providers', withPictureAnswers(value)) });
|
||||
const imageModels = createImageModels({ dataDir: store.DATA_DIR, modelsDir: () => store.MODELS_DIR, real: models.REAL, changed: value => hub.send('image-models', value) });
|
||||
/* The installed app ships Strata Control (python + strata_control) next to the host; a source checkout has none and keeps its older paths. */
|
||||
const CONTROL = findControl();
|
||||
@@ -127,6 +129,19 @@ const appUi = createAppUi({ shutdown: () => shutdown() });
|
||||
/* The app's version has one source: the desktop shell passes it (STRATA_APP_VERSION, from apps/desktop/src-tauri/Cargo.toml). Outside the app it is read from the same file in a checkout, else 'dev'. */
|
||||
function appVersion() { return appVersionFrom({ env: process.env, here: HERE }); }
|
||||
const appUpdater = appUi.updater;
|
||||
const shellNotifier = createShellNotifier();
|
||||
/** What the page needs for the desktop settings: whether this is the desktop app, the port in use, the saved choices and a busy-port notice. */
|
||||
function shellState() {
|
||||
const saved = readShellPrefs();
|
||||
const desktop = !!shellNotifier;
|
||||
return {
|
||||
desktop,
|
||||
tray: desktop && process.platform !== 'linux',
|
||||
port: PORT, defaultPort: DEFAULT_PORT,
|
||||
pinnedPort: saved.port || null, minimizeToTray: saved.tray,
|
||||
portNotice: parsePortNotice(process.env.STRATA_PORT_NOTICE),
|
||||
};
|
||||
}
|
||||
/* The engine that ships inside the installer is put in the components folder when this PC has none (or an older one); no network, hashes checked. The model list waits for it. */
|
||||
const bundledEngine = control && !process.env.STRATA_NO_BUNDLED_ENGINE
|
||||
? installBundledEngine({ bundleDir: process.env.STRATA_ENGINE_BUNDLE_DIR || resolve(HERE, '..', '..', 'engine-bundle'), components: componentsDirectory(), log: line => console.log(line) })
|
||||
@@ -224,6 +239,7 @@ models.init(hub);
|
||||
engine.init(hub);
|
||||
engineInstall.init(hub);
|
||||
monitor.init(hub, boot);
|
||||
monitor.setLiveProbe(() => native.hasLive());
|
||||
serving.init(hub, { port: PORT, host: HOST });
|
||||
runtimeModels=createRuntimeModels({hardware:()=>hardware.cachedHardware()??boot,library:()=>models.library(),requests:()=>serving.state().requests,real:models.REAL,changed:value=>{
|
||||
if(!models.REAL){const primary=value.instances.find(i=>i.id===value.defaultId);if(store.get().loaded!==(primary?.variantId??null))store.update({loaded:primary?.variantId??null});}
|
||||
@@ -557,17 +573,18 @@ function clientName(req) {
|
||||
return 'Local client';
|
||||
}
|
||||
|
||||
async function runRemoteTurn({ selection, message, history, images = [], onFrame, signal, workspaceId }) {
|
||||
async function runRemoteTurn({ selection, message, history, images = [], thinking, onFrame, signal, workspaceId }) {
|
||||
const controller = new AbortController();
|
||||
const requestSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
||||
let entry;
|
||||
const started = Date.now();
|
||||
try {
|
||||
const { connection, model } = modelProviders.resolve(selection);
|
||||
if (connection.protocol === 'openai-chat' && model.reasoning == null) await visionProbe.check(connection, model.id);
|
||||
const appliedSkills = selectSkills(skillState, message, workspaceId);
|
||||
entry = serving.open({ client: 'Strata chat', endpoint: connection.protocol === 'anthropic' ? '/messages' : connection.protocol === 'openai-responses' ? '/responses' : '/chat/completions', model: `${connection.name} · ${model.name}`, cancel: () => controller.abort(new DOMException('Cancelled by the user', 'AbortError')) });
|
||||
serving.mark(entry.id, { state: 'generating' });
|
||||
const result = await modelProviders.stream({ selection, message, history, images, signal: requestSignal, systemPrompt: ['You are Strata, a helpful assistant. Answer clearly. Do not claim to have read files or performed actions unless their results are included in the conversation.', skillInstructions(appliedSkills)].filter(Boolean).join('\n\n'), onFrame: frame => {
|
||||
const result = await modelProviders.stream({ selection, message, history, images, thinking, signal: requestSignal, systemPrompt: ['You are Strata, a helpful assistant. Answer clearly. Do not claim to have read files or performed actions unless their results are included in the conversation.', skillInstructions(appliedSkills)].filter(Boolean).join('\n\n'), onFrame: frame => {
|
||||
if (frame.type === 'start') noteSkillUse(appliedSkills);
|
||||
if (frame.type === 'done') serving.mark(entry.id, { tokensIn: frame.tokensIn, tokensOut: frame.tokensOut, usageSource: frame.usageSource });
|
||||
onFrame({ ...frame, requestId: entry.id, model: model.name, contextMax: model.contextMax, remoteModel: true, ...(frame.type === 'start' ? { skills: appliedSkills.map(s => s.name) } : {}) });
|
||||
@@ -630,7 +647,7 @@ async function runAgentTurn({ res, controller, chatId, body, message, workspaceI
|
||||
}
|
||||
|
||||
async function runTurn({ message, history = [], onFrame, signal, client, workspaceId = null, sessionId = null, remoteModel = null, runtimeModelId = undefined, nativeModelId, images=[], useTools=false, thinking='medium', thinkingBudget=0 }) {
|
||||
if (remoteModel) return runRemoteTurn({ selection: remoteModel, message, history, images, onFrame, signal, workspaceId });
|
||||
if (remoteModel) return runRemoteTurn({ selection: remoteModel, message, history, images, thinking, onFrame, signal, workspaceId });
|
||||
if(native.hasLive()||nativeModelId){
|
||||
let instance;try{instance=native.resolveInstance(nativeModelId??native.state().defaultId);}catch(e){const message=plainNoModel(e.message);onFrame({type:'error',message});return {error:message};}const started=Date.now(),cancel=new AbortController();
|
||||
const entry=serving.open({client:client??'Strata chat',endpoint:'/chat/completions',model:instance.apiModelId,instanceId:instance.id,gpuIds:instance.gpus.map(String),cancel:()=>cancel.abort()});
|
||||
@@ -1586,7 +1603,7 @@ async function api(req, res, path, method, url) {
|
||||
if (cancelMatch && method === 'POST') return json(res, 200, serving.cancel(cancelMatch[1]));
|
||||
|
||||
// --- monitor ------------------------------------------------------------
|
||||
if (path === '/api/monitor') return json(res, 200, monitor.snapshot());
|
||||
if (path === '/api/monitor') { monitor.watch(); return json(res, 200, monitor.snapshot()); }
|
||||
|
||||
// --- skills / workspace -------------------------------------------------
|
||||
if (path === '/api/skills' && method === 'GET') {
|
||||
@@ -1930,6 +1947,19 @@ async function api(req, res, path, method, url) {
|
||||
try { return json(res, 200, await agent.approve({ chatId: b.sessionId, callId: b.toolId, decision: b.decision })); }
|
||||
catch (e) { return error(res, e.code === 'approval-gone' ? 409 : 400, e.message); }
|
||||
}
|
||||
/* The desktop shell's own choices (#3402, #3477): the port it asks for, the tray, and a desktop notification through the shell. */
|
||||
if (path === '/api/locale' && method === 'GET') return json(res, 200, systemLocale());
|
||||
if (path === '/api/shell' && method === 'GET') return json(res, 200, shellState());
|
||||
if (path === '/api/shell/prefs' && method === 'POST') {
|
||||
const b = await readBody(req);
|
||||
try { writeShellPrefs(b); } catch (e) { return error(res, 400, e.message); }
|
||||
return json(res, 200, shellState());
|
||||
}
|
||||
if (path === '/api/shell/notify' && method === 'POST') {
|
||||
const b = await readBody(req);
|
||||
if (!shellNotifier || typeof b.title !== 'string') return json(res, 200, { shown: false });
|
||||
try { return json(res, 200, await shellNotifier.notify({ title: b.title, body: String(b.body ?? '') })); } catch { return json(res, 200, { shown: false }); }
|
||||
}
|
||||
/* The person changed the chat's mode while a turn runs: it applies to the next tool call, goal and helpers included. */
|
||||
if (path === '/api/chat/mode' && method === 'POST') {
|
||||
const b = await readBody(req);
|
||||
|
||||
@@ -14,8 +14,22 @@ export function createLocalShare({ handle, hasKey, changed = () => {}, host = '0
|
||||
if (listener) throw new Error('Sharing is already starting.');
|
||||
listener = http.createServer(async (req, res) => {
|
||||
const path = apiPath(new URL(req.url ?? '/', 'http://localhost').pathname);
|
||||
const method = req.method ?? 'GET';
|
||||
// #3208: with an API key set, a browser page of any site can call the shared API - the key decides, not the
|
||||
// origin. The browser's preflight (OPTIONS) never carries the key (the spec says so), so it is answered here,
|
||||
// before the path check and the key check; the real request that follows still goes through both. Without a
|
||||
// key no CORS headers go out, as before: the listener stays "a device on this network" (SECURITY.md), and a
|
||||
// web page of another site on the internet cannot read the model through it.
|
||||
if (hasKey()) {
|
||||
res.setHeader('access-control-allow-origin', '*');
|
||||
res.setHeader('access-control-allow-methods', 'GET, POST, OPTIONS');
|
||||
// the headers the preflight asks for (SDKs add their own; Anthropic browser clients send anthropic-version)
|
||||
res.setHeader('access-control-allow-headers', String(req.headers['access-control-request-headers'] ?? '') || 'authorization, content-type, x-api-key, x-strata-client, anthropic-version, anthropic-beta');
|
||||
res.setHeader('access-control-max-age', '600');
|
||||
if (method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
||||
}
|
||||
if (!['/v1/models','/v1/chat/completions','/v1/messages'].includes(path)) { res.writeHead(404); res.end('Not found'); return; }
|
||||
try { await handle(req, res, path, req.method ?? 'GET'); }
|
||||
try { await handle(req, res, path, method); }
|
||||
catch { if (!res.headersSent) res.writeHead(500); res.end('Request failed'); }
|
||||
});
|
||||
try { await new Promise((resolve,reject) => { listener.once('error',reject); listener.listen(port,host,resolve); }); }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Saved API connections. Secrets never leave the host's private credentials file. */
|
||||
import { modelReadsPictures, modelSupportsReasoning } from './vision-probe.mjs';
|
||||
import { isOpenRouter, OPENROUTER_EFFORTS } from './agent/profile.mjs';
|
||||
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
@@ -49,7 +50,7 @@ export function normalizeBaseUrl(value) {
|
||||
const publicConnection = ({ apiKey, ...connection }) => ({ ...connection, apiKeySet: !!apiKey });
|
||||
const numeric = value => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
|
||||
|
||||
export function createModelProviders({ directory, changed = () => {}, fetchImpl = fetch }) {
|
||||
export function createModelProviders({ directory, changed = () => {}, fetchImpl = fetch, knownThinks = () => null }) {
|
||||
mkdirSync(directory, { recursive: true });
|
||||
const file = join(directory, 'model-providers.json');
|
||||
let connections = [];
|
||||
@@ -142,7 +143,7 @@ export function createModelProviders({ directory, changed = () => {}, fetchImpl
|
||||
const models = [...new Map(rows.filter(m => text(m.id, 256)).map(m => [m.id, { id: text(m.id, 256), name: text(m.display_name ?? m.name) || text(m.id, 256), contextMax: numeric(m.context_length ?? m.max_context_length ?? m.context_window), maxOutput: numeric(m.max_output_tokens ?? m.max_completion_tokens ?? m.top_provider?.max_completion_tokens), vision: modelReadsPictures(m), reasoning: modelSupportsReasoning(m) }])).values()];
|
||||
return { models: models.sort((a, b) => a.name.localeCompare(b.name)) };
|
||||
}
|
||||
async function stream({ selection, message, history = [], systemPrompt = '', images = [], signal, onFrame }) {
|
||||
async function stream({ selection, message, history = [], systemPrompt = '', images = [], thinking, signal, onFrame }) {
|
||||
const { connection, model } = resolve(selection);
|
||||
const started = Date.now();
|
||||
const messages = history.filter(m => ['user', 'assistant'].includes(m.role) && typeof m.content === 'string').slice(-40).map(m => ({ role: m.role, content: m.content }));
|
||||
@@ -155,6 +156,12 @@ export function createModelProviders({ directory, changed = () => {}, fetchImpl
|
||||
: protocol === 'openai-responses'
|
||||
? { model: model.id, input: messages, instructions: systemPrompt, stream: true, store: false }
|
||||
: { model: model.id, messages: [...(systemPrompt ? [{ role: 'system', content: systemPrompt }] : []), ...messages], stream: true, ...(['openai', 'deepseek', 'openrouter', 'google', 'moonshotai'].includes(connection.provider) ? { stream_options: { include_usage: true } } : {}) };
|
||||
// Match the native composer's effort names. Unknown support and other protocols keep provider defaults.
|
||||
const choice = typeof thinking === 'string' ? thinking.toLowerCase() : '';
|
||||
if (protocol === 'openai-chat' && (model.reasoning ?? knownThinks(connection, model.id)) === true && ['off', 'low', 'medium', 'high', 'max'].includes(choice)) {
|
||||
if (isOpenRouter(connection)) payload.reasoning = { effort: OPENROUTER_EFFORTS[choice] };
|
||||
else payload.reasoning_effort = ({ off: 'none', max: 'high' })[choice] ?? choice;
|
||||
}
|
||||
const requestSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(10 * 60 * 1000)]) : AbortSignal.timeout(10 * 60 * 1000);
|
||||
const res = await request(connection, protocol === 'anthropic' ? 'messages' : protocol === 'openai-responses' ? 'responses' : 'chat/completions', { method: 'POST', body: JSON.stringify(payload), signal: requestSignal });
|
||||
if (!res.body) fail('The model API returned an empty response.');
|
||||
|
||||
@@ -3,6 +3,9 @@ import { variant as catalogVariant, footprint } from './catalog.mjs';
|
||||
import { detect, cachedHardware } from './hardware.mjs';
|
||||
import { readGpuTelemetry, gpuMemoryTotals } from './device-probes.mjs';
|
||||
import { createSystemSampler } from './system-telemetry.mjs';
|
||||
import { sensorPace, WATCH_MS } from './sensor-pace.mjs';
|
||||
import { readTemperatures } from './temperature-telemetry.mjs';
|
||||
import { addEnergy, energyWh } from './energy-meter.mjs';
|
||||
import { state as servingState } from './serving.mjs';
|
||||
import { originalDashboard } from './monitor-dashboard.mjs';
|
||||
import { get as getStore } from './store.mjs';
|
||||
@@ -58,7 +61,17 @@ let hwCache = null;
|
||||
let ticker = null;
|
||||
let memoryWarningShown = false;
|
||||
let sensorsPending = false, sensorsAt = 0;
|
||||
/* The Monitor screen asks (GET /api/monitor) every few seconds while it is open; a model that is loaded counts as live (#4066). */
|
||||
let watchedUntil = 0, liveProbe = () => false;
|
||||
export function watch() {
|
||||
const wasIdle = Date.now() >= watchedUntil;
|
||||
watchedUntil = Date.now() + WATCH_MS;
|
||||
if (wasIdle) sensorsAt = 0; // opening the Monitor after a quiet spell reads the card at once
|
||||
}
|
||||
export function setLiveProbe(probe) { liveProbe = typeof probe === 'function' ? probe : () => false; }
|
||||
let systemReading=null;
|
||||
/* CPU and SSD temperatures (null where this PC cannot give one) and the GPU energy used since the host started (#3628). */
|
||||
let temperatures={cpuTempC:null,ssdTempC:null};
|
||||
const sampleSystem=createSystemSampler();
|
||||
const finite = value => typeof value==='number'&&Number.isFinite(value);
|
||||
function aggregate(readings,key,mode='sum') {
|
||||
@@ -71,7 +84,7 @@ function hardwareSnapshot() {
|
||||
const demo=process.env.STRATA_REAL_ENGINE!=='1',hw=state.hardware;
|
||||
const devices=cachedHardware()?.gpus??hwCache?.gpus??[];
|
||||
const memoryPools=Math.max(1,devices.filter(g=>!g.unified).length+Number(devices.some(g=>g.unified)));
|
||||
const readings=demo?devices.map((gpu,i)=>({id:gpu.id??`${gpu.vendor}:${gpu.index}`,source:'demo',sampledAt:new Date().toISOString(),utilizationPct:Math.round(clamp(hw.gpuLoad+i*2,0,100)),temperatureC:Math.round(hw.gpuTempC+i),memoryUsedGb:round(Math.min(gpu.vramGb,hw.vramUsedGb/memoryPools),2),powerW:gpu.unified?null:round(35+hw.gpuLoad*2,0),powerLimitW:gpu.unified?null:300,pcieGen:gpu.unified?null:4,pcieGenMax:gpu.unified?null:4,pcieWidth:gpu.unified?null:16,pcieRxMbs:gpu.unified?null:round(hw.gpuLoad*1.2,1)})):hw.gpus.map(g=>({...g}));
|
||||
const readings=demo?devices.map((gpu,i)=>({id:gpu.id??`${gpu.vendor}:${gpu.index}`,source:'demo',sampledAt:new Date().toISOString(),utilizationPct:Math.round(clamp(hw.gpuLoad+i*2,0,100)),temperatureC:Math.round(hw.gpuTempC+i),memoryUsedGb:round(Math.min(gpu.vramGb,hw.vramUsedGb/memoryPools),2),powerW:gpu.unified?null:round(35+hw.gpuLoad*2,0),powerLimitW:gpu.unified?null:300,pcieGen:gpu.unified?null:4,pcieGenMax:gpu.unified?null:4,pcieWidth:gpu.unified?null:16,pcieRxMbs:gpu.unified?null:round(hw.gpuLoad*1.2,1),fanPct:gpu.unified?null:Math.round(30+hw.gpuLoad*0.5),clockMhz:gpu.unified?null:Math.round(1400+hw.gpuLoad*14)})):hw.gpus.map(g=>({...g}));
|
||||
const memory = gpuMemoryTotals(devices,readings);
|
||||
return {
|
||||
gpus:readings,cpus:systemReading?.cpus??[],cpuLoad:systemReading?.cpuLoad??null,
|
||||
@@ -81,7 +94,9 @@ function hardwareSnapshot() {
|
||||
ramUsedGb:systemReading?.ramUsedGb??null,ramTotalGb:systemReading?.ramTotalGb??hw.ramTotalGb,
|
||||
gpuTempC:aggregate(readings,'temperatureC','max'),tempLimitC:hw.tempLimitC,
|
||||
powerW:aggregate(readings,'powerW'),powerLimitW:aggregate(readings,'powerLimitW'),
|
||||
pcieRxMbs:aggregate(readings,'pcieRxMbs'),diskReadGbs:systemReading?.diskReadGbs??null,diskWriteMbs:systemReading?.diskWriteMbs??null,
|
||||
pcieRxMbs:aggregate(readings,'pcieRxMbs'),
|
||||
cpuTempC:demo?54:temperatures.cpuTempC,ssdTempC:demo?41:temperatures.ssdTempC,energyWh:round(energyWh(),3),
|
||||
diskReadGbs:systemReading?.diskReadGbs??null,diskWriteMbs:systemReading?.diskWriteMbs??null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -108,14 +123,16 @@ function approach(current, target, factor = 0.22) {
|
||||
}
|
||||
|
||||
function tick() {
|
||||
if(!sensorsPending&&Date.now()-sensorsAt>=3000){
|
||||
const pace=sensorPace({live:supervisor.status().running||!!liveProbe(),watchedUntil});
|
||||
if(!sensorsPending&&Date.now()-sensorsAt>=pace.intervalMs){
|
||||
sensorsPending=true;sensorsAt=Date.now();
|
||||
const hardware=cachedHardware()??hwCache;
|
||||
void Promise.all([readGpuTelemetry(hardware?.gpus??[]),sampleSystem(hardware?.processors??[])])
|
||||
.then(([readings,system])=>{state.hardware.gpus=readings;systemReading=system;})
|
||||
void Promise.all([readGpuTelemetry(hardware?.gpus??[],{transfer:pace.transfer}),sampleSystem(hardware?.processors??[]),(process.env.STRATA_REAL_ENGINE==='1'?readTemperatures():Promise.resolve(temperatures)).catch(()=>({cpuTempC:null,ssdTempC:null}))])
|
||||
.then(([readings,system,temps])=>{state.hardware.gpus=readings;systemReading=system;temperatures=temps;})
|
||||
.catch(()=>{state.hardware.gpus=[];systemReading=null;})
|
||||
.finally(()=>{sensorsPending=false;});
|
||||
}
|
||||
addEnergy(hardwareSnapshot().powerW,Date.now());
|
||||
const hw = state.hardware;
|
||||
const busy = state.phase === 'generating' || state.phase === 'reading' || state.phase === 'thinking';
|
||||
|
||||
|
||||
@@ -7,22 +7,23 @@ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, realpa
|
||||
import { join, resolve, dirname, basename, sep, relative, isAbsolute } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createServer } from 'node:net';
|
||||
import { NATIVE_FIELDS, APP_OWNED_FIELDS, withoutDraftLayer, validateNativeSettings, validateMcpServers, publicMcpServers, publicNativeEnv, restoreNativeEnv } from './native-settings.mjs';
|
||||
import { NATIVE_FIELDS, APP_OWNED_FIELDS, withoutDraftLayer, validateNativeSettings, validateMcpServers, publicMcpServers, publicNativeEnv, restoreNativeEnv, speedSwitchRows } from './native-settings.mjs';
|
||||
import { resourceSettings, saferStart, reserveChoice } from './native-resources.mjs';
|
||||
import { USER_KEY, capture as captureUserArgs, merge as mergeUserArgs, setFlag as setUserFlag, stamped as stampedUserArgs } from './native-user-args.mjs';
|
||||
import { resolvePython, venvPython } from './python-resolver.mjs';
|
||||
import os from 'node:os';
|
||||
import { runtimeVision } from './native-vision.mjs';
|
||||
import { componentsDirectory, installerEnvironment, controlEnvironment } from './control-runtime.mjs';
|
||||
import { componentsDirectory, installerEnvironment, controlEnvironment, sourceServerEnvironment } from './control-runtime.mjs';
|
||||
import { createEngineStep } from './engine-step.mjs';
|
||||
import { dataFolderFromPick, isDriveRoot, makeFolder, rememberDataFolder, syncDataFolder, terminalSettingsFile } from './model-folder.mjs';
|
||||
import { createAutoTune, writeTuneConfig } from './native-autotune.mjs';
|
||||
import { plainError, PHASE_TEXT } from './update-messages.mjs';
|
||||
import { checkRunConfig, describeIssues } from './run-config-check.mjs';
|
||||
import { checkAndParseRunConfig, checkRunConfig, describeIssues } from './run-config-check.mjs';
|
||||
import { openTextFile } from './open-text-file.mjs';
|
||||
import { markEngine, engineRefusal } from './engine-version.mjs';
|
||||
import { friendlyModelName } from './model-names.mjs';
|
||||
import { parseSaved as parseRoles, mirror as mirrorRoles } from './gpu-roles.mjs';
|
||||
import { configContextLimit } from './dsv41-context.mjs';
|
||||
|
||||
const ROOT=resolve(dirname(fileURLToPath(import.meta.url)),'..');
|
||||
const safeId=value=>typeof value==='string'&&/^[a-z0-9](?:[a-z0-9_-]|\.(?!\.)){0,79}$/i.test(value); /* a dot inside the name: deepseek-v4.1-flash-ist-das-2.00bpw */
|
||||
@@ -86,7 +87,7 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
for(const name of names){const path=join(dir,name),config=readJson(path,null);if(!config||typeof config!=='object')continue;
|
||||
const next={...config,...(config.cwd!==undefined?{cwd:fix(config.cwd)}:{}),...(Array.isArray(config.args)?{args:config.args.map(fix)}:{})};
|
||||
if(JSON.stringify(next)!==JSON.stringify(config)){try{writeModelConfig(path,next);}catch{/* read-only folder: the load reports it */}}}}
|
||||
function installedInfo(info){if(!info)return info;return {...info,models:(info.models??[]).map(entry=>{const cfg=/^strata-[a-z0-9_.-]+\.json$/i.test(entry.config??'')?readJson(entry.configPath??join(repo(),entry.config)):{};const at=cfg.args?.indexOf('--native')??-1,nativeFile=at>=0?cfg.args[at+1]:null;const dataFolder=typeof nativeFile==='string'&&basename(dirname(dirname(nativeFile)))==='models'?dirname(dirname(dirname(nativeFile))):info.data_folder;/* A model the person pointed at their own GGUF folder (not <data>/models/<tag>): remembered, so a later load runs setup with that folder again instead of downloading the files (#3199). */const ownGgufDir=typeof nativeFile==='string'&&basename(dirname(dirname(nativeFile)))!=='models'&&existsSync(nativeFile)?dirname(nativeFile):undefined;return {...entry,ownGgufDir,diskGb:entry.diskGb??entry.disk_gb,diskBytes:entry.diskBytes??entry.disk_bytes,sharedBytes:entry.sharedBytes??entry.shared_bytes,removableGb:entry.removableGb??entry.removable_gb,dataFolder,visionPrepared:!!cfg.vision,/* The picture files are prepared when the run config has a vision section: the Models screen then loads straight away instead of running setup again (it used to read 'off' for every model, which sent every load of a picture-capable model through "Apply settings and load", and setup writes the arguments, the draft layer and the reader back each time). */images:cfg.vision?'yes':'off',draftLayer:cfg.mtp!==false,drafting:cfg.drafting??'auto',/* Picture reading is ON unless the person turned it off: no saved choice and not loaded is undefined, not false (owner rule: pictures on by default). */visionEnabled:cfg.ui_vision_enabled??(()=>{const running=[...instances.values()].find(i=>i.model===entry.model&&i.status==='ready');return running?!!running.vision:undefined;})(),visionDevice:cfg.ui_vision_device,resourceProfile:cfg.ui_resource_profile,lowRamMode:cfg.args?.includes('--resident-experts')?'resident':cfg.args?.includes('--mmap-experts')?'mmap':'auto'};})};}
|
||||
function installedInfo(info){if(!info)return info;return {...info,models:(info.models??[]).map(entry=>{const cfg=/^strata-[a-z0-9_.-]+\.json$/i.test(entry.config??'')?readJson(entry.configPath??join(repo(),entry.config)):{};const at=cfg.args?.indexOf('--native')??-1,nativeFile=at>=0?cfg.args[at+1]:null;const dataFolder=typeof nativeFile==='string'&&basename(dirname(dirname(nativeFile)))==='models'?dirname(dirname(dirname(nativeFile))):info.data_folder;/* A model the person pointed at their own GGUF folder (not <data>/models/<tag>): remembered, so a later load runs setup with that folder again instead of downloading the files (#3199). */const ownGgufDir=typeof nativeFile==='string'&&basename(dirname(dirname(nativeFile)))!=='models'&&existsSync(nativeFile)?dirname(nativeFile):undefined;return {...entry,ownGgufDir,contextMax:configContextLimit(cfg)??undefined,diskGb:entry.diskGb??entry.disk_gb,diskBytes:entry.diskBytes??entry.disk_bytes,sharedBytes:entry.sharedBytes??entry.shared_bytes,removableGb:entry.removableGb??entry.removable_gb,dataFolder,visionPrepared:!!cfg.vision,/* The picture files are prepared when the run config has a vision section: the Models screen then loads straight away instead of running setup again (it used to read 'off' for every model, which sent every load of a picture-capable model through "Apply settings and load", and setup writes the arguments, the draft layer and the reader back each time). */images:cfg.vision?'yes':'off',draftLayer:cfg.mtp!==false,drafting:cfg.drafting??'auto',/* Picture reading is ON unless the person turned it off: no saved choice and not loaded is undefined, not false (owner rule: pictures on by default). */visionEnabled:cfg.ui_vision_enabled??(()=>{const running=[...instances.values()].find(i=>i.model===entry.model&&i.status==='ready');return running?!!running.vision:undefined;})(),visionDevice:cfg.ui_vision_device,resourceProfile:cfg.ui_resource_profile,lowRamMode:cfg.args?.includes('--resident-experts')?'resident':cfg.args?.includes('--mmap-experts')?'mmap':'auto'};})};}
|
||||
function installRoot(){const found=[preferences.installDir].find(d=>d&&existsSync(d));if(found)return realpathSync(found);mkdirSync(defaultInstall(),{recursive:true});return realpathSync(defaultInstall());}
|
||||
/* The settings file Strata Control and the installer read (control-runtime.mjs sets STRATA_SETTINGS_FILE to it): the app's data folder is written there. */
|
||||
function controlSettingsFile(){return process.env.STRATA_SETTINGS_FILE||join(dirname(componentsDirectory()),'app-settings.json');}
|
||||
@@ -137,7 +138,7 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
if(closing)return state();
|
||||
/* A read that is not forced never makes the window wait for the (slow) status of Control: it gets the last state at once, and a stale one
|
||||
is refreshed in the background. Only the first read, and a forced one after an operation, waits. */
|
||||
if(refreshing)return !force&&snapshot.available?state():refreshing;if(!force&&Date.now()-lastRead<45000)return state();
|
||||
if(refreshing)return !force&&snapshot.available?state():force?refreshing.then(()=>refresh(true)):refreshing; /* a forced read that meets one already running (started before an engine update finished) waits for it and reads again, so it never settles on the old answer */if(!force&&Date.now()-lastRead<45000)return state();
|
||||
probeDrafting();
|
||||
refreshing=(async()=>{try{const [catalog,status]=packaged?await (async()=>{const s=await tool('strata_status',{hardware:true});const c=await tool('strata_models').catch(error=>({families:[],contexts:[],recommendation:{family:null,model:null},context_note:'',error:error.message}));return [c,s];})():await Promise.all([tool('strata_models'),tool('strata_status',{hardware:true})]);let visionReader=false;if(packaged){try{const e=await control.engine();const root=e?.engine?.root??(e?.engine?.exe?dirname(e.engine.exe):'');visionReader=!!root&&['strata-vision','strata-vision.exe'].some(n=>existsSync(join(root,n)));}catch{visionReader=false;}}const markImages=c=>c&&Array.isArray(c.families)?{...c,families:c.families.map(f=>({...f,imagesCapable:!!f.images,images:!!f.images&&visionReader,sizes:(f.sizes??[]).map(s=>({...s,imagesCapable:!!s.images,images:!!s.images&&visionReader}))}))}:c;snapshot={available:true,repo:packaged?status.installDir:repo(),version:packaged?(control.version??null):await execute('git',['describe','--tags','--always'],repo()).then(s=>s.trim()).catch(()=>null),summary:status.summary,catalog:markEngine(packaged?markImages(catalog):catalog,status.engine),engineInfo:{version:status.engine?.version??null,source:status.engine?.source??null},visionReader:packaged?visionReader:true,installed:installedInfo(status.installed),hardware:status.hardware,error:null,install:status.install,external:status.running??null,...(packaged?{packaged:true,installDir:status.installDir,engine:status.engine,installerError:status.installerError}:{})};lastRead=Date.now();if(status.install?.running&&![...jobs.values()].some(j=>j.state==='running'))begin('install',j=>watchInstall(j,status.install));}catch(error){snapshot={...snapshot,available:false,error:error.message};lastRead=Date.now();}emit();return state();})().finally(()=>{refreshing=null;});const run=refreshing;if(!force&&snapshot.available){run.catch(()=>{});return state();}return run;
|
||||
}
|
||||
@@ -149,8 +150,8 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
function settingsFile(model){const {path}=recordFile(model);return {model,path,name:basename(path),...checkRunConfig(readFileSync(path,'utf8')),note:'Changes you make in the file apply the next time the model starts.'};}
|
||||
async function openSettingsFile(model,options={}){const info=settingsFile(model);await openTextFile(info.path,options);return info;}
|
||||
/* A mistake in the file holds the start back with the line, instead of an engine that dies on it. Warnings (an unknown key) never do. */
|
||||
function assertSettingsFile(path){const check=checkRunConfig(readFileSync(path,'utf8'));if(!check.ok)throw new Error(`The settings file for this model has a mistake, so the model was not started. ${describeIssues(check.issues,'error').join(' ')}`);}
|
||||
function settings(model){const {config}=record(model);const values=Object.fromEntries(NATIVE_FIELDS.map(f=>[f.id,config[f.id]??null]));if(typeof values.aliases==='string')values.aliases=values.aliases.split(',').map(v=>v.trim()).filter(Boolean);values.sampling=config.sampling??{};values.args=config.args??[];values.env=publicNativeEnv(config.env??{});return {model,values,mcp:publicMcpServers(config.mcp_servers??config.mcpServers??{}),restartNeeded:true,capabilities:{dsExperts:ownEngine(config)&&dsExpertsSupported(config)}};}
|
||||
function checkedSettingsConfig(text){const check=checkAndParseRunConfig(text);if(!check.ok)throw new Error(`The settings file for this model has a mistake, so the model was not started. ${describeIssues(check.issues,'error').join(' ')}`);return check.config;}
|
||||
function settings(model){const {entry,config}=record(model);const values=Object.fromEntries(NATIVE_FIELDS.map(f=>[f.id,config[f.id]??null]));if(typeof values.aliases==='string')values.aliases=values.aliases.split(',').map(v=>v.trim()).filter(Boolean);values.sampling=config.sampling??{};values.args=config.args??[];values.env=publicNativeEnv(config.env??{});return {model,values,mcp:publicMcpServers(config.mcp_servers??config.mcpServers??{}),restartNeeded:true,capabilities:{dsExperts:ownEngine(config)&&dsExpertsSupported(config)},speedSwitches:speedSwitchRows(config.env??{},{deepseek:ownEngine(config),arch:config.arch??null,backend:config.backend??entry?.backend??snapshot.installed?.engine?.backend??null,gpus:snapshot.hardware?.gpus??[]})};}
|
||||
function save(model,{values,mcp}){if([...jobs.values()].some(j=>j.state==='running'))throw new Error('Finish the current operation before saving native settings.');const {path,config}=record(model);if(busy(requests().filter(r=>instances.get(r.instanceId)?.model===model)))throw new Error('Finish this model’s requests before saving its settings.');const patch=validateNativeSettings(values??{}),next={...config};if(patch.ds_experts!=null&&!dsExpertsSupported(config))throw new Error('The Speed choice needs the newer DeepSeek engine. Update the engine in Settings, then choose it.');for(const field of nativeFields())if(field.supported===false&&patch[field.id]!=null&&JSON.stringify(patch[field.id])!==JSON.stringify(config[field.id]))throw new Error(`${field.label} needs a newer native Strata installation. Update it in Settings → About.`);for(const [key,value] of Object.entries(patch)){if(key==='env')next.env=restoreNativeEnv(value,config.env??{});else if(value===null)delete next[key];else next[key]=value;}if('vram_reserve_mib' in patch){const base=captureUserArgs(next);const mine=setUserFlag(base,'--vram-reserve-mib',patch.vram_reserve_mib);if(mine.length)next[USER_KEY]=mine;else delete next[USER_KEY];next.args=mine.length&&patch.vram_reserve_mib!==null?mergeUserArgs(next.args??[],mine):setUserFlag(next.args??[],'--vram-reserve-mib',null);delete next.ui_reserve_applied;if(Array.isArray(next.args_written))next.args_written=[...next.args];}if(mcp!==undefined){next.mcp_servers=validateMcpServers(mcp,config.mcp_servers??config.mcpServers??{});delete next.mcpServers;}copyFileSync(path,path+'.bak');chmodSync(path+'.bak',0o600);const temp=path+'.ui-tmp';writeFileSync(temp,JSON.stringify(next,null,2)+'\n',{mode:0o600});renameSync(temp,path);return settings(model);}
|
||||
function previousEngine(){if(packaged)return !!snapshot.engine?.previous;try{const data=snapshot.installed?.data_dir??snapshot.installed?.data_folder;return [join(preferences.repo,'engine/.previous'),data&&join(data,'engine/.previous')].filter(Boolean).some(existsSync)&&readFileSync(join(preferences.repo,'setup.py'),'utf8').includes('--rollback-engine');}catch{return false;}}
|
||||
async function plan(args){await refresh();if(!snapshot.available)throw new Error(snapshot.error);const result=await tool('strata_install',installArgs(args,false));
|
||||
@@ -275,7 +276,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
/* Which drafter runs, when it is not the one that was asked for (the engine's own words, "Drafting: MTP (DFlash2 didn't fit)"): under Details of the load. */
|
||||
async function noteDrafting(job,instance,key){let note='';try{const r=await fetcher(`http://127.0.0.1:${instance.port}/metrics`,{headers:{authorization:`Bearer ${key}`},signal:AbortSignal.timeout(2000)});if(r.ok)note=String((await r.json())?.engine?.drafting_note??'');}catch{/* a convenience */}const lines=[job.drafterNote,note].filter(Boolean);if(lines.length)job.details=[job.details,...lines].filter(Boolean).join('\n');}
|
||||
async function loadEngine(job,args){
|
||||
await refresh(true);if(job.state!=='running')return;const {config,entry,path:configFile}=record(args.model);assertSettingsFile(configFile);if(packaged){const engine=await control.engine();if(!engine.usable)throw new Error(ENGINE_NOT_RECOGNISED+(engine.notice?` ${engine.notice}`:''));}if(!entry.ready){const error=missingFilesError(args.model,entry.missing);append(args.model,error.details[0]);throw error;}
|
||||
await refresh(true);if(job.state!=='running')return;const {entry,path:configFile}=recordFile(args.model);const config=checkedSettingsConfig(readFileSync(configFile,'utf8'));if(packaged){const engine=await control.engine();if(!engine.usable)throw new Error(ENGINE_NOT_RECOGNISED+(engine.notice?` ${engine.notice}`:''));}if(!entry.ready){const error=missingFilesError(args.model,entry.missing);append(args.model,error.details[0]);throw error;}
|
||||
if(externalRunning().length)throw new Error('A Strata server is already running outside this UI. Stop it before loading here, or connect its API in Settings → Models.');
|
||||
if(args.multiple&&instances.size>=8)throw new Error('Unload an instance before adding another. Up to eight native instances can run here.');
|
||||
const {cfg,resources,visionPreferences,parallelChoice,parallelNote:chosenNote}=buildConfig(args,config);
|
||||
@@ -328,7 +329,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
try{await startWith(retryConfig);append(id,'Strata Control reports the engine is ready (second try).');}
|
||||
catch(second){const said2=humanStartError(second);instance.status='error';instance.error=said2;/* the first stop and the second try stay in job.details; the pop-up shows the last stop once */emit();throw new Error(instance.error);}}}
|
||||
} else {
|
||||
proc=spawnProcess(py,[join(repo(),'serve/server.py'),'--engine','strata','--config',configPath,'--port',String(cfg.port),'--host','127.0.0.1'],{cwd:repo(),detached:process.platform!=='win32',stdio:['ignore','pipe','pipe'],env:{...process.env,PYTHONUNBUFFERED:'1',PYTHONUTF8:'1',PYTHONIOENCODING:'utf-8',STRATA_API_KEY:''}});
|
||||
proc=spawnProcess(py,[join(repo(),'serve/server.py'),'--engine','strata','--config',configPath,'--port',String(cfg.port),'--host','127.0.0.1'],{cwd:repo(),detached:process.platform!=='win32',stdio:['ignore','pipe','pipe'],env:sourceServerEnvironment()});
|
||||
instances.set(id,instance);processes.set(id,proc);if(!defaultId||!args.multiple||wasDefault)defaultId=id;
|
||||
for(const stream of [proc.stdout,proc.stderr])stream.on('data',chunk=>{const lines=chunk.toString().split('\n').filter(Boolean);lines.forEach(line=>append(id,line.replaceAll(key,'[redacted]')));job.summary=lines.at(-1)??job.summary;emit();});
|
||||
proc.on('error',error=>{instance.status='error';instance.error=error.message;emit();});proc.on('exit',code=>{if(instances.has(id)&&instance.status!=='unloading'){instance.status='error';instance.error=`Engine stopped${code===null?'':` (exit ${code})`}. Open diagnostics or reload.`;emit();}});emit();
|
||||
|
||||
@@ -26,6 +26,64 @@ export const NATIVE_FIELDS=[
|
||||
{id:'mcp_max_rounds',label:'Maximum tool rounds',group:'Tools',type:'number',min:1,max:100},
|
||||
{id:'mcp_max_result_chars',label:'Maximum tool result characters',group:'Tools',type:'number',min:100,max:1000000},
|
||||
];
|
||||
/** Engine environment names the app shows and edits: STRATA_* and the DeepSeek engine's DS4_*. Any other name in a model's file is kept untouched and not shown. */
|
||||
export const ENV_FLAG=/^(?:STRATA|DS4)_[A-Z0-9_]+$/;
|
||||
/**
|
||||
* The speed features of 0.2.7 that have a switch in Expert settings. A switch has a default ('on' or 'off'); outside it, it writes exactly the
|
||||
* environment entries `env` into the model's settings file ("env"); back in its default it removes them, so a default launch carries none of them.
|
||||
* "Faster long prompts" (Qwen KVA) is OFF by default (owner decision, 26 October): STRATA_KVA=1 turns it on, and an earlier test build's STRATA_KVA=0 reads as off.
|
||||
* The other two are on by default. The names are the engine switch contract (phaseA-tests/notes-027-plan.md); strata_control/speed_switches.py says the
|
||||
* same for the Terminal (tests/terminal/test_speed_switches.py compares the two).
|
||||
*/
|
||||
export const SPEED_SWITCHES=[
|
||||
{id:'faster_long_prompts',label:'Faster long prompts',group:'Speed',
|
||||
hint:'Reads very long prompts faster by approximating part of the work. Off by default: answers about long documents can miss or mix up details.',
|
||||
default:'off',env:{STRATA_KVA:'1'}},
|
||||
{id:'faster_12gb_nvidia',label:'Faster answers on 12 GB NVIDIA cards',group:'Speed',
|
||||
hint:'Prefers the parts of the model already on the graphics card, which makes answers much faster on 12 GB cards. Turn off if answers look worse than before.',
|
||||
default:'on',env:{STRATA_ROUTE_PRIOR:'0'}},
|
||||
{id:'faster_radeon_9000',label:'Faster answers on Radeon RX 9000 / AI PRO R9700',group:'Speed',
|
||||
hint:'Shares the work between the graphics card and the processor in a way that makes answers faster on these cards. Turn off to go back to the previous setting, which is slower.',
|
||||
default:'on',env:{STRATA_PCIE_FRAC_DEFAULT:'old'}},
|
||||
{id:'deepseek_speedups',label:'DeepSeek speed-ups',group:'Speed',
|
||||
hint:'Reads long prompts faster and loads the parts of the model it needs faster. Turn off if DeepSeek stops working or its answers look wrong; it then runs as in version 0.2.6.',
|
||||
default:'on',env:{DS4_DEFAULTS:'026',DS4_FAST_ATTN:'0',DS4_ATTN_SLAB:'0',DS4_COPY_PIN:'0',DS4_COPY_HUGETLB:'0',DS4_STORE_SPARE:'0',DS4_STORE_PUT:'0'},
|
||||
/* an earlier test build wrote only these six for the same state: they read as the switch being set, and go with it */
|
||||
legacy:{DS4_FAST_ATTN:'0',DS4_ATTN_SLAB:'0',DS4_COPY_PIN:'0',DS4_COPY_HUGETLB:'0',DS4_STORE_SPARE:'0',DS4_STORE_PUT:'0'}},
|
||||
];
|
||||
const GFX12=/^gfx120[01]$/i, RADEON_9000=/\b(?:RX\s*9\d{3}|AI\s*PRO\s*R97\d{2}|R97\d{2})\b/i;
|
||||
/**
|
||||
* Which switches make sense for this model on this PC: only those are shown. `backend` is the engine's (cuda, hip, sycl, metal, cpu). `arch` is the "arch"
|
||||
* of the model's run config: setup writes it only for the families with an engine path of their own (Qwen3.6-35B-A3B "qwen35moe", DeepSeek "deepseek41");
|
||||
* the Qwen3.8-Flash-Next files (the original, Swift, Coder, Unsloth, Gyro, NVFP4) have none. KVA was measured on the Flash-Next shape (48 layers, 512
|
||||
* experts) and the engine declines it on any other, so "Faster long prompts" is shown for a model without an "arch" only.
|
||||
*/
|
||||
/** A discrete NVIDIA card of 13 GiB or less: the engine's rule for residency-biased routing (not an integrated chip, not a card that shares system memory). */
|
||||
const smallNvidia=gpus=>(gpus??[]).some(g=>{
|
||||
if(g.usable===false||g.uma||g.integrated||(g.unified_gb??0)>0)return false;
|
||||
const vendor=String(g.vendor??'').toLowerCase();
|
||||
if(vendor!=='nvidia'&&!(vendor===''&&/nvidia|geforce|\brtx\b|\bgtx\b|quadro|tesla/i.test(String(g.name??''))))return false;
|
||||
return g.vram_gb>0&&g.vram_gb<=13;
|
||||
});
|
||||
export function speedSwitchApplies(id,{deepseek=false,backend=null,gpus=[],arch=null}={}){
|
||||
if(id==='deepseek_speedups')return !!deepseek;
|
||||
if(deepseek)return false; // the Qwen engine's switches
|
||||
if(id==='faster_long_prompts')return !arch&&(backend==='cuda'||backend==='hip');
|
||||
if(id==='faster_12gb_nvidia')return !arch&&backend==='cuda'&&smallNvidia(gpus); // the engine's routing needs the Flash-Next shape (512 experts, top-10) and a CUDA build
|
||||
if(id==='faster_radeon_9000')return backend==='hip'&&(gpus??[]).some(g=>(!g.vendor||g.vendor==='amd')&&(GFX12.test(String(g.arch??''))||(!g.arch&&RADEON_9000.test(String(g.name??'')))));
|
||||
return false;
|
||||
}
|
||||
/** The switch is outside its default when every entry it writes is in the environment with its value; anything less or other (a hand-edited file, STRATA_KVA=0) is the default state and is left alone. */
|
||||
export function speedSwitchIsSet(env,id){const sw=SPEED_SWITCHES.find(s=>s.id===id),has=entries=>!!entries&&Object.keys(entries).length>0&&Object.entries(entries).every(([name,value])=>env?.[name]===value);return !!sw&&(has(sw.env)||has(sw.legacy));}
|
||||
/** Is the feature on for a launch with this environment? */
|
||||
export function speedSwitchIsOn(env,id){const sw=SPEED_SWITCHES.find(s=>s.id===id);return !!sw&&speedSwitchIsSet(env,id)!==(sw.default==='on');}
|
||||
/** The environment with the feature on or off: its default removes the switch's entries, the other state writes them. Nothing else in it changes. */
|
||||
export function setSpeedSwitch(env,id,on){
|
||||
const sw=SPEED_SWITCHES.find(s=>s.id===id);if(!sw)throw new Error(`Unknown speed switch: ${id}`);
|
||||
const write=on!==(sw.default==='on'),next={...(env??{})};for(const [name,value] of Object.entries(sw.env)){if(write)next[name]=value;else delete next[name];}if(!write)for(const name of Object.keys(sw.legacy??{}))delete next[name];return next;
|
||||
}
|
||||
/** What the settings answer carries: each switch that applies, with its words, its default, the entries it writes outside the default, and whether the feature is on. */
|
||||
export function speedSwitchRows(env,context){return SPEED_SWITCHES.filter(s=>speedSwitchApplies(s.id,context)).map(s=>({...s,on:speedSwitchIsOn(env,s.id)}));}
|
||||
/** Settings the app itself applies when it starts a model (they are not read from the engine's source, so the engine's text never says it supports them). */
|
||||
export const APP_OWNED_FIELDS=['mtp','batchMtp','drafting','ds_experts'];
|
||||
/** The engine arguments without the draft layer: --mtp <folder or count>, --spec-min-p <p> (its confidence threshold) and --batch-mtp. --spec stays: prompt lookup still uses its windows. Same rule as strata_control/draft_layer.py. */
|
||||
@@ -40,7 +98,7 @@ export function validateNativeSettings(values){
|
||||
for(const [key,value] of Object.entries(values)){
|
||||
|
||||
if(key==='args'){if(!Array.isArray(value)||value.length>200||value.some(v=>typeof v!=='string'||v.length>4000||/[\n\r\0]/.test(v))||value.some(v=>['--host','--port','--api-key'].includes(v)))throw new Error('Engine arguments must be text values. Network access stays managed by Strata.');result.args=[...value];continue;}
|
||||
if(key==='env'){if(!value||typeof value!=='object'||Array.isArray(value))throw new Error('Engine environment must be an object.');const env={};for(const [name,text] of Object.entries(value)){if(!/^STRATA_[A-Z0-9_]+$/.test(name)||typeof text!=='string'||text.length>4000||text.includes('\0'))throw new Error('Use STRATA_ environment flags with text values.');env[name]=text;}result.env=env;continue;}
|
||||
if(key==='env'){if(!value||typeof value!=='object'||Array.isArray(value))throw new Error('Engine environment must be an object.');const env={};for(const [name,text] of Object.entries(value)){if(!ENV_FLAG.test(name)||typeof text!=='string'||text.length>4000||text.includes('\0'))throw new Error('Use STRATA_ environment flags with text values.');env[name]=text;}result.env=env;continue;}
|
||||
if(key==='sampling'){if(!value||typeof value!=='object'||Array.isArray(value))throw new Error('Sampling must be an object.');const limits={temperature:[0,10],top_p:[Number.MIN_VALUE,1],top_k:[1,64],min_p:[0,1],repetition_penalty:[Number.MIN_VALUE,10],presence_penalty:[0,10],frequency_penalty:[0,10],penalty_last_n:[0,1048576],seed:[1,2147483647]};for(const [name,n] of Object.entries(value)){if(name==='experimental_speed_projection'){if(typeof n!=='boolean')throw new Error('Speed projection must be on or off.');continue;}if(!limits[name]||!Number.isFinite(n)||(['top_k','seed','penalty_last_n'].includes(name)&&!Number.isInteger(n))||n<limits[name][0]||n>limits[name][1])throw new Error(`Choose a valid sampling ${name}.`);}result.sampling={...value};continue;}
|
||||
const field=NATIVE_FIELDS.find(f=>f.id===key);if(!field)throw new Error(`Unknown native setting: ${key}`);
|
||||
if(value===null||value===''){result[key]=null;continue;}
|
||||
@@ -75,5 +133,5 @@ export function validateMcpServers(servers,previous={}){
|
||||
}
|
||||
|
||||
const sensitive=name=>/KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL/i.test(name);
|
||||
export function publicNativeEnv(env){return Object.fromEntries(Object.entries(env).filter(([name])=>/^STRATA_[A-Z0-9_]+$/.test(name)).map(([name,value])=>[name,sensitive(name)?MASK:value]));}
|
||||
export function restoreNativeEnv(env,previous={}){const result=Object.fromEntries(Object.entries(previous).filter(([name])=>!/^STRATA_[A-Z0-9_]+$/.test(name)));for(const [name,value] of Object.entries(env)){if(value===MASK){if(previous[name]===undefined)throw new Error(`Enter a new value for ${name}.`);result[name]=previous[name];}else result[name]=value;}return result;}
|
||||
export function publicNativeEnv(env){return Object.fromEntries(Object.entries(env).filter(([name])=>ENV_FLAG.test(name)).map(([name,value])=>[name,sensitive(name)?MASK:value]));}
|
||||
export function restoreNativeEnv(env,previous={}){const result=Object.fromEntries(Object.entries(previous).filter(([name])=>!ENV_FLAG.test(name)));for(const [name,value] of Object.entries(env)){if(value===MASK){if(previous[name]===undefined)throw new Error(`Enter a new value for ${name}.`);result[name]=previous[name];}else result[name]=value;}return result;}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* The computer's regional format, for the Monitor's cost line (#3628): which region and, where the system names it, which currency the
|
||||
* person uses. Read once, without PowerShell:
|
||||
* Windows the regional format (Settings > Time & language > Region > Regional format) from the registry: LocaleName ("en-US"); when it
|
||||
* names no region, the country or region setting (Geo\Name).
|
||||
* macOS `defaults read -g AppleLocale` ("en_US", or "de_DE@currency=CHF" when the person set another currency).
|
||||
* Linux LC_ALL, LC_MONETARY or LANG ("de_DE.UTF-8"), and the locale's own currency when `locale -k int_curr_symbol` answers.
|
||||
* Nothing here is a guess: no region means no answer, and the page then falls back to its own language list.
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process';
|
||||
|
||||
const NONE = Object.freeze({ locale: null, region: null, currency: null, source: null });
|
||||
|
||||
/** "en_US.UTF-8" / "de_DE@euro" / "zh-Hans-CN" -> { locale: "en-US", region: "US" } (null parts when the tag has none). */
|
||||
export function parseLocaleTag(text) {
|
||||
const tag = String(text ?? '').trim().replace(/[.@].*$/, '').replace(/_/g, '-');
|
||||
if (!tag || /^(c|posix)$/i.test(tag)) return { locale: null, region: null };
|
||||
const parts = tag.split('-');
|
||||
const region = parts.slice(1).find(p => /^[A-Za-z]{2}$/.test(p)) ?? null;
|
||||
const language = /^[A-Za-z]{2,3}$/.test(parts[0]) ? parts[0].toLowerCase() : null;
|
||||
if (!language) return { locale: null, region: null };
|
||||
return { locale: region ? `${language}-${region.toUpperCase()}` : language, region: region ? region.toUpperCase() : null };
|
||||
}
|
||||
|
||||
export function parseRegValue(out, name) {
|
||||
const m = new RegExp(`^\\s*${name}\\s+REG_SZ\\s+(.*?)\\s*$`, 'mi').exec(String(out ?? ''));
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
const currencyCode = value => (/^[A-Z]{3}$/.test(String(value ?? '').trim().toUpperCase()) ? String(value).trim().toUpperCase() : null);
|
||||
|
||||
export function detectRegion({ platform = process.platform, env = process.env, run = (cmd, args) => execFileSync(cmd, args, { encoding: 'utf8', timeout: 4000, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] }) } = {}) {
|
||||
try {
|
||||
if (platform === 'win32') {
|
||||
const intl = 'HKCU\\Control Panel\\International';
|
||||
const named = parseLocaleTag(parseRegValue(run('reg', ['query', intl, '/v', 'LocaleName']), 'LocaleName'));
|
||||
if (named.region) return { ...named, currency: null, source: 'windows' };
|
||||
let geo = null;
|
||||
try { geo = parseRegValue(run('reg', ['query', `${intl}\\Geo`, '/v', 'Name']), 'Name'); } catch { /* no country setting */ }
|
||||
const region = /^[A-Za-z]{2}$/.test(geo ?? '') ? geo.toUpperCase() : null;
|
||||
return region ? { locale: named.locale ? `${named.locale}-${region}` : null, region, currency: null, source: 'windows' } : { ...NONE };
|
||||
}
|
||||
if (platform === 'darwin') {
|
||||
const raw = String(run('defaults', ['read', '-g', 'AppleLocale'])).trim();
|
||||
const named = parseLocaleTag(raw);
|
||||
const currency = currencyCode(/@currency=([A-Za-z]{3})/.exec(raw)?.[1]);
|
||||
return named.region || currency ? { ...named, currency, source: 'macos' } : { ...NONE };
|
||||
}
|
||||
const named = parseLocaleTag(env.LC_ALL || env.LC_MONETARY || env.LANG);
|
||||
let currency = null;
|
||||
try { currency = currencyCode(/int_curr_symbol="?([A-Za-z]{3})/.exec(run('locale', ['-k', 'int_curr_symbol']))?.[1]); } catch { /* no locale command */ }
|
||||
return named.region || currency ? { ...named, currency, source: 'linux' } : { ...NONE };
|
||||
} catch {
|
||||
return { ...NONE };
|
||||
}
|
||||
}
|
||||
|
||||
let cached = null;
|
||||
/** The answer is cached: the regional format does not change while the host runs. */
|
||||
export function systemLocale(options) {
|
||||
if (options) return detectRegion(options);
|
||||
// A test or a demo can name the regional format (the screenshots of the Monitor); a packaged app never reads it.
|
||||
if (process.env.STRATA_TEST_LOCALE && process.env.STRATA_PACKAGED !== '1') { const named = parseLocaleTag(process.env.STRATA_TEST_LOCALE); return { ...named, currency: null, source: 'test' }; }
|
||||
cached ??= detectRegion();
|
||||
return cached;
|
||||
}
|
||||
@@ -134,6 +134,14 @@ export function checkRunConfig(text, { known = KNOWN_KEYS } = {}) {
|
||||
return { ok: !issues.some(i => i.level === 'error'), issues };
|
||||
}
|
||||
|
||||
/** Validate and parse one captured text value, so callers use the checked snapshot. */
|
||||
export function checkAndParseRunConfig(text, options) {
|
||||
text = String(text ?? '');
|
||||
const checked = checkRunConfig(text, options);
|
||||
if (!checked.ok) return { ...checked, config: null };
|
||||
return { ...checked, config: JSON.parse(text.replace(/^\uFEFF/, '')) };
|
||||
}
|
||||
|
||||
/** The English lines of the issues, `Line 12: …`, for the Details of a refused start. */
|
||||
export function describeIssues(issues, level) {
|
||||
return issues.filter(i => !level || i.level === level).map(i => `${i.line ? `Line ${i.line}: ` : ''}${i.message}`);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* How often the host asks the graphics card driver for its readings (#4066).
|
||||
* It used to start two nvidia-smi processes every 3 s for as long as the app was open, even with nothing loaded and
|
||||
* the Monitor out of sight; on some PCs that was felt as a stutter while typing. Now the 3 s pace is kept while a model
|
||||
* runs or the Monitor is being looked at, and an idle app asks once every 30 s.
|
||||
*/
|
||||
export const ACTIVE_MS = 3000;
|
||||
export const IDLE_MS = 30000;
|
||||
/** The Monitor screen asks again every 10 s while it is open; this is how long one ask keeps the fast pace. */
|
||||
export const WATCH_MS = 25000;
|
||||
|
||||
/** `live`: a model is loaded or an engine runs. `watchedUntil`: ms timestamp until which the Monitor was last seen open. */
|
||||
export function sensorPace({ live = false, watchedUntil = 0, now = Date.now() } = {}) {
|
||||
const watched = now < watchedUntil;
|
||||
return { intervalMs: live || watched ? ACTIVE_MS : IDLE_MS, transfer: !!live };
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* The desktop shell's own choices, kept in <Strata data folder>/shell-prefs.json (#3402, #3477): the shell reads the file when it starts
|
||||
* (the port it asks for, whether closing the window keeps Strata in the tray, the language of the tray menu), before any page exists.
|
||||
* The page changes them through the host (POST /api/shell/prefs); a change of the port or the tray applies the next time Strata starts.
|
||||
*
|
||||
* { "port": 0 | 1024..65535, "tray": false, "language": "en" }
|
||||
*
|
||||
* Port 0 is the default: the shell asks for 47831 and takes a free port when that one is busy.
|
||||
*/
|
||||
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { componentsDirectory } from './control-runtime.mjs';
|
||||
|
||||
export const DEFAULT_PORT = 47831;
|
||||
export const LANGUAGES = ['en', 'zh', 'ja', 'de', 'es', 'ru', 'pt', 'ko'];
|
||||
export const DEFAULTS = Object.freeze({ port: 0, tray: false, language: 'en' });
|
||||
|
||||
export const validPort = value => Number.isInteger(value) && value >= 1024 && value <= 65535;
|
||||
|
||||
export function shellPrefsFile({ env = process.env } = {}) {
|
||||
return join(dirname(componentsDirectory({ env })), 'shell-prefs.json');
|
||||
}
|
||||
|
||||
/** The file's content with every field checked; anything missing or wrong is the default. */
|
||||
export function cleanPrefs(raw) {
|
||||
const value = raw && typeof raw === 'object' ? raw : {};
|
||||
return {
|
||||
port: validPort(value.port) ? value.port : 0,
|
||||
tray: value.tray === true,
|
||||
language: LANGUAGES.includes(value.language) ? value.language : 'en',
|
||||
};
|
||||
}
|
||||
|
||||
export function readShellPrefs(file = shellPrefsFile()) {
|
||||
try { return cleanPrefs(JSON.parse(readFileSync(file, 'utf8').replace(/^/, ''))); } catch { return { ...DEFAULTS }; }
|
||||
}
|
||||
|
||||
/** Merge a change into the file. Throws a plain sentence for a port that cannot be used. */
|
||||
export function writeShellPrefs(patch, file = shellPrefsFile()) {
|
||||
const next = readShellPrefs(file);
|
||||
if (patch && 'port' in patch) {
|
||||
const wanted = patch.port === null || patch.port === '' || patch.port === 0 ? 0 : Number(patch.port);
|
||||
if (wanted !== 0 && !validPort(wanted)) throw new Error('Choose a port between 1024 and 65535, or leave it empty to let Strata choose.');
|
||||
next.port = wanted;
|
||||
}
|
||||
if (patch && 'tray' in patch) next.tray = patch.tray === true;
|
||||
if (patch && 'language' in patch && LANGUAGES.includes(patch.language)) next.language = patch.language;
|
||||
mkdirSync(dirname(file), { recursive: true });
|
||||
const temporary = `${file}.${process.pid}.tmp`;
|
||||
writeFileSync(temporary, JSON.stringify(next, null, 2) + '\n');
|
||||
renameSync(temporary, file);
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* What the shell tells the host about this start: STRATA_PORT_NOTICE is "busy:<wanted>:<used>" when the port it wanted was taken
|
||||
* (set by the shell, never by the person). The page turns it into a sentence in the person's language.
|
||||
*/
|
||||
export function parsePortNotice(value) {
|
||||
const m = /^busy:(\d{1,5}):(\d{1,5})$/.exec(String(value ?? ''));
|
||||
return m ? { wanted: Number(m[1]), used: Number(m[2]) } : null;
|
||||
}
|
||||
|
||||
/** The notification route of the shell's bridge (loopback, token). Null outside the desktop app. */
|
||||
export function createShellNotifier({ env = process.env, fetcher = fetch } = {}) {
|
||||
const base = env.STRATA_SHELL_BRIDGE, token = env.STRATA_SHELL_TOKEN;
|
||||
if (!base || !token || !/^http:\/\/127\.0\.0\.1:\d+$/.test(base)) return null;
|
||||
return {
|
||||
async notify({ title, body }) {
|
||||
const query = `title=${encodeURIComponent(String(title ?? '').slice(0, 120))}&body=${encodeURIComponent(String(body ?? '').slice(0, 300))}`;
|
||||
const response = await fetcher(`${base}/shell/notify?${query}`, { method: 'POST', headers: { authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(5000) });
|
||||
let reply = null;
|
||||
try { reply = await response.json(); } catch { /* no body */ }
|
||||
return { shown: response.ok && reply?.shown === true };
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* CPU and SSD temperatures for the Monitor's Power card (#3628). A value that this PC cannot give stays null and its line is left out,
|
||||
* so the card never shows a made-up number.
|
||||
*
|
||||
* Linux: the kernel's hwmon sensors (coretemp, k10temp, zenpower for the processor; nvme and drivetemp for the drives).
|
||||
* Windows: the drive reports its own temperature to a query that needs no administrator (IOCTL_STORAGE_QUERY_PROPERTY with the device
|
||||
* temperature property). Get-StorageReliabilityCounter is tried first because it is the documented route; on a normal account it is
|
||||
* refused, then the query is used. One long-lived PowerShell asks every 30 s (windows-poll.mjs).
|
||||
* There is no unprivileged, trustworthy processor temperature on Windows, so none is shown there.
|
||||
* macOS: none.
|
||||
*/
|
||||
import { readFile, readdir } from 'node:fs/promises';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { createPowerShellLoop, endWithHost } from './windows-poll.mjs';
|
||||
|
||||
const CPU_SENSORS = new Set(['coretemp', 'k10temp', 'zenpower', 'cpu_thermal', 'cpu-thermal']);
|
||||
const DISK_SENSORS = new Set(['nvme', 'drivetemp']);
|
||||
const plausible = c => Number.isFinite(c) && c > 0 && c < 150;
|
||||
|
||||
/** hwmon entries [{name, temps: [milli-degrees…]}] -> the hottest processor and drive sensor. */
|
||||
export function pickTemperatures(entries) {
|
||||
let cpu = null, ssd = null;
|
||||
for (const { name, temps } of entries) {
|
||||
const hottest = Math.max(...temps.map(t => t / 1000).filter(plausible), -Infinity);
|
||||
if (!Number.isFinite(hottest)) continue;
|
||||
if (CPU_SENSORS.has(name)) cpu = Math.max(cpu ?? -Infinity, hottest);
|
||||
else if (DISK_SENSORS.has(name)) ssd = Math.max(ssd ?? -Infinity, hottest);
|
||||
}
|
||||
return { cpuTempC: cpu === null ? null : Math.round(cpu), ssdTempC: ssd === null ? null : Math.round(ssd) };
|
||||
}
|
||||
|
||||
export async function readLinuxTemperatures({ base = '/sys/class/hwmon', read = readFile, list = readdir } = {}) {
|
||||
const entries = [];
|
||||
let folders = [];
|
||||
try { folders = await list(base); } catch { return { cpuTempC: null, ssdTempC: null }; }
|
||||
for (const folder of folders) {
|
||||
try {
|
||||
const name = String(await read(`${base}/${folder}/name`, 'utf8')).trim();
|
||||
if (!CPU_SENSORS.has(name) && !DISK_SENSORS.has(name)) continue;
|
||||
const files = (await list(`${base}/${folder}`)).filter(f => /^temp\d+_input$/.test(f));
|
||||
const temps = [];
|
||||
for (const file of files) temps.push(Number(String(await read(`${base}/${folder}/${file}`, 'utf8')).trim()));
|
||||
entries.push({ name, temps });
|
||||
} catch { /* a sensor that disappears is skipped */ }
|
||||
}
|
||||
return pickTemperatures(entries);
|
||||
}
|
||||
|
||||
/** One line of the Windows loop: "ssd 53" or "ssd -". */
|
||||
export function parseSsdLine(line) {
|
||||
const m = /^\s*ssd\s+(-?\d+|-)\s*$/.exec(String(line));
|
||||
if (!m) return undefined;
|
||||
const value = m[1] === '-' ? NaN : Number(m[1]);
|
||||
return plausible(value) ? value : null;
|
||||
}
|
||||
|
||||
const WINDOWS_SCRIPT = String.raw`
|
||||
$ErrorActionPreference='SilentlyContinue'
|
||||
$cs=@'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class StrataDiskTemp {
|
||||
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)] static extern IntPtr CreateFile(string n, uint a, uint s, IntPtr p, uint d, uint f, IntPtr t);
|
||||
[DllImport("kernel32.dll", SetLastError=true)] static extern bool DeviceIoControl(IntPtr h, uint c, byte[] i, uint il, byte[] o, uint ol, out uint r, IntPtr ov);
|
||||
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr h);
|
||||
public static int Read(int n) {
|
||||
IntPtr h = CreateFile("\\\\.\\PhysicalDrive" + n, 0, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);
|
||||
if (h == (IntPtr)(-1)) return -1;
|
||||
try {
|
||||
byte[] q = new byte[12]; q[0] = 0x34; byte[] o = new byte[512]; uint r;
|
||||
if (!DeviceIoControl(h, 0x002D1400, q, 12, o, 512, out r, IntPtr.Zero)) return -1;
|
||||
if (BitConverter.ToUInt16(o, 12) < 1) return -1;
|
||||
return BitConverter.ToInt16(o, 26);
|
||||
} finally { CloseHandle(h); }
|
||||
}
|
||||
}
|
||||
'@
|
||||
$compiled=$false
|
||||
while($true){
|
||||
$max=-1
|
||||
$c=@(Get-PhysicalDisk | Get-StorageReliabilityCounter | Where-Object { $_.Temperature -gt 0 })
|
||||
if($c.Count -gt 0){ $max=($c | Measure-Object Temperature -Maximum).Maximum }
|
||||
else {
|
||||
if(-not $compiled){ Add-Type -TypeDefinition $cs; $compiled=$true }
|
||||
foreach($n in 0..15){ $v=[StrataDiskTemp]::Read($n); if($v -gt $max){ $max=$v } }
|
||||
}
|
||||
if($max -gt 0){ [Console]::Out.WriteLine('ssd '+$max) } else { [Console]::Out.WriteLine('ssd -') }
|
||||
[Console]::Out.Flush()
|
||||
Start-Sleep -Seconds 30
|
||||
}`;
|
||||
|
||||
export function createWindowsSsdReader({ spawnFn = spawn, maxStarts = 2, now = Date.now, intervalSec = 30 } = {}) {
|
||||
let latest = null;
|
||||
const loop = createPowerShellLoop({
|
||||
script: WINDOWS_SCRIPT, spawnFn, maxStarts,
|
||||
onLine: line => {
|
||||
const parsed = parseSsdLine(line);
|
||||
if (parsed === undefined) return false;
|
||||
latest = { value: parsed, at: now() };
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return {
|
||||
read() { loop.start(); return latest && now() - latest.at < 4 * intervalSec * 1000 ? latest.value : null; },
|
||||
stop() { loop.stop(); },
|
||||
get starts() { return loop.starts; },
|
||||
};
|
||||
}
|
||||
|
||||
let windowsReader = null;
|
||||
let linuxCache = { at: 0, value: { cpuTempC: null, ssdTempC: null } };
|
||||
|
||||
/** The temperatures this PC can give now: { cpuTempC, ssdTempC } (null where unreadable). Cheap to call every few seconds. */
|
||||
export async function readTemperatures({ platform = process.platform, now = Date.now } = {}) {
|
||||
if (platform === 'linux') {
|
||||
if (now() - linuxCache.at > 10_000) linuxCache = { at: now(), value: await readLinuxTemperatures() };
|
||||
return linuxCache.value;
|
||||
}
|
||||
if (platform === 'win32') {
|
||||
windowsReader ??= endWithHost(createWindowsSsdReader());
|
||||
return { cpuTempC: null, ssdTempC: windowsReader.read() };
|
||||
}
|
||||
return { cpuTempC: null, ssdTempC: null };
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { currencyChoices, currencyLabel } from '../lib/currency';
|
||||
import { tr } from '../lib/i18n';
|
||||
import { useMoney } from '../lib/money';
|
||||
import { setElectricity, useStore } from '../lib/store';
|
||||
import { Btn, Select } from './ui';
|
||||
|
||||
/**
|
||||
* The price of one kWh for the Monitor's cost line (#3628). The person types only the number: the currency is the one of the computer's
|
||||
* region, shown beside it. "Change currency" is a way to pick another one, never a step that has to be taken.
|
||||
*/
|
||||
export function ElectricityPrice() {
|
||||
const price = useStore(s => s.electricityPrice), override = useStore(s => s.electricityCurrency);
|
||||
const money = useMoney();
|
||||
const [text, setText] = useState(price > 0 ? String(price) : ''), [picking, setPicking] = useState(false);
|
||||
useEffect(() => { setText(price > 0 ? String(price) : ''); }, [price]);
|
||||
const commit = () => setElectricity(Number(text.replace(',', '.')), override);
|
||||
const choices = useMemo(() => currencyChoices(money.locale), [money.locale]);
|
||||
return <span className="stack">
|
||||
<span className="row">
|
||||
<input className="st-input" inputMode="decimal" size={7} aria-label={tr("Price per kWh")} placeholder="0.30" value={text} onChange={e => setText(e.target.value)} onBlur={commit} onKeyDown={e => { if (e.key === 'Enter') commit(); }}/>
|
||||
<span className="small muted">{money.currency ? tr("per kWh, in {0}", [currencyLabel(money.currency, money.locale)]) : tr("per kWh")}</span>
|
||||
<Btn size="sm" onClick={() => setPicking(v => !v)}>{tr("Change currency")}</Btn>
|
||||
</span>
|
||||
{picking ? <Select ariaLabel={tr("Currency")} translateLabels={false} value={override}
|
||||
options={[{ value: '', label: tr("Automatic ({0})", [money.detected ?? '—']) }, ...choices]}
|
||||
onChange={value => { setElectricity(price, String(value)); setPicking(false); }}/> : null}
|
||||
</span>;
|
||||
}
|
||||
@@ -26,7 +26,7 @@ const steps = ['Welcome', 'Your model', 'Prepare', 'Make it yours'];
|
||||
function DeepSeekNote({ version, plan }: { version: NonNullable<ReturnType<typeof nativeCatalogModels>[number]['sizes'][number]>; plan: Record<string, any> | null }) {
|
||||
void plan; // the plan's own disk line repeats what the version says in on_this_pc
|
||||
return <div className="engine-warning" role="status"><Icon name="info" size={18}/><div>
|
||||
<p>{tr("This is a very large model. The download is {0} GB and it needs about as much free disk space. It runs on one 12 GB graphics card with 46 to 64 GB of RAM, slowly: about 5 to 7 words a second. What does not fit in memory is read from your SSD, so an NVMe drive matters.", [Math.round(version.download_gb)])}</p>
|
||||
<p>{tr("This is a very large model. The download is {0} GB and it needs about as much free disk space. It runs on NVIDIA RTX 30, 40 and 50 cards, on a Radeon AI PRO R9700 (Linux) or on a Ryzen AI Max / Strix Halo PC (Windows); how fast it is depends on the memory and card (the line below says what to expect on this computer). What does not fit in memory is read from your SSD, so an NVMe drive matters.", [Math.round(version.download_gb)])}</p>
|
||||
{version.on_this_pc ? <p className="small muted">{version.on_this_pc}</p> : null}
|
||||
<p className="small muted">{tr("You can stop the download and continue later; it picks up where it stopped. Strata warns you here but never blocks you.")}</p>
|
||||
</div></div>;
|
||||
|
||||
@@ -21,6 +21,7 @@ import { nativeAction, useNative, type NativeJob } from '../lib/native';
|
||||
import { tuneEffective } from '../lib/native-tune';
|
||||
import { tunedFields } from '../lib/tune-values';
|
||||
import { draftingChoices as draftingOptions } from '../lib/drafting';
|
||||
import { deepseekContexts, deepseekContextNote } from '../lib/deepseek-context';
|
||||
import { NativeSettings } from './NativeSettings';
|
||||
import { NativeSettingsFile } from './NativeSettingsFile';
|
||||
import { RunImageModel } from './RunImageModel';
|
||||
@@ -83,7 +84,12 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
}, [selectedSize?.id, installed?.ready, resourceProfile, vision, native?.jobs.filter(j => j.state === 'done').length]);
|
||||
useEffect(() => { if (tunedNow?.fields.kv) setKv(tunedNow.fields.kv as typeof kv); if (tunedNow?.fields.lowRam) setLowRam(tunedNow.fields.lowRam as typeof lowRam); }, [tunedNow]);
|
||||
const active = native?.jobs.find(j => j.state === 'running');
|
||||
const job = native?.jobs.find(j => j.id === jobId) ?? (!jobId ? native?.jobs.filter(j => ['load', 'install'].includes(j.kind)).at(-1) : undefined);
|
||||
// Catalog groups share quantization names. A remembered job must still belong
|
||||
// to the selected version; another family's failure stays in diagnostics.
|
||||
const selectedJob = (j: NativeJob) => !!selectedSize && (j.model === selectedSize.id ||
|
||||
(j.model === selectedSize.model && j.family === selectedSize.sourceFamily));
|
||||
const job = native?.jobs.find(j => j.id === jobId && selectedJob(j)) ??
|
||||
native?.jobs.filter(j => ['load', 'install'].includes(j.kind) && selectedJob(j)).at(-1);
|
||||
const noFit = selectedSize?.on_this_pc === 'does not fit';
|
||||
// Graphics cards (strata_control/gpu_fit.py: the one rule of the app, the Terminal and setup): the recommended option is pre-selected, the person may pick another
|
||||
// A card a loaded model already uses (a second model on it usually does not fit): said on its row, and not the pre-selected one while another is free.
|
||||
@@ -138,8 +144,11 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
if (installed.resourceProfile && !loadedSelected?.resourceProfile)
|
||||
setResourceProfile(installed.resourceProfile);
|
||||
}
|
||||
else
|
||||
else {
|
||||
setVision('yes');
|
||||
if (selectedSize.multi_file)
|
||||
setContext(8192); // DeepSeek runs at the one context its engine was checked at (as in the guided setup)
|
||||
}
|
||||
}, [selectedSize?.id, installed?.model, installed?.ready, installed?.context, installed?.kv, installed?.images, installed?.visionEnabled, installed?.visionDevice, installed?.lowRamMode, installed?.resourceProfile]);
|
||||
useEffect(() => { if (jobId && job?.state === 'done' && job.id !== toastedJob && (job.kind === 'load' || job.kind === 'install')) {
|
||||
setToastedJob(job.id);
|
||||
@@ -183,8 +192,9 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
return; presetPending.current = true; setFamily(f.family); setSize(f.sizes.find(s => s.model === v.model && (s.sourceFamily === v.family || f.family === v.family))?.id ?? ''); setContext(v.context ?? 32768); setVision(v.vision ?? 'no'); setKv(v.kv ?? 'int8'); setLowRam(v.low_ram ?? 'auto'); }}/><Btn icon="models" onClick={() => setManage(true)}>{tr("Manage models")}</Btn></span></div>
|
||||
<div className="native-version"><VersionPicker pick={native?.catalog?.recommendation} versions={allSizes} selectedId={selectedSize.id} hardware={fitHardware} downloaded={readyIds} onSelect={id => { const f = families.find(f => f.sizes.some(s => s.id === id)); if (f) { setFamily(f.family); setSize(id); setError(''); } }} onOwnGguf={() => (onImport ?? onOtherModels)()}/></div>
|
||||
{tr(noFit ? <p className="small muted native-spill-note"><Icon name="disk" size={16}/>{tr("May run slower \u00B7 Model data can be read from SSD.")}</p> : null)}
|
||||
{tr(selectedSize?.multi_file && deepseekContextNote(context) ? <p className="small muted native-spill-note"><Icon name="info" size={16}/>{tr(deepseekContextNote(context))}</p> : null)}
|
||||
<div className="native-choice__row">
|
||||
<div className="native-choice__col"><span className="native-choice__label">{tr("Conversation context")}</span><Select ariaLabel={tr("Conversation context")} value={String(context)} options={contextChoices(native?.catalog?.contexts ?? [8192, 32768, 65536, 131072, 262144], context).map(n => ({ value: String(n), label: n % 1024 === 0 ? `${n / 1024}K` : `${n.toLocaleString()} tokens` }))} onChange={n => setContext(Number(n))}/></div>
|
||||
<div className="native-choice__col"><span className="native-choice__label">{tr("Conversation context")}</span><Select ariaLabel={tr("Conversation context")} value={String(context)} options={contextChoices(selectedSize?.multi_file ? deepseekContexts(installed?.contextMax) : native?.catalog?.contexts ?? [8192, 32768, 65536, 131072, 262144], context).map(n => ({ value: String(n), label: n % 1024 === 0 ? `${n / 1024}K` : `${n.toLocaleString()} tokens` }))} onChange={n => setContext(Number(n))}/></div>
|
||||
{tr(draftingChoices ? <div className="native-choice__col"><span className="native-choice__label">{tr("Drafting")}</span><Select ariaLabel={tr("Drafting")} value={drafting} disabled={!!active} options={draftingChoices.map(v => ({ value: v, label: tr(draftingWords[v]) }))} onChange={v => setDrafting(String(v))}/></div> : null)}
|
||||
<div className="native-choice__col"><span className="native-choice__label">{tr("Computer use")}</span><div className="native-resource-choice"><button aria-pressed={resourceProfile === 'work'} data-selected={resourceProfile === 'work'} onClick={() => setResourceProfile('work')}><Icon name="computer" size={20}/><span><strong>{tr("Leave 20% for my work")}</strong><small>{tr("Headroom for your other apps")}</small></span>{tr(resourceProfile === 'work' ? <Icon name="check" size={15}/> : null)}</button><button aria-pressed={resourceProfile === 'performance'} data-selected={resourceProfile === 'performance'} onClick={() => setResourceProfile('performance')}><Icon name="bolt" size={20}/><span><strong>{tr("Full performance")}</strong><small>{tr("Give Strata priority")}</small></span>{tr(resourceProfile === 'performance' ? <Icon name="check" size={15}/> : null)}</button></div></div>
|
||||
</div>
|
||||
@@ -204,13 +214,13 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
{tr(expert && IMAGE_GENERATION_VISIBLE ? <><details className="native-generation"><summary>{tr("Image generation ")}<span className="muted">{tr("Optional")}</span></summary><RunImageModel library={imageLibrary} disabled={pending || !!active} onChange={() => { }}/></details></> : null)}
|
||||
{tr(expert ? <section id="native-expert-settings" ref={expertSection} className="st-card native-execution"><h2 className="h4">{tr("Expert settings")}</h2>{tr(tunedNow ? <p className="small muted native-tuned-note">{tr("Showing the values of your tuning '{0}'.", [tunedNow.name])}</p> : null)}<label className="native-toggle"><span><strong>{tr("Run multiple models")}</strong><small>{tr("Loading another model keeps the others loaded.")}</small></span><button className="st-toggle" role="switch" aria-label={tr("Run multiple models")} aria-checked={multiple} onClick={() => setMultiple(v => !v)}/></label>{tr(cardsGpus.length > 1 ? <label className="input-field"><span>{tr("Layer split")}</span><input className="st-input" value={split} placeholder={tr("auto or 16,32")} onChange={e => setSplit(e.target.value)}/></label> : null)}<div className="native-field-grid"><label className="input-field"><span>{tr("Context memory")}</span><Select ariaLabel={tr("Native KV format")} value={kv} options={[{ value: 'int8', label: 'INT8' }, { value: 'q4_0', label: 'Q4' }, { value: 'k8v4', label: 'K8 / V4' }]} onChange={setKv}/></label><label className="input-field"><span>{tr("Low-RAM mode")}</span><Select ariaLabel={tr("Native low RAM mode")} value={lowRam} options={['auto', 'resident', 'mmap'].map(v => ({ value: v, label: v === 'auto' ? 'Automatic' : v === 'resident' ? 'Keep experts in RAM' : 'Read experts from disk' }))} onChange={setLowRam}/></label></div>{tr(installed ? <><NativeSettingsFile model={installed.model}/><NativeSettings model={installed.model}/></> : <p className="small muted">{tr("More engine parameters become available after preparing this version.")}</p>)}</section> : null)}
|
||||
</div>
|
||||
<aside className="stack stack--lg"><PcFitCard gpuName={pcGpu?.name} ramGb={pcRam} vramGb={pcVram} inUse={keptModels.length ? { ramGb: Math.round(keptRam), vramGb: Math.round(keptVramOn(chosenCards)) } : null} fit={selectedSize ? fitOn(selectedSize, fitHardware, true, { context, kv }) : null}/><section className="st-card"><h2 className="h4">{tr("Model files")}</h2><p className="small muted">{tr("Strata prepares all shards, the draft layer and shared files. Completed downloads are reused.")}</p><code className="native-path">{installed?.dataFolder ?? native?.downloadDirectory ?? native?.installed?.data_folder ?? 'Choose an installation'}</code><Btn size="sm" variant="ghost" onClick={() => { setSettingsSection('Storage'); go('settings'); }}>{tr("Storage settings")}</Btn></section><Btn size="sm" variant="ghost" onClick={onOtherModels}>{tr("Other local models\u2026")}</Btn></aside>
|
||||
<aside className="stack stack--lg"><PcFitCard gpuName={pcGpu?.name} ramGb={pcRam} vramGb={pcVram} diskGb={selectedSize?.multi_file ? selectedSize.disk_gb ?? selectedSize.download_gb : null} inUse={keptModels.length ? { ramGb: Math.round(keptRam), vramGb: Math.round(keptVramOn(chosenCards)) } : null} fit={selectedSize ? fitOn(selectedSize, fitHardware, true, { context, kv }) : null}/><section className="st-card"><h2 className="h4">{tr("Model files")}</h2><p className="small muted">{tr("Strata prepares all shards, the draft layer and shared files. Completed downloads are reused.")}</p><code className="native-path">{installed?.dataFolder ?? native?.downloadDirectory ?? native?.installed?.data_folder ?? 'Choose an installation'}</code><Btn size="sm" variant="ghost" onClick={() => { setSettingsSection('Storage'); go('settings'); }}>{tr("Storage settings")}</Btn></section><Btn size="sm" variant="ghost" onClick={onOtherModels}>{tr("Other local models\u2026")}</Btn></aside>
|
||||
</div>
|
||||
|
||||
<Dialog open={!!plan} title={tr(installed?.ready ? "Apply settings and load" : "Download and prepare")} compact onClose={() => !pending && setPlan(null)} actions={<><Btn disabled={pending} onClick={() => setPlan(null)}>{tr("Cancel")}</Btn><Btn variant="primary" disabled={pending || !!plan?.plan?.disk_short} onClick={() => void download()}>{tr(installed?.ready ? "Apply & load" : "Download & load")}</Btn></>}><p><strong>{tr(plan?.plan?.title ?? selectedFamily?.title)}</strong>{tr(installed?.ready ? null : <>{tr(" \u00B7 approximately ")}{tr(plan?.plan?.download_gb ?? selectedSize?.download_gb)}{tr(" GB")}</>)}</p>{tr(plan?.drafting?.needed ? <p className="small muted">{tr("Choosing DFlash2 also downloads its drafter: about {0} GB, once. If it does not fit in graphics memory, the model uses the MTP instead.", [plan.drafting.gb])}</p> : null)}{tr(installed?.ready ? <p className="small muted">{tr("The model files are already on this computer. Nothing will be downloaded; only the settings you chose are applied.")}</p> : null)}{tr(plan?.plan?.disk_short ? <ErrorNotice message={plan.plan.disk_short}/> : null)}{tr(installed?.ready ? <p className="small muted">{tr("Any currently loaded native models will be unloaded first.")}</p> : <><p className="small muted">{tr("Existing files are checked and reused. Any currently loaded native models will be unloaded before preparing files.")}</p><p className="small muted">{tr("Your computer may feel slower while weights load. You can stop the download and resume later.")}</p></>)}{tr(selectedSize?.images ? <p className="small muted">{tr("Vision encoder files are included. Picture reading loads only when enabled.")}</p> : null)}</Dialog>
|
||||
<NativeDeleteDialog id={deleting} onClose={() => setDeleting(null)}/>
|
||||
<Dialog open={manage} title={tr("Manage models")} onClose={() => setManage(false)} actions={<Btn onClick={() => setManage(false)}>{tr("Done")}</Btn>}><div className="native-manage">
|
||||
{tr(downloaded.length ? <div className="native-manage-list" role="list">{tr(downloaded.map(m => { const v = allSizes.find(x => x.id === m.model), loaded = native?.instances.some(i => i.model === m.model && i.status === 'ready'); return <div className="native-manage-row" role="listitem" key={m.model} data-selected={selectedSize?.id === m.model || undefined}>
|
||||
{tr(downloaded.some(m => m.ready || m.diskGb) ? <div className="native-manage-list" role="list">{tr(downloaded.filter(m => m.ready || m.diskGb).map(m => { const v = allSizes.find(x => x.id === m.model), loaded = native?.instances.some(i => i.model === m.model && i.status === 'ready'); return <div className="native-manage-row" role="listitem" key={m.model} data-selected={selectedSize?.id === m.model || undefined}>
|
||||
<span className="native-manage-name"><strong>{tr(v ? `${catalogVersionLabel(v)}` : m.model_name ?? m.model)}</strong><small>{tr(v ? v.model : m.model)}{tr(m.diskGb ? ` · ${sizeGb(m.diskGb)}` : '')}</small></span>
|
||||
<span className={loaded ? 'st-pill st-pill--accent' : 'st-pill'}>{tr(loaded ? 'Loaded' : 'Downloaded')}</span>
|
||||
<span className="native-manage-actions"><Btn size="sm" disabled={!!active || !v} onClick={() => { if (v) setSize(v.id); setManage(false); }}>{tr(selectedSize?.id === m.model ? 'Selected' : 'Use version')}</Btn><Btn size="sm" variant="danger" disabled={!!active} onClick={() => setDeleting(m.model)}>{tr("Delete")}</Btn></span>
|
||||
|
||||
@@ -9,11 +9,25 @@ import { Btn, ErrorNotice, Select } from './ui';
|
||||
import { speedOptions } from '../lib/deepseek-speed';
|
||||
import type { RunCard } from '../lib/native-fit';
|
||||
import { NativeMcpEditor } from './NativeMcpEditor';
|
||||
/** One of the speed features: what it is called, what it does, whether it is on by default, and the engine environment it writes outside its default (server/native-settings.mjs). */
|
||||
interface SpeedSwitch {
|
||||
id: string;
|
||||
label: string;
|
||||
hint: string;
|
||||
default: 'on' | 'off';
|
||||
env: Record<string, string>;
|
||||
/** What an earlier test build wrote for the same state: it reads as set, and goes with it. */
|
||||
legacy?: Record<string, string>;
|
||||
}
|
||||
const switchHas = (entries: Record<string, string> | undefined, env: Record<string, string> | undefined) => !!entries && Object.keys(entries).length > 0 && Object.entries(entries).every(([name, value]) => env?.[name] === value);
|
||||
const switchIsSet = (sw: SpeedSwitch, env: Record<string, string> | undefined) => switchHas(sw.env, env) || switchHas(sw.legacy, env);
|
||||
const switchIsOn = (sw: SpeedSwitch, env: Record<string, string> | undefined) => switchIsSet(sw, env) !== (sw.default === 'on');
|
||||
export function NativeSettings({ model }: {
|
||||
model: string;
|
||||
}) {
|
||||
const native = useNative();
|
||||
const [revision,setRevision]=useState(0);
|
||||
const [switches, setSwitches] = useState<SpeedSwitch[]>([]); // the speed switches that apply to this model on this PC
|
||||
const [dsExperts, setDsExperts] = useState(false); // the installed DeepSeek engine knows --experts (0.1.50 or newer)
|
||||
const [tuned, setTuned] = useState<{ preset: { name: string } | null; changed: TunedChange[] } | null>(null);
|
||||
const [values, setValues] = useState<Record<string, any>>({}), [mcp, setMcp] = useState('{}'), [original, setOriginal] = useState(''), [text, setText] = useState(''), [tab, setTab] = useState('fields'), [pending, setPending] = useState(false), [error, setError] = useState(''), [notice, setNotice] = useState(''), [query, setQuery] = useState('');
|
||||
@@ -30,10 +44,12 @@ export function NativeSettings({ model }: {
|
||||
mcp: Record<string, any>;
|
||||
tuned?: { preset: { name: string } | null; changed: TunedChange[] } | null;
|
||||
capabilities?: { dsExperts?: boolean };
|
||||
speedSwitches?: SpeedSwitch[];
|
||||
}>(`/api/native/models/${model}/settings`).then(input => {
|
||||
if (!active)
|
||||
return;
|
||||
setDsExperts(!!input.capabilities?.dsExperts);
|
||||
setSwitches(input.speedSwitches ?? []);
|
||||
setTuned(input.tuned ?? null);
|
||||
const value = nativeSettingsResponse(input);
|
||||
setValues(value.values);
|
||||
@@ -47,6 +63,8 @@ export function NativeSettings({ model }: {
|
||||
return () => { active = false; };
|
||||
}, [model,revision]);
|
||||
const change = (id: string, value: any) => { const next = { ...values, [id]: value }; setValues(next); setText(JSON.stringify(next, null, 2)); setNotice(''); };
|
||||
/** The default removes the switch's environment entries, the other state writes them: the model's file then carries nothing for a default launch. */
|
||||
const changeSwitch = (sw: SpeedSwitch, on: boolean) => { const env = { ...(values.env ?? {}) }; const write = on !== (sw.default === 'on'); for (const [name, value] of Object.entries(sw.env)) { if (write) env[name] = value; else delete env[name]; } if (!write) for (const name of Object.keys(sw.legacy ?? {})) delete env[name]; change('env', env); };
|
||||
async function save() {
|
||||
setPending(true);
|
||||
setError('');
|
||||
@@ -97,8 +115,9 @@ export function NativeSettings({ model }: {
|
||||
{error ? <><ErrorNotice message={tr('Couldn’t load model settings.')} next={tr('Check that the engine is connected and this model is installed, then try again.')} onRetry={() => setRevision(value => value + 1)}/><details className="native-settings-diagnostic"><summary className="small">{tr('Details')}</summary><p className="small muted">{error}</p></details></> : <p className="small muted" role="status">{tr('Reading saved settings…')}</p>}
|
||||
</details>;
|
||||
const rows = (native?.fields ?? []).filter(f => (!f.forModel || (model.toLowerCase().includes(f.forModel) && (f.id !== 'ds_experts' || dsExperts))) && `${tr(f.label)} ${f.id} ${tr(f.group)}`.toLowerCase().includes(query.toLowerCase()));
|
||||
const switchRows = switches.filter(sw => `${tr(sw.label)} ${sw.id} ${tr('Speed')}`.toLowerCase().includes(query.toLowerCase()));
|
||||
return <details className="native-settings"><summary>{tr("Native engine settings")}</summary>{tr(tuned?.changed?.length ? <div className="native-tuned"><p className="small"><strong>{tr("Your tuning '{0}' sets", [tuned.preset?.name ?? ''])}</strong></p><ul>{tuned.changed.map(c => { const d = describeChange(c); return <li key={c.flag}><span>{d.name}</span><b>{d.now || tr('off')}</b>{d.was !== null ? <small className="muted">{tr("was {0}", [d.was])}</small> : null}</li>; })}</ul></div> : null)}<p className="small muted">{tr(!original ? 'Reading saved settings… ' : '')}{tr("Settings for this model. Blank values use Strata\u2019s defaults.")}</p><div className="editor-tabs" role="tablist" aria-label={tr("Native settings editor")}>{tr(['fields', 'json', 'tools'].map(t => <button key={t} role="tab" aria-selected={tab === t} onClick={() => chooseTab(t)}>{tr(t === 'fields' ? 'Parameters' : t === 'json' ? 'JSON' : 'MCP tools')}</button>))}</div>
|
||||
{tr(tab === 'fields' ? <><input className="st-input" type="search" aria-label={tr("Search native parameters")} placeholder={tr("Search parameters…")} value={query} onChange={e => setQuery(e.target.value)}/>{tr(['Responses', 'GPU & memory', 'Serving', 'Image reading', 'Tools', 'Recovery'].map(group => rows.some(f => f.group === group) ? <section className="native-settings-group" key={group}><h3 className="h4">{tr(group)}</h3>{tr(rows.filter(f => f.group === group).map(f => <label className="native-setting-row" key={f.id}><span><strong>{tr(f.label)}</strong>{tr(f.supported === false ? <small>{tr("Requires a newer Strata installation.")}</small> : f.hint ? <small>{tr(f.hint)}</small> : null)}</span><NativeFieldInput field={f} value={values[f.id]} onChange={v => change(f.id, v)}/></label>))}</section> : null))}<section className="native-settings-group"><h3 className="h4">{tr("Sampling")}</h3>{tr([['temperature', 'Temperature'], ['top_p', 'Top-p'], ['top_k', 'Top-k'], ['min_p', 'Min-p'], ['repetition_penalty', 'Repetition penalty'], ['presence_penalty', 'Presence penalty'], ['frequency_penalty', 'Frequency penalty'], ['penalty_last_n', 'Penalty window'], ['seed', 'Seed']].map(([id, label]) => <label className="native-setting-row" key={id}><span><strong>{tr(label)}</strong></span><DecimalInput ariaLabel={tr("Native {0}", [label])} value={values.sampling?.[id] ?? null} onChange={n => { const sampling = { ...values.sampling }; if (n === null) delete sampling[id]; else sampling[id] = n; change('sampling', sampling); }}/></label>))}</section></> : tab === 'json' ? <><p className="small muted">{tr("Engine arguments and STRATA environment flags are editable here. Program paths and network access remain managed by the host.")}</p><textarea className="st-input native-json" aria-label={tr("Native configuration JSON")} spellCheck={false} value={text} onChange={e => { setText(e.target.value); setNotice(''); }}/></> : <><p className="small muted">{tr("Add local programs or HTTP MCP servers. Connected tools can read or change the resources you give them access to. Enable tools in chat when you want the model to use them.")}</p><NativeMcpEditor text={mcp} onChange={value => { setMcp(value); setNotice(''); }}/><details><summary className="small">{tr("Configuration example")}</summary><pre className="native-example">{'{\n "files": {\n "command": "npx",\n "args": ["-y", "@modelcontextprotocol/server-filesystem", "/your/folder"],\n "disabled": false\n }\n}'}</pre><p className="xs muted">{tr("Saved credentials are masked. Keep the mask to preserve an existing value; type a new value to replace it.")}</p></details></>)}
|
||||
{tr(tab === 'fields' ? <><input className="st-input" type="search" aria-label={tr("Search native parameters")} placeholder={tr("Search parameters…")} value={query} onChange={e => setQuery(e.target.value)}/>{tr(['Responses', 'GPU & memory', 'Serving', 'Image reading', 'Tools', 'Recovery'].map(group => rows.some(f => f.group === group) ? <section className="native-settings-group" key={group}><h3 className="h4">{tr(group)}</h3>{tr(rows.filter(f => f.group === group).map(f => <label className="native-setting-row" key={f.id}><span><strong>{tr(f.label)}</strong>{tr(f.supported === false ? <small>{tr("Requires a newer Strata installation.")}</small> : f.hint ? <small>{tr(f.hint)}</small> : null)}</span><NativeFieldInput field={f} value={values[f.id]} onChange={v => change(f.id, v)}/></label>))}</section> : null))}{tr(switchRows.length ? <section className="native-settings-group"><h3 className="h4">{tr("Speed")}</h3>{tr(switchRows.map(sw => <label className="native-setting-row" key={sw.id}><span><strong>{tr(sw.label)}</strong><small>{tr(sw.hint)}</small></span><Select ariaLabel={tr(sw.label)} value={switchIsOn(sw, values.env) ? 'on' : 'off'} options={sw.default === 'on' ? [{ value: 'on', label: 'On (recommended)' }, { value: 'off', label: 'Off' }] : [{ value: 'off', label: 'Off (recommended)' }, { value: 'on', label: 'On' }]} onChange={v => changeSwitch(sw, v === 'on')}/></label>))}</section> : null)}<section className="native-settings-group"><h3 className="h4">{tr("Sampling")}</h3>{tr([['temperature', 'Temperature'], ['top_p', 'Top-p'], ['top_k', 'Top-k'], ['min_p', 'Min-p'], ['repetition_penalty', 'Repetition penalty'], ['presence_penalty', 'Presence penalty'], ['frequency_penalty', 'Frequency penalty'], ['penalty_last_n', 'Penalty window'], ['seed', 'Seed']].map(([id, label]) => <label className="native-setting-row" key={id}><span><strong>{tr(label)}</strong></span><DecimalInput ariaLabel={tr("Native {0}", [label])} value={values.sampling?.[id] ?? null} onChange={n => { const sampling = { ...values.sampling }; if (n === null) delete sampling[id]; else sampling[id] = n; change('sampling', sampling); }}/></label>))}</section></> : tab === 'json' ? <><p className="small muted">{tr("Engine arguments and STRATA environment flags are editable here. Program paths and network access remain managed by the host.")}</p><textarea className="st-input native-json" aria-label={tr("Native configuration JSON")} spellCheck={false} value={text} onChange={e => { setText(e.target.value); setNotice(''); }}/></> : <><p className="small muted">{tr("Add local programs or HTTP MCP servers. Connected tools can read or change the resources you give them access to. Enable tools in chat when you want the model to use them.")}</p><NativeMcpEditor text={mcp} onChange={value => { setMcp(value); setNotice(''); }}/><details><summary className="small">{tr("Configuration example")}</summary><pre className="native-example">{'{\n "files": {\n "command": "npx",\n "args": ["-y", "@modelcontextprotocol/server-filesystem", "/your/folder"],\n "disabled": false\n }\n}'}</pre><p className="xs muted">{tr("Saved credentials are masked. Keep the mask to preserve an existing value; type a new value to replace it.")}</p></details></>)}
|
||||
<div className="native-settings-actions"><Btn disabled={pending || !original || !dirty} variant="primary" onClick={() => void save()}>{tr(pending ? 'Saving…' : 'Save settings')}</Btn><Btn disabled={pending} onClick={() => { const saved = JSON.parse(original || '{"values":{},"mcp":{}}'); setValues(saved.values); setText(JSON.stringify(saved.values, null, 2)); setMcp(JSON.stringify(saved.mcp, null, 2)); setError(''); }}>{tr("Restore saved")}</Btn></div>{tr(notice ? <p role="status" className="small">{tr(notice)}</p> : null)}{tr(error ? <ErrorNotice message={error} onRetry={() => setRevision(value => value + 1)}/> : null)}
|
||||
</details>;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ function Meter({ label, total, use }: { label: string; total: number; use: numbe
|
||||
* "This computer": the graphics memory and RAM, and what the chosen version would take of each. One plain line says whether it fits
|
||||
* or how much would be read from the SSD (the n-gram table, which always sits on the SSD, is not counted).
|
||||
*/
|
||||
export function PcFitCard({ gpuName, ramGb, vramGb, fit, inUse = null }: { gpuName?: string | null; ramGb: number | null; vramGb: number; fit: Fit | null; inUse?: { ramGb: number; vramGb: number } | null }) {
|
||||
export function PcFitCard({ gpuName, ramGb, vramGb, fit, inUse = null, diskGb = null }: { gpuName?: string | null; ramGb: number | null; vramGb: number; fit: Fit | null; inUse?: { ramGb: number; vramGb: number } | null; diskGb?: number | null }) {
|
||||
const freeVram = Math.max(0, vramGb - (inUse?.vramGb ?? 0)), freeRam = ramGb ? Math.max(0, ramGb - (inUse?.ramGb ?? 0)) : 0;
|
||||
return <section className="st-card pc-card">
|
||||
<div><h2 className="h4">{tr("This computer")}</h2>
|
||||
@@ -23,6 +23,7 @@ export function PcFitCard({ gpuName, ramGb, vramGb, fit, inUse = null }: { gpuNa
|
||||
{tr(fit?.known && vramGb > 0 ? <Meter label="Graphics memory" total={freeVram} use={fit.vramUseGb}/> : null)}
|
||||
{tr(fit?.known && ramGb ? <Meter label="RAM" total={freeRam} use={fit.ramUseGb}/> : null)}
|
||||
{tr(inUse ? <p className="xs muted">{tr("Models that stay loaded use about {0} GB of RAM and {1} GB of this card’s memory.", [inUse.ramGb, inUse.vramGb])}</p> : null)}
|
||||
{tr(fit?.known ? <p className="pc-note">{tr(fit.spillGb >= 1 ? 'About {0} GB will be read from the SSD, so answers can be a little slower.' : 'Fits fully in memory.', [Math.round(fit.spillGb)])}</p> : null)}
|
||||
{tr(diskGb ? <p className="pc-note">{tr("Needs about {0} GB of free disk space. What does not fit in memory is read from your SSD.", [Math.round(diskGb)])}</p> : null)}
|
||||
{tr(fit?.known && (diskGb ? fit.spillGb >= 1 : true) ? <p className="pc-note">{tr(fit.spillGb >= 1 ? 'About {0} GB will be read from the SSD, so answers can be a little slower.' : 'Fits fully in memory.', [Math.round(fit.spillGb)])}</p> : null)}
|
||||
</section>;
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export function SettingsStorage() {
|
||||
const library = state.library;
|
||||
const versions = library ? pickerGroups(library).flatMap(group => group.versions) : [];
|
||||
// ONE list and ONE total: the optimized models (sizes from their real files, shared files counted once) and the other local models together.
|
||||
const nativeModels = native?.installed?.models ?? [];
|
||||
const nativeModels = (native?.installed?.models ?? []).filter(m => m.ready || (m.diskGb ?? 0) > 0); // a model whose files are all gone (an old config) is not a 0.0 GB download
|
||||
const sharedGb = (native?.installed?.usage?.shared_bytes ?? 0) / 1e9;
|
||||
const nativeGb = native?.installed?.usage ? native.installed.usage.total_gb : nativeModels.reduce((n, m) => n + (m.diskGb ?? 0), 0);
|
||||
const totalGb = nativeGb + versions.reduce((n, v) => n + (v.diskGb ?? 0), 0);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { NativeProgress } from './NativeModels';
|
||||
import { jobErrorText } from '../lib/native-job-diagnostics';
|
||||
import { appVersionLabel } from '../lib/app-version';
|
||||
import { fetchUpdateCheck, publishUpdateCheck, useInstalledVersions, useLastUpdateCheck } from '../lib/update-check';
|
||||
import { shownEngineVersion } from '../lib/engine-version';
|
||||
/** A check from an earlier day shows its date too, because the result is kept between visits. */
|
||||
function checkedLabel(at: string) {
|
||||
const when = new Date(at), sameDay = when.toDateString() === new Date().toDateString();
|
||||
@@ -30,7 +31,7 @@ export function SettingsUpdates() {
|
||||
const [error, setError] = useState('');
|
||||
const request = useRef<AbortController | null>(null);
|
||||
useEffect(() => () => request.current?.abort(), []);
|
||||
const installed = component === 'strata' ? appVersion : native?.available ? (native.installed?.engine?.version ?? 'Not installed') : engineVersion;
|
||||
const installed = component === 'strata' ? appVersion : native?.available ? (native.installed?.engine?.version ?? 'Not installed') : shownEngineVersion(native, engineVersion);
|
||||
const active = native?.jobs.find(j => j.state === 'running');
|
||||
const result = checking || error ? null : (fresh ?? last);
|
||||
async function operate(kind: string) {
|
||||
@@ -78,7 +79,7 @@ export function SettingsUpdates() {
|
||||
return <>
|
||||
<div className="settings-versions" aria-busy={checking}>
|
||||
{tr(versionRow('Strata', appVersion, result?.strata))}
|
||||
{tr(versionRow('Engine', native?.available ? (native.installed?.engine?.version ?? 'Not installed') : engineVersion, result?.engine))}
|
||||
{tr(versionRow('Engine', native?.available ? (native.installed?.engine?.version ?? 'Not installed') : shownEngineVersion(native, engineVersion), result?.engine))}
|
||||
<div className="settings-update-actions"><Btn size="sm" icon="refresh" disabled={checking} className={checking ? 'settings-update-checking' : undefined} onClick={() => void check()}>{tr(checking ? 'Checking…' : error || result?.strata.status === 'error' ? 'Try again' : result ? 'Check again' : 'Check for updates')}</Btn><Btn size="sm" variant="ghost" onClick={() => setRollback(true)}>{tr("Roll back\u2026")}</Btn></div>
|
||||
<div className="settings-update-inline" role="status" aria-live="polite">{tr(checking ? 'Checking for new versions…' : error ? <span className="inline-error">{tr(error)}</span> : result ? <><span className="sr-only">{tr(result.strata.message)} {tr(result.engine.message)} </span>{tr("Checked at ")}{tr(checkedLabel(result.checkedAt))}</> : null)}</div>
|
||||
</div>
|
||||
@@ -95,11 +96,11 @@ export function SettingsUpdates() {
|
||||
</>;
|
||||
}
|
||||
|
||||
/** "Strata 0.2.5 · Engine 0.1.50": the app's own version first, the engine's beside it (About). */
|
||||
/** "Strata 0.2.7 · Engine 0.1.52": the app's own version first, the engine's beside it (About). */
|
||||
export function AboutVersions() {
|
||||
const native = useNative();
|
||||
const app = appVersionLabel(useStore(s => s.setup?.app.version), native?.version);
|
||||
const engineVersion = useStore(s => s.engine?.engine.version);
|
||||
const engine = native?.available ? native.installed?.engine?.version : engineVersion;
|
||||
const engine = shownEngineVersion(native, engineVersion);
|
||||
return <p className="settings-about-version"><strong>{tr("Strata")} {app ?? ''}</strong>{engine ? <span className="muted"> · {tr("Engine")} {engine}</span> : null}</p>;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '../lib/client';
|
||||
import { tr } from '../lib/i18n';
|
||||
import { askNotificationPermission, playDoneSound } from '../lib/attention';
|
||||
import { setApprovalNotify, setDoneSound, useStore } from '../lib/store';
|
||||
import { Btn } from './ui';
|
||||
|
||||
/** What the host says about the desktop shell (GET /api/shell): null in a browser or while it loads. */
|
||||
export interface ShellState {
|
||||
desktop: boolean;
|
||||
tray: boolean;
|
||||
port: number;
|
||||
defaultPort: number;
|
||||
pinnedPort: number | null;
|
||||
minimizeToTray: boolean;
|
||||
portNotice: { wanted: number; used: number } | null;
|
||||
}
|
||||
|
||||
export function useShellState(): [ShellState | null, (patch: { port?: number | null; tray?: boolean }) => Promise<string | null>] {
|
||||
const [shell, setShell] = useState<ShellState | null>(null);
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
api.get<ShellState>('/api/shell').then(s => { if (alive) setShell(s); }).catch(() => undefined);
|
||||
return () => { alive = false; };
|
||||
}, []);
|
||||
const save = async (patch: { port?: number | null; tray?: boolean }) => {
|
||||
try { setShell(await api.post<ShellState>('/api/shell/prefs', patch)); return null; }
|
||||
catch (error) { return error instanceof Error ? error.message : 'The choice was not saved.'; }
|
||||
};
|
||||
return [shell, save];
|
||||
}
|
||||
|
||||
/** The sound for finished work (#3586): off until switched on, with a button to hear it. */
|
||||
export function DoneSoundSetting() {
|
||||
const on = useStore(s => s.doneSound);
|
||||
return <span className="row">
|
||||
<Btn size="sm" onClick={() => playDoneSound()}>{tr("Play sound")}</Btn>
|
||||
<button className="st-toggle" role="switch" aria-checked={on} aria-label={tr("Sound when work finishes")} onClick={() => setDoneSound(!on)}/>
|
||||
</span>;
|
||||
}
|
||||
|
||||
/** A desktop notification when Manual mode waits for an approval (#3477). */
|
||||
export function ApprovalNotifySetting() {
|
||||
const on = useStore(s => s.approvalNotify);
|
||||
return <button className="st-toggle" role="switch" aria-checked={on} aria-label={tr("Notify me when Strata needs an answer")}
|
||||
onClick={() => { if (!on) void askNotificationPermission(); setApprovalNotify(!on); }}/>;
|
||||
}
|
||||
|
||||
/** Keep the app running in the tray when its window is closed (#3477). Desktop app on Windows and macOS only. */
|
||||
export function TraySetting({ shell, save }: { shell: ShellState; save: (patch: { tray: boolean }) => Promise<string | null> }) {
|
||||
return <button className="st-toggle" role="switch" aria-checked={shell.minimizeToTray} aria-label={tr("Minimise to tray")} onClick={() => void save({ tray: !shell.minimizeToTray })}/>;
|
||||
}
|
||||
|
||||
/** The port the app's window and local address use (#3402). Empty lets Strata choose; the choice applies the next time Strata starts. */
|
||||
export function PortSetting({ shell, save }: { shell: ShellState; save: (patch: { port: number | null }) => Promise<string | null> }) {
|
||||
const [text, setText] = useState(shell.pinnedPort ? String(shell.pinnedPort) : ''), [problem, setProblem] = useState('');
|
||||
useEffect(() => { setText(shell.pinnedPort ? String(shell.pinnedPort) : ''); }, [shell.pinnedPort]);
|
||||
const commit = async () => {
|
||||
const trimmed = text.trim();
|
||||
if (trimmed === (shell.pinnedPort ? String(shell.pinnedPort) : '')) { setProblem(''); return; }
|
||||
if (trimmed === '') { setProblem((await save({ port: null })) ?? ''); return; }
|
||||
const value = Number(trimmed);
|
||||
if (!Number.isInteger(value) || value < 1024 || value > 65535) { setProblem(tr("Choose a port between 1024 and 65535, or leave it empty.")); return; }
|
||||
setProblem((await save({ port: value })) ?? '');
|
||||
};
|
||||
return <span className="stack">
|
||||
<span className="row">
|
||||
<input className="st-input" inputMode="numeric" size={7} aria-label={tr("Port (empty = automatic)")} placeholder={String(shell.defaultPort)} value={text}
|
||||
onChange={e => setText(e.target.value.replace(/[^0-9]/g, '').slice(0, 5))} onBlur={() => void commit()} onKeyDown={e => { if (e.key === 'Enter') void commit(); }}/>
|
||||
</span>
|
||||
<span className="xs muted">{tr("Using port {0} now.", [shell.port])}</span>
|
||||
{problem ? <span className="xs" role="alert">{problem}</span> : null}
|
||||
</span>;
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export function VersionPicker({ versions, selectedId, hardware, onSelect, onOwnG
|
||||
return <>
|
||||
<button ref={trigger} type="button" className="version-trigger" aria-haspopup="menu" aria-expanded={open} aria-label={tr("Model version")} onClick={() => setOpen(v => !v)}>
|
||||
<strong>{selected && !apart.has(selected.id) ? versionName(selected, true) : tr(moreLabel ?? "Choose a version")}</strong>{tr(selected && !apart.has(selected.id) && downloaded?.has(selected.id) ? <span className="version-row__have">{tr("Downloaded")}</span> : null)}
|
||||
<span className="version-trigger__need muted">{tr(fit ? `${gb(fit.memoryGb)} RAM + VRAM` : '')}</span>
|
||||
<span className="version-trigger__need muted">{tr(fit && selected?.multi_file && selected.disk_gb ? tr("{0} RAM + VRAM, plus about {1} GB of disk", [gb(fit.memoryGb), Math.round(selected.disk_gb)]) : fit ? `${gb(fit.memoryGb)} RAM + VRAM` : '')}</span>
|
||||
<Icon name="chevron" size={14} className="version-trigger__chev"/>
|
||||
</button>
|
||||
{tr(open ? <FloatingMenu anchor={trigger.current} title={tr("Model version")} width={620} onClose={() => setOpen(false)}>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Telling the person that Strata needs them or has finished (#3586, #3477). Both are opt-in and off by default.
|
||||
*
|
||||
* - A short sound when an answer or task that took a while is finished, or when Strata is in the background (public/strata-done-sound.wav,
|
||||
* made for Strata from sine waves, no sample).
|
||||
* - A desktop notification when Manual mode waits for an approval and Strata is in the background. The desktop app shows it through its own
|
||||
* shell (the host passes it on, so it also works while the window is hidden in the tray); a browser uses the page's notifications.
|
||||
*/
|
||||
import { api } from './client';
|
||||
import { tr } from './i18n';
|
||||
|
||||
/** An answer that came back faster than this does not chime while the person is looking at the window. */
|
||||
export const QUICK_MS = 8000;
|
||||
const SOUND_URL = '/strata-done-sound.wav';
|
||||
const MIN_GAP_MS = 2000;
|
||||
let lastPlayed = 0;
|
||||
|
||||
export interface DoneContext { enabled: boolean; durationMs: number; focused: boolean; now?: number }
|
||||
|
||||
/** Whether the finished-work sound should play now: only when asked for, only for work that took a while or when the window is not in front. */
|
||||
export function shouldPlayDone({ enabled, durationMs, focused, now = Date.now() }: DoneContext): boolean {
|
||||
if (!enabled) return false;
|
||||
if (now - lastPlayed < MIN_GAP_MS) return false;
|
||||
return !focused || durationMs >= QUICK_MS;
|
||||
}
|
||||
|
||||
export const windowInFront = (): boolean => typeof document !== 'undefined' && document.visibilityState === 'visible' && document.hasFocus();
|
||||
|
||||
export function playDoneSound(make: (url: string) => { volume: number; play(): Promise<void> | void } = url => new Audio(url)): boolean {
|
||||
try {
|
||||
const audio = make(SOUND_URL);
|
||||
audio.volume = 0.6;
|
||||
lastPlayed = Date.now();
|
||||
const started = audio.play();
|
||||
if (started && typeof (started as Promise<void>).catch === 'function') (started as Promise<void>).catch(() => { /* the browser asked for a click first: no sound this time */ });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Called when an answer or a task is finished. */
|
||||
export function workFinished(enabled: boolean, durationMs: number): void {
|
||||
if (shouldPlayDone({ enabled, durationMs, focused: windowInFront() })) playDoneSound();
|
||||
}
|
||||
|
||||
export interface Notice { title: string; body: string }
|
||||
|
||||
/** The sentence of the approval notification, in the person's language. */
|
||||
export function approvalNotice(tool: string): Notice {
|
||||
return { title: tr('Strata needs your answer'), body: tr('The assistant wants to use {0}. Open Strata to approve or deny.', [tool || tr('a tool')]) };
|
||||
}
|
||||
|
||||
/** Shows a notification through the desktop shell when there is one, else through the page. Returns whether something was shown. */
|
||||
export async function showNotice(notice: Notice, deps: { post?: (body: Notice) => Promise<{ shown?: boolean }>; page?: (notice: Notice) => boolean } = {}): Promise<boolean> {
|
||||
const post = deps.post ?? (body => api.post<{ shown?: boolean }>('/api/shell/notify', body));
|
||||
try {
|
||||
const result = await post(notice);
|
||||
if (result?.shown) return true;
|
||||
} catch { /* no shell here: the page's own notification below */ }
|
||||
return (deps.page ?? pageNotification)(notice);
|
||||
}
|
||||
|
||||
function pageNotification(notice: Notice): boolean {
|
||||
try {
|
||||
if (typeof Notification === 'undefined' || Notification.permission !== 'granted') return false;
|
||||
new Notification(notice.title, { body: notice.body, tag: 'strata-approval' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Manual mode waits for an approval. Only a window that is not in front needs the notification. */
|
||||
export function approvalNeeded(enabled: boolean, tool: string, focused = windowInFront()): Promise<boolean> {
|
||||
if (!enabled || focused) return Promise.resolve(false);
|
||||
return showNotice(approvalNotice(tool));
|
||||
}
|
||||
|
||||
/** Turning approval notifications on in a browser asks the page's permission once; the desktop shell needs none. */
|
||||
export async function askNotificationPermission(): Promise<void> {
|
||||
try {
|
||||
if (typeof Notification !== 'undefined' && Notification.permission === 'default') await Notification.requestPermission();
|
||||
} catch { /* some web views have none */ }
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* The currency of the Monitor's cost line (#3628): the person's own, found from the computer's region, never typed and never a guess.
|
||||
*
|
||||
* Order: the system (the host reads the regional format of Windows, macOS or Linux: its region, or its currency when the system names one),
|
||||
* then the browser's language list, then the region of the app's language, and then no currency at all: the number is shown without a
|
||||
* symbol rather than in a currency the person does not use. A currency picked in Settings overrides all of this.
|
||||
*/
|
||||
|
||||
/** ISO 3166 region -> ISO 4217 currency. */
|
||||
const PAIRS = `AD:EUR AE:AED AF:AFN AG:XCD AI:XCD AL:ALL AM:AMD AO:AOA AR:ARS AS:USD AT:EUR AU:AUD AW:AWG AX:EUR AZ:AZN
|
||||
BA:BAM BB:BBD BD:BDT BE:EUR BF:XOF BG:EUR BH:BHD BI:BIF BJ:XOF BL:EUR BM:BMD BN:BND BO:BOB BQ:USD BR:BRL BS:BSD BT:BTN BV:NOK BW:BWP BY:BYN BZ:BZD
|
||||
CA:CAD CC:AUD CD:CDF CF:XAF CG:XAF CH:CHF CI:XOF CK:NZD CL:CLP CM:XAF CN:CNY CO:COP CR:CRC CU:CUP CV:CVE CW:XCG CX:AUD CY:EUR CZ:CZK
|
||||
DE:EUR DJ:DJF DK:DKK DM:XCD DO:DOP DZ:DZD EC:USD EE:EUR EG:EGP EH:MAD ER:ERN ES:EUR ET:ETB FI:EUR FJ:FJD FK:FKP FM:USD FO:DKK FR:EUR
|
||||
GA:XAF GB:GBP GD:XCD GE:GEL GF:EUR GG:GBP GH:GHS GI:GIP GL:DKK GM:GMD GN:GNF GP:EUR GQ:XAF GR:EUR GS:GBP GT:GTQ GU:USD GW:XOF GY:GYD
|
||||
HK:HKD HM:AUD HN:HNL HR:EUR HT:HTG HU:HUF ID:IDR IE:EUR IL:ILS IM:GBP IN:INR IO:USD IQ:IQD IR:IRR IS:ISK IT:EUR
|
||||
JE:GBP JM:JMD JO:JOD JP:JPY KE:KES KG:KGS KH:KHR KI:AUD KM:KMF KN:XCD KP:KPW KR:KRW KW:KWD KY:KYD KZ:KZT
|
||||
LA:LAK LB:LBP LC:XCD LI:CHF LK:LKR LR:LRD LS:LSL LT:EUR LU:EUR LV:EUR LY:LYD
|
||||
MA:MAD MC:EUR MD:MDL ME:EUR MF:EUR MG:MGA MH:USD MK:MKD ML:XOF MM:MMK MN:MNT MO:MOP MP:USD MQ:EUR MR:MRU MS:XCD MT:EUR MU:MUR MV:MVR MW:MWK MX:MXN MY:MYR MZ:MZN
|
||||
NA:NAD NC:XPF NE:XOF NF:AUD NG:NGN NI:NIO NL:EUR NO:NOK NP:NPR NR:AUD NU:NZD NZ:NZD OM:OMR
|
||||
PA:PAB PE:PEN PF:XPF PG:PGK PH:PHP PK:PKR PL:PLN PM:EUR PR:USD PS:ILS PT:EUR PW:USD PY:PYG QA:QAR RE:EUR RO:RON RS:RSD RU:RUB RW:RWF
|
||||
SA:SAR SB:SBD SC:SCR SD:SDG SE:SEK SG:SGD SH:SHP SI:EUR SJ:NOK SK:EUR SL:SLE SM:EUR SN:XOF SO:SOS SR:SRD SS:SSP ST:STN SV:USD SX:XCG SY:SYP SZ:SZL
|
||||
TC:USD TD:XAF TF:EUR TG:XOF TH:THB TJ:TJS TK:NZD TL:USD TM:TMT TN:TND TO:TOP TR:TRY TT:TTD TV:AUD TW:TWD TZ:TZS
|
||||
UA:UAH UG:UGX UM:USD US:USD UY:UYU UZ:UZS VA:EUR VC:XCD VE:VES VG:USD VI:USD VN:VND VU:VUV WF:XPF WS:WST XK:EUR YE:YER YT:EUR ZA:ZAR ZM:ZMW ZW:ZWG`;
|
||||
|
||||
export const REGION_CURRENCY: Readonly<Record<string, string>> = Object.freeze(Object.fromEntries(PAIRS.split(/\s+/).map(pair => pair.split(':') as [string, string])));
|
||||
|
||||
/** The region of each app language that has one region of its own; English and Spanish-speaking countries are many, so only the first is a guess. */
|
||||
export const LANGUAGE_REGION: Readonly<Record<string, string>> = Object.freeze({ zh: 'CN', ja: 'JP', de: 'DE', es: 'ES', ru: 'RU', pt: 'BR', ko: 'KR' });
|
||||
|
||||
export const currencyForRegion = (region: string | null | undefined): string | null => (region ? REGION_CURRENCY[region.toUpperCase()] ?? null : null);
|
||||
|
||||
/** The region in a language tag ("en-US", "de_DE.UTF-8", "zh-Hans-CN"), or null when the tag names none. */
|
||||
export function regionOfLocale(tag: string | null | undefined): string | null {
|
||||
if (!tag) return null;
|
||||
const clean = String(tag).replace(/[.@].*$/, '').replace(/_/g, '-');
|
||||
try {
|
||||
const region = new Intl.Locale(clean).region;
|
||||
return region ?? null;
|
||||
} catch {
|
||||
const found = /-([A-Za-z]{2})(?:-|$)/.exec(clean);
|
||||
return found ? found[1].toUpperCase() : null;
|
||||
}
|
||||
}
|
||||
|
||||
/** What the host read from the system (GET /api/locale). */
|
||||
export interface SystemLocale { locale: string | null; region: string | null; currency: string | null; source: string | null }
|
||||
|
||||
export interface ResolvedCurrency { currency: string | null; locale: string; source: 'system' | 'browser' | 'language' | 'none' }
|
||||
|
||||
/**
|
||||
* The currency to show money in, and the locale to write the number in.
|
||||
* `override` is the currency the person chose in Settings (empty: automatic).
|
||||
*/
|
||||
export function resolveCurrency({ system, browserLanguages = [], language, override = '' }: { system?: SystemLocale | null; browserLanguages?: readonly string[]; language: string; override?: string }): ResolvedCurrency {
|
||||
const appTag = language === 'zh' ? 'zh-CN' : language;
|
||||
const locale = system?.locale ?? browserLanguages.find(tag => regionOfLocale(tag)) ?? browserLanguages[0] ?? appTag;
|
||||
if (/^[A-Za-z]{3}$/.test(override)) return { currency: override.toUpperCase(), locale, source: 'system' };
|
||||
const fromSystem = system?.currency ?? currencyForRegion(system?.region);
|
||||
if (fromSystem) return { currency: fromSystem, locale, source: 'system' };
|
||||
for (const tag of browserLanguages) {
|
||||
const found = currencyForRegion(regionOfLocale(tag));
|
||||
if (found) return { currency: found, locale, source: 'browser' };
|
||||
}
|
||||
const fromLanguage = currencyForRegion(LANGUAGE_REGION[language]);
|
||||
if (fromLanguage) return { currency: fromLanguage, locale, source: 'language' };
|
||||
return { currency: null, locale, source: 'none' };
|
||||
}
|
||||
|
||||
/** How many decimals this currency normally has (2 for dollars and euros, 0 for yen); 2 when there is no currency. */
|
||||
export function minorDigits(currency: string | null, locale: string): number {
|
||||
if (!currency) return 2;
|
||||
try { return new Intl.NumberFormat(locale, { style: 'currency', currency }).resolvedOptions().maximumFractionDigits ?? 2; } catch { return 2; }
|
||||
}
|
||||
|
||||
/**
|
||||
* An amount of money, written for the person's locale with the currency's usual digits ("$0.12", "$1.23", "1,23 €", "¥3").
|
||||
* An amount below the smallest normal unit (one cent; one yen) is not shown as 0.00 or as a long run of decimals but as
|
||||
* "less than $0.01" (`lessThan` writes the sentence in the person's language). With no currency the number is shown alone,
|
||||
* and never in a currency that was only guessed.
|
||||
*/
|
||||
export function formatMoney(amount: number, { currency, locale, lessThan = shown => `less than ${shown}` }: { currency: string | null; locale: string; lessThan?: (shown: string) => string }): string {
|
||||
if (!Number.isFinite(amount) || amount < 0) return '—';
|
||||
const digits = minorDigits(currency, locale), unit = 10 ** -digits;
|
||||
const write = (value: number): string => {
|
||||
try {
|
||||
if (currency) return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(value);
|
||||
} catch { /* an unknown currency code or locale: the number alone */ }
|
||||
try {
|
||||
return new Intl.NumberFormat(locale, { minimumFractionDigits: digits, maximumFractionDigits: digits }).format(value);
|
||||
} catch {
|
||||
return value.toFixed(digits);
|
||||
}
|
||||
};
|
||||
return amount > 0 && amount < unit ? lessThan(write(unit)) : write(amount);
|
||||
}
|
||||
|
||||
/** "USD ($)": the code and its symbol in the person's locale, for the Settings line. */
|
||||
export function currencyLabel(currency: string, locale: string): string {
|
||||
try {
|
||||
const symbol = new Intl.NumberFormat(locale, { style: 'currency', currency, currencyDisplay: 'narrowSymbol' }).formatToParts(0).find(part => part.type === 'currency')?.value;
|
||||
return symbol && symbol !== currency ? `${currency} (${symbol})` : currency;
|
||||
} catch {
|
||||
return currency;
|
||||
}
|
||||
}
|
||||
|
||||
/** Every currency this web view knows, by name, for the override list. */
|
||||
export function currencyChoices(locale: string): { value: string; label: string }[] {
|
||||
let codes: string[] = [];
|
||||
try { codes = (Intl as unknown as { supportedValuesOf?: (key: string) => string[] }).supportedValuesOf?.('currency') ?? []; } catch { /* an older web view */ }
|
||||
let names: Intl.DisplayNames | null = null;
|
||||
try { names = new Intl.DisplayNames([locale], { type: 'currency' }); } catch { /* no names */ }
|
||||
return codes.map(code => ({ value: code, label: `${code} – ${names?.of(code) ?? code}` }));
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* DeepSeek-V4.1-Flash's context sizes in the app: 8K up to the pack's own limit (the model reads 1,048,576 positions natively).
|
||||
* Project Maya v1.1.28 (#442, @needmorevram, MIT) offered 512K and 1M the same way. strata_control/dsv41_context.py says the same for setup and the
|
||||
* Terminal (tests/terminal/test_dsv41_context.py compares the two). The engine takes whatever --max-context it is given; the rope settings come from the pack.
|
||||
*/
|
||||
export const CONTEXT_CHOICES = [8192, 32768, 65536, 131072, 262144, 524288, 1048576];
|
||||
/** The largest size not flagged as untested; also how far the list goes while the pack does not say more (a pack without ds4.txt behaves as before). */
|
||||
export const TESTED_CONTEXT = 262144;
|
||||
/** The model's own length: the most any pack can say. */
|
||||
export const NATIVE_CONTEXT = 1048576;
|
||||
export const UNTESTED_NOTE = 'Not tested yet. A context this long takes graphics memory from the model and reads long prompts slowly.';
|
||||
|
||||
/** The sizes to offer: up to the pack's limit (`contextMax`, from its ds4.txt) when the installed model has one, else up to 256K. */
|
||||
export function deepseekContexts(limit?: number | null): number[] {
|
||||
const cap = typeof limit === 'number' && Number.isFinite(limit) && limit > 0 ? limit : TESTED_CONTEXT;
|
||||
return CONTEXT_CHOICES.filter(size => size <= cap);
|
||||
}
|
||||
|
||||
/** An empty string when the size can run; otherwise one sentence (only a size the model cannot address is refused). */
|
||||
export function deepseekContextProblem(context: number, limit?: number | null): string {
|
||||
const cap = typeof limit === 'number' && Number.isFinite(limit) && limit > 0 ? limit : NATIVE_CONTEXT;
|
||||
if (!Number.isInteger(context) || context < 1024) return 'Choose a context of at least 1,024 tokens.';
|
||||
return context > cap ? `A context past ${cap.toLocaleString('en-US')} tokens is more than this model reads.` : '';
|
||||
}
|
||||
|
||||
/** The plain words past 256K, an empty string otherwise. */
|
||||
export const deepseekContextNote = (context: number): string => (context > TESTED_CONTEXT ? UNTESTED_NOTE : '');
|
||||
@@ -0,0 +1,8 @@
|
||||
/** The host's design-mode placeholder for the engine version; it is not an engine anyone installed. */
|
||||
const PLACEHOLDER_ENGINE = /^strata-engine 0\.1\.0$/;
|
||||
/** The engine version the app shows: the installed engine's once the host has read it; until then nothing (never the placeholder). */
|
||||
export function shownEngineVersion(native: { available?: boolean; installed?: { engine?: { version?: string | null } | null } | null } | null | undefined, storeVersion: string | undefined): string | undefined {
|
||||
if (native?.available) return native.installed?.engine?.version ?? undefined;
|
||||
return storeVersion && !PLACEHOLDER_ENGINE.test(storeVersion) ? storeVersion : undefined;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* When a chat follows the newest token, and when the reader has let go (#4014).
|
||||
*
|
||||
* The old rule asked "is the reader within 120 px of the bottom?" on every streamed token and pulled the view
|
||||
* back if so. One wheel notch is about 100 px, so it always landed inside that band and was undone by the next
|
||||
* token; only a scrollbar drag, which jumps further, got away. The rule now follows what the reader did:
|
||||
* a wheel, finger or key moving up lets go at once, and the chat follows again when the reader is back at
|
||||
* the bottom.
|
||||
*/
|
||||
export interface Scroller { scrollHeight: number; scrollTop: number; clientHeight: number }
|
||||
|
||||
/** Pixels of slack that still count as "at the bottom" (the same as `atBottom` in reasoning.ts). */
|
||||
export const FOLLOW_SLACK = 24;
|
||||
/** Past this far from the bottom the "jump to latest" button is offered. */
|
||||
export const PILL_DISTANCE = 400;
|
||||
/** A scroll this soon after the reader's own input was the reader's. */
|
||||
export const INPUT_WINDOW_MS = 400;
|
||||
|
||||
export const distanceFromBottom = (el: Scroller): number => el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||
|
||||
/**
|
||||
* Whether the chat should keep following after a scroll event.
|
||||
* - back at the bottom: follow again;
|
||||
* - the reader moved it (a wheel, finger, key or the scrollbar a moment ago) and it is not at the bottom: let go;
|
||||
* - far away whatever moved it (a jump): let go;
|
||||
* - otherwise (the page grew under a following reader and no input came): keep the state it had.
|
||||
*/
|
||||
export function followAfterScroll(distance: number, wasFollowing: boolean, msSinceInput: number): boolean {
|
||||
if (distance <= FOLLOW_SLACK) return true;
|
||||
if (msSinceInput <= INPUT_WINDOW_MS || distance > PILL_DISTANCE) return false;
|
||||
return wasFollowing;
|
||||
}
|
||||
|
||||
/** A wheel turned up (negative delta) or a finger dragging the content down: the reader wants to go back. */
|
||||
export const wheelLeavesBottom = (deltaY: number): boolean => deltaY < 0;
|
||||
|
||||
/** Keys that move a scroller up. */
|
||||
export const KEYS_UP = new Set(['PageUp', 'ArrowUp', 'Home']);
|
||||
@@ -7,6 +7,7 @@ import tune_es from '../locales/tune-es.json';
|
||||
import tune_ru from '../locales/tune-ru.json';
|
||||
import tune_pt from '../locales/tune-pt.json';
|
||||
import tune_ko from '../locales/tune-ko.json';
|
||||
import tune_fr from '../locales/tune-fr.json';
|
||||
import tools_en from '../locales/tools-en.json';
|
||||
import tools_zh from '../locales/tools-zh.json';
|
||||
import tools_ja from '../locales/tools-ja.json';
|
||||
@@ -15,6 +16,7 @@ import tools_es from '../locales/tools-es.json';
|
||||
import tools_ru from '../locales/tools-ru.json';
|
||||
import tools_pt from '../locales/tools-pt.json';
|
||||
import tools_ko from '../locales/tools-ko.json';
|
||||
import tools_fr from '../locales/tools-fr.json';
|
||||
import fix_en from '../locales/fix-en.json';
|
||||
import fix_zh from '../locales/fix-zh.json';
|
||||
import fix_ja from '../locales/fix-ja.json';
|
||||
@@ -23,6 +25,17 @@ import fix_es from '../locales/fix-es.json';
|
||||
import fix_ru from '../locales/fix-ru.json';
|
||||
import fix_pt from '../locales/fix-pt.json';
|
||||
import fix_ko from '../locales/fix-ko.json';
|
||||
import fix_fr from '../locales/fix-fr.json';
|
||||
/* The strings of the 0.2.7 GitHub items (gh-<lang>.json, scripts/make-gh-locales.py). */
|
||||
import gh_en from '../locales/gh-en.json';
|
||||
import gh_zh from '../locales/gh-zh.json';
|
||||
import gh_ja from '../locales/gh-ja.json';
|
||||
import gh_de from '../locales/gh-de.json';
|
||||
import gh_es from '../locales/gh-es.json';
|
||||
import gh_ru from '../locales/gh-ru.json';
|
||||
import gh_pt from '../locales/gh-pt.json';
|
||||
import gh_ko from '../locales/gh-ko.json';
|
||||
import gh_fr from '../locales/gh-fr.json';
|
||||
import base_zh from '../locales/zh.json';
|
||||
import base_ja from '../locales/ja.json';
|
||||
import base_de from '../locales/de.json';
|
||||
@@ -30,15 +43,16 @@ import base_es from '../locales/es.json';
|
||||
import base_ru from '../locales/ru.json';
|
||||
import base_pt from '../locales/pt.json';
|
||||
import base_ko from '../locales/ko.json';
|
||||
import base_fr from '../locales/fr.json';
|
||||
|
||||
export const LANGUAGES=[{value:'en',label:'English'},{value:'zh',label:'中文(简体)'},{value:'ja',label:'日本語'},{value:'de',label:'Deutsch'},{value:'es',label:'Español'},{value:'ru',label:'Русский'},{value:'pt',label:'Português'},{value:'ko',label:'한국어'}] as const;
|
||||
export const LANGUAGES=[{value:'en',label:'English'},{value:'zh',label:'中文(简体)'},{value:'ja',label:'日本語'},{value:'de',label:'Deutsch'},{value:'fr',label:'Français'},{value:'es',label:'Español'},{value:'ru',label:'Русский'},{value:'pt',label:'Português'},{value:'ko',label:'한국어'}] as const;
|
||||
export type Language=typeof LANGUAGES[number]['value'];
|
||||
/* The Tune model dialog's strings live in tune-<lang>.json (apps/host/scripts/make-tune-locales.py) so other interface work does not collide with them. */
|
||||
/* The tool-server, picture and agent-result strings of the tools and images fixes live in tools-<lang>.json for the same reason. */
|
||||
/* The strings that had no key in the files above (#3479, #3825) and the 0.2.4.3 fixes live in fix-<lang>.json (scripts/make-fix-locales.py). */
|
||||
const english:Record<string,string>={...baseEnglish,...tune_en,...tools_en,...fix_en};
|
||||
const [zh,ja,de,es,ru,pt,ko]=[{...base_zh,...tune_zh,...tools_zh,...fix_zh},{...base_ja,...tune_ja,...tools_ja,...fix_ja},{...base_de,...tune_de,...tools_de,...fix_de},{...base_es,...tune_es,...tools_es,...fix_es},{...base_ru,...tune_ru,...tools_ru,...fix_ru},{...base_pt,...tune_pt,...tools_pt,...fix_pt},{...base_ko,...tune_ko,...tools_ko,...fix_ko}] as Record<string,string>[];
|
||||
const catalogs:Record<Language,Record<string,string>>={en:english,zh,ja,de,es,ru,pt,ko};
|
||||
const english:Record<string,string>={...baseEnglish,...tune_en,...tools_en,...fix_en,...gh_en};
|
||||
const [zh,ja,de,es,ru,pt,ko,fr]=[{...base_zh,...tune_zh,...tools_zh,...fix_zh,...gh_zh},{...base_ja,...tune_ja,...tools_ja,...fix_ja,...gh_ja},{...base_de,...tune_de,...tools_de,...fix_de,...gh_de},{...base_es,...tune_es,...tools_es,...fix_es,...gh_es},{...base_ru,...tune_ru,...tools_ru,...fix_ru,...gh_ru},{...base_pt,...tune_pt,...tools_pt,...fix_pt,...gh_pt},{...base_ko,...tune_ko,...tools_ko,...fix_ko,...gh_ko},{...base_fr,...tune_fr,...tools_fr,...fix_fr,...gh_fr}] as Record<string,string>[];
|
||||
const catalogs:Record<Language,Record<string,string>>={en:english,zh,ja,de,fr,es,ru,pt,ko};
|
||||
const entities:Record<string,string>={amp:'&',lt:'<',gt:'>',quot:'"',apos:"'",nbsp:'\u00a0'};
|
||||
const decode=(text:string)=>text.replace(/&(#x[\da-f]+|#\d+|amp|lt|gt|quot|apos|nbsp);/gi,(whole,key:string)=>key[0]==='#'?String.fromCodePoint(key[1].toLowerCase()==='x'?parseInt(key.slice(2),16):parseInt(key.slice(1),10)):entities[key]??whole);
|
||||
const ids=new Map(Object.entries(english).map(([id,value])=>[decode(value),id]));
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { tr } from './i18n';
|
||||
import { api } from './client';
|
||||
import { formatMoney, resolveCurrency, type ResolvedCurrency, type SystemLocale } from './currency';
|
||||
import { useStore } from './store';
|
||||
|
||||
let asked: Promise<SystemLocale | null> | null = null;
|
||||
|
||||
/** The computer's regional format, read once from the host (null in a browser build that has none, or while it loads). */
|
||||
export function useSystemLocale(): SystemLocale | null {
|
||||
const [value, setValue] = useState<SystemLocale | null>(null);
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
asked ??= api.get<SystemLocale>('/api/locale').catch(() => null);
|
||||
void asked.then(v => { if (alive) setValue(v); });
|
||||
return () => { alive = false; };
|
||||
}, []);
|
||||
return value;
|
||||
}
|
||||
|
||||
/** The person's currency and locale for money (#3628) and a function that writes an amount in them. */
|
||||
export function useMoney(): ResolvedCurrency & { format: (amount: number) => string; detected: string | null } {
|
||||
const system = useSystemLocale(), language = useStore(s => s.language), override = useStore(s => s.electricityCurrency);
|
||||
const browserLanguages = typeof navigator === 'undefined' ? [] : [...(navigator.languages ?? [navigator.language])].filter(Boolean);
|
||||
const resolved = resolveCurrency({ system, browserLanguages, language, override });
|
||||
const detected = override ? resolveCurrency({ system, browserLanguages, language }).currency : resolved.currency;
|
||||
return { ...resolved, detected, format: amount => formatMoney(amount, { ...resolved, lessThan: shown => tr('less than {0}', [shown]) }) };
|
||||
}
|
||||
@@ -29,3 +29,16 @@ export function gpuCapacity(devices:GpuInfo[]) {
|
||||
const shared=devices.filter(g=>g.unified).map(g=>g.vramGb);
|
||||
return devices.filter(g=>!g.unified).reduce((n,g)=>n+g.vramGb,0)+(shared.length?Math.max(...shared):0);
|
||||
}
|
||||
|
||||
/**
|
||||
* The cards "All GPUs" covers by default. A small integrated chip (an AMD or Intel adapter on the Windows/Vulkan list with a carve-out under
|
||||
* 2 GB) beside a real card is left out: Strata cannot run a model on it (the native engine report marks it unusable), and counting it adds a
|
||||
* second "GPU" and half a gigabyte to every total. It stays in the picker. Alone (a Strix Halo, an iGPU-only PC) it stays the default.
|
||||
*/
|
||||
export function defaultGpuSet(devices:GpuInfo[]):GpuInfo[] {
|
||||
const real=(g:GpuInfo)=>g.vramGb>=4||g.vendor==='nvidia'||g.backend==='cuda';
|
||||
const small=(g:GpuInfo)=>g.backend==='vulkan'&&(g.vendor==='amd'||g.vendor==='intel')&&g.vramGb<2&&!g.unified;
|
||||
if(!devices.some(real))return devices;
|
||||
const kept=devices.filter(g=>!small(g));
|
||||
return kept.length?kept:devices;
|
||||
}
|
||||
@@ -54,7 +54,9 @@ export function withHostSensors(live:MonitorState,host:MonitorState|null):Monito
|
||||
const empty=(g:GpuReading)=>![g.utilizationPct,g.temperatureC,g.memoryUsedGb,g.powerW].some(hasReading);
|
||||
const pick=<T,>(a:T,b:T)=>hasReading(a as unknown)?a:b;
|
||||
return {...live,hardware:{...h,
|
||||
gpus:(h.gpus??[]).map(g=>{const sensor=s.gpus?.find(x=>x.id===g.id);return sensor&&empty(g)?{...g,...sensor}:g;}),
|
||||
// The fan and the clock are the host's own readings; the engine's metrics do not carry them (#3628).
|
||||
gpus:(h.gpus??[]).map(g=>{const sensor=s.gpus?.find(x=>x.id===g.id);const own=sensor&&empty(g)?{...g,...sensor}:g;return sensor?{...own,fanPct:own.fanPct??sensor.fanPct,clockMhz:own.clockMhz??sensor.clockMhz}:own;}),
|
||||
cpuTempC:s.cpuTempC,ssdTempC:s.ssdTempC,energyWh:s.energyWh,
|
||||
gpuLoad:pick(h.gpuLoad,s.gpuLoad),vramUsedGb:pick(h.vramUsedGb,s.vramUsedGb),vramTotalGb:pick(h.vramTotalGb,s.vramTotalGb),gpuTempC:pick(h.gpuTempC,s.gpuTempC),
|
||||
powerW:pick(h.powerW,s.powerW),powerLimitW:pick(h.powerLimitW,s.powerLimitW),pcieRxMbs:pick(h.pcieRxMbs,s.pcieRxMbs)}};
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { api } from './client';
|
||||
import type { Hardware, RuntimeModels } from './types';
|
||||
export interface GpuChoiceOption {key:string;cards:number[];label:string;verdict:'fits'|'tight'|'spills'|'unknown';words:string;notes:string[];recommended:boolean}
|
||||
export interface GpuChoice {options:GpuChoiceOption[];recommended:string|null;ignored:{name?:string;why_ignored:string}[];cards:number}
|
||||
export interface NativeSize {/** The first engine that reads this size's files (the catalog), and whether the installed engine is older: the screen then says "needs a newer engine" and offers the update. */engine_min?:string|null;needsNewerEngine?:boolean;id:string;model:string;about:string;download_gb:number;ram_needed_gb:number;experts_gb:number;on_this_pc:string|null;installed:boolean;gpu_options?:GpuChoice|null;images?:boolean;imagesCapable?:boolean;/** DeepSeek-V4.1-Flash and the like: several big files and an engine of their own. */multi_file?:boolean}
|
||||
export interface NativeSize {/** The first engine that reads this size's files (the catalog), and whether the installed engine is older: the screen then says "needs a newer engine" and offers the update. */engine_min?:string|null;needsNewerEngine?:boolean;id:string;model:string;about:string;download_gb:number;ram_needed_gb:number;experts_gb:number;on_this_pc:string|null;installed:boolean;gpu_options?:GpuChoice|null;images?:boolean;imagesCapable?:boolean;/** DeepSeek-V4.1-Flash and the like: several big files and an engine of their own. */multi_file?:boolean;/** DeepSeek: the free disk the set needs (its download plus the pack and room to work), from the set itself. */disk_gb?:number}
|
||||
export interface NativeFamily {family:string;title:string;about:string;experimental:boolean;images:boolean;sizes:NativeSize[]}
|
||||
export interface NativeField {id:string;label:string;group:string;type:'number'|'boolean'|'string'|'enum'|'array';min?:number;max?:number;choices?:string[];hint?:string;supported?:boolean;forModel?:string}
|
||||
export interface NativeJob {id:string;kind:string;guided?:boolean;model?:string;family?:string;state:'running'|'done'|'error'|'cancelled';startedAt:string;summary:string;error:string|null;details?:string;result:Record<string,any>|null;progress?:Record<string,any>|null}
|
||||
@@ -12,7 +12,7 @@ export interface NativeState {
|
||||
/** The picture reader (strata-vision) is installed with the engine, so models marked for pictures can read them here. */
|
||||
/** Strata Control's view of the engine for this computer (the app): usable false = none found or recognised; `notice` says why in plain words. */
|
||||
engine?:{usable:boolean;notice?:string|null;wrongMake?:boolean;version?:string|null};
|
||||
visionReader?: boolean;available:boolean;repo:string;downloadDirectory?:string;/** Free bytes on the drive that holds the models folder. */downloadFreeBytes?:number|null;downloadTotalBytes?:number|null;version?:string|null;summary:string;error:string|null;catalog:{families:NativeFamily[];contexts:number[];recommendation:{family:string|null;model:string|null;context?:number;why?:string};context_note:string}|null;installed:{models:{model:string;config:string;model_name:string;ready:boolean;/** The picture-reading files are on this computer (notes-split-agent8.md, item 8). */visionPrepared?:boolean;context?:number;kv?:string;images?:string;visionEnabled?:boolean;visionDevice?:string;lowRamMode?:string;resourceProfile?:'work'|'performance';/** The Drafting choice saved with the model: auto | mtp | dflash2 | off. */drafting?:string;dataFolder?:string;diskGb?:number;diskBytes?:number;/** Files this model shares with another (the picture reader, the draft layer): counted once, in `usage`. */sharedBytes?:number;/** What Delete frees: files in Strata's own folders that no other model uses. */removableGb?:number;missing_files?:string[]}[];/** What all models take on disk, from their real files, shared files counted once. */usage?:{total_bytes:number;total_gb:number;shared_bytes:number;shared_gb:number;models:Record<string,{bytes:number;gb:number}>}|null;data_folder:string;engine:{version:string|null}}|null;hardware:{ram_gb:number;engine_vendor?:string;gpus:{index:number;name:string;vram_gb:number;usable:boolean;/** SMs x clock: only the order between cards matters (strata_control/gpu_roles.py). */speed?:number;vendor?:string;uma?:boolean;integrated?:boolean;unified_gb?:number;problem?:string|null}[]}|null;fields:NativeField[];instances:NativeInstance[];defaultId:string|null;jobs:NativeJob[];capabilities:{/** The engine serves DFlash2 (Control says): the Drafting choice then offers it. */dflash2?:boolean;responses?:boolean;updates:boolean;rollback:boolean;calibration:boolean;componentUpdates?:boolean}}
|
||||
visionReader?: boolean;available:boolean;repo:string;downloadDirectory?:string;/** Free bytes on the drive that holds the models folder. */downloadFreeBytes?:number|null;downloadTotalBytes?:number|null;version?:string|null;summary:string;error:string|null;catalog:{families:NativeFamily[];contexts:number[];recommendation:{family:string|null;model:string|null;context?:number;why?:string};context_note:string}|null;installed:{models:{model:string;config:string;model_name:string;ready:boolean;/** The picture-reading files are on this computer (notes-split-agent8.md, item 8). */visionPrepared?:boolean;context?:number;kv?:string;images?:string;visionEnabled?:boolean;visionDevice?:string;lowRamMode?:string;resourceProfile?:'work'|'performance';/** The Drafting choice saved with the model: auto | mtp | dflash2 | off. */drafting?:string;/** DeepSeek: the most context its pack reads (max_position_embeddings in the pack's ds4.txt); absent when the pack does not say. */contextMax?:number;dataFolder?:string;diskGb?:number;diskBytes?:number;/** Files this model shares with another (the picture reader, the draft layer): counted once, in `usage`. */sharedBytes?:number;/** What Delete frees: files in Strata's own folders that no other model uses. */removableGb?:number;missing_files?:string[]}[];/** What all models take on disk, from their real files, shared files counted once. */usage?:{total_bytes:number;total_gb:number;shared_bytes:number;shared_gb:number;models:Record<string,{bytes:number;gb:number}>}|null;data_folder:string;engine:{version:string|null}}|null;hardware:{ram_gb:number;engine_vendor?:string;gpus:{index:number;name:string;vram_gb:number;usable:boolean;/** SMs x clock: only the order between cards matters (strata_control/gpu_roles.py). */speed?:number;vendor?:string;uma?:boolean;integrated?:boolean;unified_gb?:number;problem?:string|null}[]}|null;fields:NativeField[];instances:NativeInstance[];defaultId:string|null;jobs:NativeJob[];capabilities:{/** The engine serves DFlash2 (Control says): the Drafting choice then offers it. */dflash2?:boolean;responses?:boolean;updates:boolean;rollback:boolean;calibration:boolean;componentUpdates?:boolean}}
|
||||
let state:NativeState|null=null;
|
||||
const listeners=new Set<()=>void>();
|
||||
/** True once the first read of the native state has ended, whatever its answer: until then the Models page must not pick a model of its own (#3740). */
|
||||
|
||||
@@ -5,6 +5,7 @@ import { updateAgent } from './agents';
|
||||
import { LOOP_USAGE, parseLoop, type LoopCommand } from './loop';
|
||||
import { LoopScheduler } from './loop-runner';
|
||||
import { applyStatusFrame, clearStatus } from './live-status';
|
||||
import { approvalNeeded, workFinished } from './attention';
|
||||
import { applyGoalFrame } from './goal';
|
||||
import { tidyAnswer } from './answer-text';
|
||||
import { appendText, mergeDeliverable, readDeliverable, readGroup, readSkills, upsertGroup } from './deliverables';
|
||||
@@ -82,6 +83,13 @@ export interface AppState {
|
||||
transparency: 'translucent' | 'opaque';
|
||||
/** Chrome scale, independent of the conversation's reading size. */
|
||||
uiScale: number;
|
||||
/** Price of one kWh for the Monitor's cost line (0 = not set) and the currency the person picked instead of the detected one ('' = automatic) (#3628). */
|
||||
electricityPrice: number;
|
||||
electricityCurrency: string;
|
||||
/** Opt-in: a short sound when a task finishes (#3586). */
|
||||
doneSound: boolean;
|
||||
/** Opt-in: a desktop notification when Manual mode waits for an approval (#3477). */
|
||||
approvalNotify: boolean;
|
||||
/** Governs anonymous usage reporting. Nothing is sent in this build; the
|
||||
* choice is recorded so a version that does offer it starts from it. */
|
||||
shareTelemetry: boolean;
|
||||
@@ -143,6 +151,10 @@ export interface Prefs {
|
||||
motion: 'system' | 'full' | 'reduced';
|
||||
transparency: 'translucent' | 'opaque';
|
||||
uiScale: number;
|
||||
electricityPrice: number;
|
||||
electricityCurrency: string;
|
||||
doneSound: boolean;
|
||||
approvalNotify: boolean;
|
||||
/** Governs anonymous usage reporting. Nothing is sent in this build; the
|
||||
* choice is recorded so a version that does offer it starts from it. */
|
||||
shareTelemetry: boolean;
|
||||
@@ -160,6 +172,10 @@ const PREFS_DEFAULTS: Prefs = {
|
||||
motion: 'system',
|
||||
transparency: 'translucent',
|
||||
uiScale: 1,
|
||||
electricityPrice: 0,
|
||||
electricityCurrency: '',
|
||||
doneSound: false,
|
||||
approvalNotify: false,
|
||||
shareTelemetry: false,
|
||||
};
|
||||
|
||||
@@ -203,6 +219,11 @@ function loadPrefs(): Prefs {
|
||||
language: validLanguage(parsed.language) ? parsed.language : 'en',
|
||||
fontFamily: typeof parsed.fontFamily === 'string' ? parsed.fontFamily.slice(0,128).replace(/[\x00-\x1f]/g,'') : '',
|
||||
contentFontSize: Math.min(CONTENT_FS_MAX, Math.max(CONTENT_FS_MIN, Number(parsed.contentFontSize) || 14)),
|
||||
uiScale: [0.9, 1, 1.1, 1.25, 1.5, 1.75, 2].includes(Number(parsed.uiScale)) ? Number(parsed.uiScale) : 1,
|
||||
electricityPrice: Number.isFinite(Number(parsed.electricityPrice)) && Number(parsed.electricityPrice) > 0 && Number(parsed.electricityPrice) < 1000 ? Number(parsed.electricityPrice) : 0,
|
||||
electricityCurrency: typeof parsed.electricityCurrency === 'string' && /^[A-Za-z]{3}$/.test(parsed.electricityCurrency) ? parsed.electricityCurrency.toUpperCase() : '',
|
||||
doneSound: parsed.doneSound === true,
|
||||
approvalNotify: parsed.approvalNotify === true,
|
||||
};
|
||||
} catch {
|
||||
/* fall through to the next key */
|
||||
@@ -215,6 +236,9 @@ const prefs = loadPrefs();
|
||||
// Use the saved language for the very first render, before host bootstrap.
|
||||
setTranslationLanguage(prefs.language);
|
||||
|
||||
/** The last approval question a notification was shown for, so one question is announced once. */
|
||||
let lastApprovalNotice = '';
|
||||
|
||||
let state: AppState = {
|
||||
language: prefs.language,
|
||||
fontFamily: prefs.fontFamily,
|
||||
@@ -232,6 +256,10 @@ let state: AppState = {
|
||||
motion: prefs.motion,
|
||||
transparency: prefs.transparency,
|
||||
uiScale: prefs.uiScale,
|
||||
electricityPrice: prefs.electricityPrice,
|
||||
electricityCurrency: prefs.electricityCurrency,
|
||||
doneSound: prefs.doneSound,
|
||||
approvalNotify: prefs.approvalNotify,
|
||||
paletteOpen: false,
|
||||
shortcutsOpen: false,
|
||||
settingsSection: 'Appearance',
|
||||
@@ -422,9 +450,11 @@ function applyPrefs() {
|
||||
root.dataset.contrast = state.contrast;
|
||||
root.dataset.density = state.density;
|
||||
root.style.setProperty('--st-content-fs', `${state.contentFontSize}px`);
|
||||
// The chrome scale. The reading size is deliberately separate: someone who
|
||||
// needs larger prose does not necessarily want larger buttons.
|
||||
root.style.setProperty('--st-ui-scale', String(state.uiScale));
|
||||
// The interface size (#3970): the whole page is scaled, so every text, button, menu and dialog follows, not only the parts that were
|
||||
// written with the scale token. The token stays at 1 so nothing is scaled twice. The reading size stays separate: someone who needs
|
||||
// larger prose does not necessarily want larger buttons.
|
||||
root.style.setProperty('--st-ui-scale', '1');
|
||||
root.style.setProperty('zoom', state.uiScale === 1 ? '' : String(state.uiScale));
|
||||
|
||||
// "system" means the OS decides, so the attribute is absent rather than set
|
||||
// to a third value the stylesheet would have to special-case.
|
||||
@@ -452,6 +482,10 @@ function savePrefs() {
|
||||
motion: state.motion,
|
||||
transparency: state.transparency,
|
||||
uiScale: state.uiScale,
|
||||
electricityPrice: state.electricityPrice,
|
||||
electricityCurrency: state.electricityCurrency,
|
||||
doneSound: state.doneSound,
|
||||
approvalNotify: state.approvalNotify,
|
||||
shareTelemetry: state.shareTelemetry,
|
||||
};
|
||||
try {
|
||||
@@ -471,7 +505,17 @@ export function toggleRail() {
|
||||
set({ rail: !state.rail });
|
||||
savePrefs();
|
||||
}
|
||||
export function setLanguage(language:Language){if(!validLanguage(language))return;setTranslationLanguage(language);set({language});savePrefs();}
|
||||
export function setLanguage(language:Language){if(!validLanguage(language))return;setTranslationLanguage(language);set({language});savePrefs();rememberShellLanguage(language);}
|
||||
let shellAnnounced = false;
|
||||
/** The desktop shell's tray menu follows the app's language; it reads it from a small file the host keeps (no effect in a browser). */
|
||||
function rememberShellLanguage(language:Language){api.post('/api/shell/prefs',{language}).catch(()=>undefined);}
|
||||
/** The shell started on another port than it wanted: the chats and settings saved on that port are not here, so say so once (#3402). */
|
||||
async function announcePortNotice(){
|
||||
try{
|
||||
const shell=await api.get<{portNotice:{wanted:number;used:number}|null}>('/api/shell');
|
||||
if(shell.portNotice)pushToast('warn',tr('Strata could not use its port'),tr('Port {0} is used by another program, so Strata started on port {1}. Chats and settings saved on port {0} are not shown until Strata can use it again. Close the other program and restart Strata.',[shell.portNotice.wanted,shell.portNotice.used]));
|
||||
}catch{/* a browser or an older host has no shell */}
|
||||
}
|
||||
export function setFontFamily(fontFamily:string){set({fontFamily:fontFamily.trim().slice(0,128).replace(/[\x00-\x1f]/g,'')});savePrefs();}
|
||||
|
||||
export function setFocus(focus: boolean) {
|
||||
@@ -544,7 +588,24 @@ export function setTransparency(transparency: 'translucent' | 'opaque') {
|
||||
savePrefs();
|
||||
}
|
||||
|
||||
export const UI_SCALES = [0.9, 1, 1.1, 1.25] as const;
|
||||
/** The electricity price for the Monitor's cost line: a positive price per kWh; 0 turns the line off. The currency is the detected one unless a three-letter code is given (#3628). */
|
||||
export function setElectricity(price: number, currency: string) {
|
||||
const ok = Number.isFinite(price) && price > 0 && price < 1000;
|
||||
set({ electricityPrice: ok ? Math.round(price * 10000) / 10000 : 0, electricityCurrency: /^[A-Za-z]{3}$/.test(currency) ? currency.toUpperCase() : '' });
|
||||
savePrefs();
|
||||
}
|
||||
|
||||
export function setDoneSound(doneSound: boolean) {
|
||||
set({ doneSound });
|
||||
savePrefs();
|
||||
}
|
||||
|
||||
export function setApprovalNotify(approvalNotify: boolean) {
|
||||
set({ approvalNotify });
|
||||
savePrefs();
|
||||
}
|
||||
|
||||
export const UI_SCALES = [0.9, 1, 1.1, 1.25, 1.5, 1.75, 2] as const;
|
||||
|
||||
export function setUiScale(uiScale: number) {
|
||||
set({ uiScale });
|
||||
@@ -680,8 +741,8 @@ function applyEvent(e: StreamEvent) {
|
||||
// A task finished on its own (the host ran it on schedule): say so, and say when it needs a person's yes or failed.
|
||||
const t = data as unknown as { title?: string; status?: string; attention?: string | null; error?: string | null };
|
||||
if (t.status === 'error') pushToast('error', 'Task failed', String(t.title ?? ''));
|
||||
else if (t.attention) pushToast('warn', 'Task needs your yes', String(t.title ?? ''));
|
||||
else if (t.status === 'done') pushToast('success', 'Task finished', String(t.title ?? ''));
|
||||
else if (t.attention) { pushToast('warn', 'Task needs your yes', String(t.title ?? '')); void approvalNeeded(state.approvalNotify, String(t.title ?? '')); }
|
||||
else if (t.status === 'done') { pushToast('success', 'Task finished', String(t.title ?? '')); workFinished(state.doneSound, Infinity); }
|
||||
break;
|
||||
}
|
||||
case 'agent':
|
||||
@@ -726,6 +787,7 @@ export async function bootstrap() {
|
||||
connection: 'live',
|
||||
});
|
||||
applyProviders(providers);
|
||||
if (!shellAnnounced) { shellAnnounced = true; void announcePortNotice(); rememberShellLanguage(state.language); }
|
||||
closeEvents?.();
|
||||
let reconnect = false;
|
||||
closeEvents = onEvent(applyEvent, (status) => {
|
||||
@@ -1746,6 +1808,12 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
|
||||
patchAssistant((m) => ({ ...m, demo: frame.demo === true, remoteModel: frame.remoteModel === true, requestId: typeof frame.requestId === 'string' ? frame.requestId : m.requestId, model: String(frame.model), contextMax: Number(frame.contextMax), skillsUsed: Array.isArray(frame.skills) ? frame.skills.map(String) : [] }));
|
||||
} else if (type === 'status') {
|
||||
applyStatusFrame(sessionId, frame as Record<string, unknown>);
|
||||
// Manual mode waits for the person: a notification when they have opted in and Strata is in the background (#3477), once per question.
|
||||
if (frame.code === 'waiting-approval' && lastApprovalNotice !== `${sessionId}:${String(frame.seq)}`) {
|
||||
lastApprovalNotice = `${sessionId}:${String(frame.seq)}`;
|
||||
const tool = (frame.args as { tool?: unknown } | undefined)?.tool;
|
||||
void approvalNeeded(state.approvalNotify, typeof tool === 'string' ? tool : '');
|
||||
}
|
||||
} else if (type === 'goal') {
|
||||
applyGoalFrame(sessionId, frame.goal);
|
||||
} else if (type === 'plan') {
|
||||
@@ -1820,6 +1888,7 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
|
||||
patchAssistant((m) => ({ ...m, error: said, streaming: false, ...(typeof frame.code === 'string' ? { errorCode: frame.code } : {}), ...(typeof frame.details === 'string' && frame.details ? { errorDetail: frame.details } : {}) }));
|
||||
} else if (type === 'done') {
|
||||
clearStatus(sessionId);
|
||||
workFinished(state.doneSound, Number(frame.durationMs ?? performance.now() - started));
|
||||
patchSession(sessionId, (sess) => ({
|
||||
...sess,
|
||||
tokensIn: sess.tokensIn + Number(frame.tokensIn ?? 0),
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface HardwareCheck {
|
||||
|
||||
export interface ProcessorInfo { id:string; socket:string; model:string; cores:number; threads:number; arch:string; ghz:number; logicalIds?:number[] }
|
||||
export interface CpuReading { id:string; utilizationPct:number|null }
|
||||
export interface GpuReading { id:string; temperatureC:number|null; utilizationPct:number|null; memoryUsedGb:number|null; source:string; sampledAt:string; powerW?:number|null; powerLimitW?:number|null; pcieGen?:number|null; pcieGenMax?:number|null; pcieWidth?:number|null; pcieRxMbs?:number|null }
|
||||
export interface GpuReading { id:string; temperatureC:number|null; utilizationPct:number|null; memoryUsedGb:number|null; source:string; sampledAt:string; powerW?:number|null; powerLimitW?:number|null; pcieGen?:number|null; pcieGenMax?:number|null; pcieWidth?:number|null; pcieRxMbs?:number|null; fanPct?:number|null; clockMhz?:number|null }
|
||||
|
||||
export interface GpuInfo {
|
||||
id?: string;
|
||||
@@ -285,6 +285,10 @@ export interface MonitorState {
|
||||
powerW?: number | null;
|
||||
powerLimitW?: number | null;
|
||||
pcieRxMbs?: number | null;
|
||||
/** CPU and SSD temperatures (only where this PC can give one) and the GPU energy used since the host started, in Wh (#3628). */
|
||||
cpuTempC?: number | null;
|
||||
ssdTempC?: number | null;
|
||||
energyWh?: number | null;
|
||||
diskReadGbs?: number | null;
|
||||
diskWriteMbs?: number | null;
|
||||
gpuLoad: number | null;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useStore } from './store';
|
||||
import { useNative } from './native';
|
||||
import { appVersionLabel } from './app-version';
|
||||
import { shownEngineVersion } from './engine-version';
|
||||
import { readUpdateCache, saveUpdateCache, type InstalledVersions } from './update-cache';
|
||||
import type { UpdateCheckResult } from './types';
|
||||
|
||||
@@ -10,7 +11,7 @@ export function useInstalledVersions(): InstalledVersions {
|
||||
const native = useNative();
|
||||
const engineVersion = useStore(s => s.engine?.engine.version);
|
||||
const appVersion = appVersionLabel(useStore(s => s.setup?.app.version), native?.version);
|
||||
return { appVersion: appVersion ?? null, engineVersion: (native?.available ? native.installed?.engine?.version : engineVersion) ?? null, controlVersion: native?.version ?? null };
|
||||
return { appVersion: appVersion ?? null, engineVersion: shownEngineVersion(native, engineVersion) ?? null, controlVersion: native?.version ?? null };
|
||||
}
|
||||
|
||||
/** The one place the app asks the host to check for updates. Called only from a button press. */
|
||||
|
||||
@@ -67,5 +67,19 @@
|
||||
"fx66": "Standard (Schneller auf diesem PC)",
|
||||
"fx67": "Standard (Volle Qualität auf diesem PC)",
|
||||
"fx68": "Schneller (Standard auf diesem PC)",
|
||||
"fx69": "Volle Qualität (Standard auf diesem PC)"
|
||||
"fx69": "Volle Qualität (Standard auf diesem PC)",
|
||||
"fx70": "Lange Prompts schneller lesen",
|
||||
"fx71": "Schnellere Antworten auf Radeon RX 9000 / AI PRO R9700",
|
||||
"fx72": "DeepSeek-Beschleunigungen",
|
||||
"fx73": "Liest sehr lange Prompts schneller, indem ein Teil der Arbeit angenähert wird. Standardmäßig aus: Antworten zu langen Dokumenten können Einzelheiten auslassen oder verwechseln.",
|
||||
"fx74": "Verteilt die Arbeit so zwischen Grafikkarte und Prozessor, dass Antworten auf diesen Karten schneller werden. Ausschalten stellt die vorherige, langsamere Einstellung wieder her.",
|
||||
"fx75": "Liest lange Prompts schneller und lädt die benötigten Teile des Modells schneller. Schalten Sie die Option aus, wenn DeepSeek nicht mehr funktioniert oder die Antworten falsch aussehen; dann läuft es wie in Version 0.2.6.",
|
||||
"fx76": "Ein (empfohlen)",
|
||||
"fx77": "Das ist ein sehr großes Modell. Der Download umfasst {0} GB und braucht etwa so viel freien Speicherplatz. Es läuft auf NVIDIA-RTX-Karten der Serien 30, 40 und 50, auf einer Radeon AI PRO R9700 (Linux) oder auf einem Ryzen-AI-Max-/Strix-Halo-PC (Windows); wie schnell es ist, hängt von Speicher und Grafikkarte ab (die Zeile darunter sagt, was auf diesem Computer zu erwarten ist). Was nicht in den Speicher passt, wird von deiner SSD gelesen, deshalb zählt eine NVMe-SSD.",
|
||||
"fx78": "Noch nicht getestet. Ein so langer Kontext nimmt dem Modell Grafikspeicher weg, und lange Prompts werden langsam gelesen.",
|
||||
"fx79": "Aus (empfohlen)",
|
||||
"fx80": "Braucht etwa {0} GB freien Speicherplatz. Was nicht in den Speicher passt, wird von Ihrer SSD gelesen.",
|
||||
"fx81": "{0} RAM + VRAM, dazu etwa {1} GB Speicherplatz",
|
||||
"fx82": "Schnellere Antworten auf NVIDIA-Karten mit 12 GB",
|
||||
"fx83": "Bevorzugt die Teile des Modells, die schon auf der Grafikkarte liegen. Das macht Antworten auf Karten mit 12 GB deutlich schneller. Schalten Sie die Option aus, wenn die Antworten schlechter aussehen als vorher."
|
||||
}
|
||||
@@ -67,5 +67,19 @@
|
||||
"fx66": "Default (Faster on this PC)",
|
||||
"fx67": "Default (Full quality on this PC)",
|
||||
"fx68": "Faster (default on this PC)",
|
||||
"fx69": "Full quality (default on this PC)"
|
||||
"fx69": "Full quality (default on this PC)",
|
||||
"fx70": "Faster long prompts",
|
||||
"fx71": "Faster answers on Radeon RX 9000 / AI PRO R9700",
|
||||
"fx72": "DeepSeek speed-ups",
|
||||
"fx73": "Reads very long prompts faster by approximating part of the work. Off by default: answers about long documents can miss or mix up details.",
|
||||
"fx74": "Shares the work between the graphics card and the processor in a way that makes answers faster on these cards. Turn off to go back to the previous setting, which is slower.",
|
||||
"fx75": "Reads long prompts faster and loads the parts of the model it needs faster. Turn off if DeepSeek stops working or its answers look wrong; it then runs as in version 0.2.6.",
|
||||
"fx76": "On (recommended)",
|
||||
"fx77": "This is a very large model. The download is {0} GB and it needs about as much free disk space. It runs on NVIDIA RTX 30, 40 and 50 cards, on a Radeon AI PRO R9700 (Linux) or on a Ryzen AI Max / Strix Halo PC (Windows); how fast it is depends on the memory and card (the line below says what to expect on this computer). What does not fit in memory is read from your SSD, so an NVMe drive matters.",
|
||||
"fx78": "Not tested yet. A context this long takes graphics memory from the model and reads long prompts slowly.",
|
||||
"fx79": "Off (recommended)",
|
||||
"fx80": "Needs about {0} GB of free disk space. What does not fit in memory is read from your SSD.",
|
||||
"fx81": "{0} RAM + VRAM, plus about {1} GB of disk",
|
||||
"fx82": "Faster answers on 12 GB NVIDIA cards",
|
||||
"fx83": "Prefers the parts of the model already on the graphics card, which makes answers much faster on 12 GB cards. Turn off if answers look worse than before."
|
||||
}
|
||||
@@ -67,5 +67,19 @@
|
||||
"fx66": "Predeterminado (Más rápido en este PC)",
|
||||
"fx67": "Predeterminado (Calidad completa en este PC)",
|
||||
"fx68": "Más rápido (predeterminado en este PC)",
|
||||
"fx69": "Calidad completa (predeterminado en este PC)"
|
||||
"fx69": "Calidad completa (predeterminado en este PC)",
|
||||
"fx70": "Prompts largos más rápidos",
|
||||
"fx71": "Respuestas más rápidas en Radeon RX 9000 / AI PRO R9700",
|
||||
"fx72": "Aceleraciones de DeepSeek",
|
||||
"fx73": "Lee prompts muy largos más rápido aproximando una parte del trabajo. Desactivado de forma predeterminada: las respuestas sobre documentos largos pueden omitir o confundir detalles.",
|
||||
"fx74": "Reparte el trabajo entre la tarjeta gráfica y el procesador de una forma que acelera las respuestas en estas tarjetas. Si lo desactivas, vuelve el ajuste anterior, que es más lento.",
|
||||
"fx75": "Lee los prompts largos más rápido y carga más rápido las partes del modelo que necesita. Desactívalo si DeepSeek deja de funcionar o sus respuestas parecen incorrectas; entonces funciona como en la versión 0.2.6.",
|
||||
"fx76": "Activado (recomendado)",
|
||||
"fx77": "Es un modelo muy grande. La descarga es de {0} GB y necesita casi el mismo espacio libre en disco. Funciona con tarjetas NVIDIA RTX de las series 30, 40 y 50, con una Radeon AI PRO R9700 (Linux) o en un PC con Ryzen AI Max / Strix Halo (Windows); su velocidad depende de la memoria y de la tarjeta (la línea de abajo indica qué esperar en este equipo). Lo que no cabe en memoria se lee de tu SSD, así que importa que sea NVMe.",
|
||||
"fx78": "Aún sin probar. Un contexto tan largo le quita memoria gráfica al modelo y lee los prompts largos con lentitud.",
|
||||
"fx79": "Desactivado (recomendado)",
|
||||
"fx80": "Necesita unos {0} GB de espacio libre en disco. Lo que no cabe en memoria se lee de tu SSD.",
|
||||
"fx81": "{0} de RAM + VRAM, más unos {1} GB de disco",
|
||||
"fx82": "Respuestas más rápidas en tarjetas NVIDIA de 12 GB",
|
||||
"fx83": "Prefiere las partes del modelo que ya están en la tarjeta gráfica, lo que hace las respuestas mucho más rápidas en tarjetas de 12 GB. Desactívalo si las respuestas parecen peores que antes."
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"fx01": "ne s’exécutera plus et sera retirée.",
|
||||
"fx02": "Téléchargé, mais pas chargé. Ouvrez Modèles pour le charger.",
|
||||
"fx03": "{0} lignes ajoutées, {1} supprimées",
|
||||
"fx04": "{0} fichiers modifiés",
|
||||
"fx05": "Déposez des fichiers pour les ajouter",
|
||||
"fx06": "Images et fichiers texte",
|
||||
"fx07": "Garder l’objectif",
|
||||
"fx08": "Supprimer l’objectif",
|
||||
"fx09": "L’assistant cesse de travailler à l’atteindre. Votre conversation et vos fichiers restent comme ils sont.",
|
||||
"fx10": "Ce modèle peut lire les images, mais le moteur installé ici n’inclut pas encore le lecteur d’images : Lire les images est donc désactivé. Mettez à jour le moteur dans Paramètres → À propos pour l’activer.",
|
||||
"fx11": "Affichage des valeurs de votre optimisation « {0} ».",
|
||||
"fx12": "Votre optimisation « {0} » règle",
|
||||
"fx13": "était {0}",
|
||||
"fx14": "· {0} de RAM",
|
||||
"fx15": "Le message n’a pas été annulé",
|
||||
"fx16": "Réglages supplémentaires pour le programme",
|
||||
"fx17": "Rédiger avec l’IA",
|
||||
"fx18": "Temps de réflexion",
|
||||
"fx19": "Temps de rédaction",
|
||||
"fx20": "tokens/s",
|
||||
"fx21": "% du contexte",
|
||||
"fx22": "Cette page demande des fichiers à internet, que l’aperçu ne charge pas : elle peut donc paraître incomplète. Voir le code montre exactement ce qui a été écrit.",
|
||||
"fx23": "Garder A",
|
||||
"fx24": "Garder B",
|
||||
"fx25": "A pris",
|
||||
"fx26": "Variante",
|
||||
"fx27": "Modifié",
|
||||
"fx28": "Rien",
|
||||
"fx29": "Les deux versions sont prêtes. Comparez A et B, puis gardez-en une.",
|
||||
"fx30": "L’assistant répond encore",
|
||||
"fx31": "Envoyez votre message quand il a terminé.",
|
||||
"fx32": "Le message n’a pas été mis en file d’attente",
|
||||
"fx33": "Aller au dernier message",
|
||||
"fx34": "Retirer la tâche",
|
||||
"fx35": "Ouvrir une nouvelle conversation ; garder celle-ci",
|
||||
"fx36": "Faire d’abord rédiger un plan à l’assistant ; rien ne s’exécute tant que vous ne l’avez pas approuvé",
|
||||
"fx37": "Chercher sur le web, lire les pages et répondre avec des sources",
|
||||
"fx38": "Copié",
|
||||
"fx39": "Réponse",
|
||||
"fx40": "Message",
|
||||
"fx41": "PDF ajouté",
|
||||
"fx42": "Sélectionnez le texte et copiez-le avec Ctrl+C.",
|
||||
"fx43": "{0} est dans cette conversation. Posez une question à son sujet dans votre prochain message.",
|
||||
"fx44": "Le moteur est prêt.",
|
||||
"fx45": "Étape suivante : choisir un modèle.",
|
||||
"fx46": "Téléchargement du moteur",
|
||||
"fx47": "Vous pouvez fermer Strata ; le téléchargement reprendra là où il s’est arrêté.",
|
||||
"fx48": "Bienvenue dans Strata",
|
||||
"fx49": "Examen de votre carte graphique…",
|
||||
"fx50": "Strata a trouvé votre carte graphique :",
|
||||
"fx51": "{0} cartes trouvées. Strata utilise la première pour le moteur ; vous pourrez changer cela plus tard dans les Paramètres.",
|
||||
"fx52": "Strata n’a pas reconnu de carte graphique prise en charge.",
|
||||
"fx53": "Pour y exécuter des modèles, Strata a besoin d’un téléchargement :",
|
||||
"fx54": "Le moteur pour cette carte",
|
||||
"fx55": "Le moteur a besoin d’environ {0} Go d’espace libre sur ce disque et il n’y en a que {1} Go. Libérez de l’espace, puis continuez.",
|
||||
"fx56": "Il vient du serveur de mise à jour de Strata et il est vérifié avant utilisation. Vos modèles et vos conversations ne sont jamais touchés.",
|
||||
"fx57": "Strata n’a pas pu joindre github.com. Vérifiez votre connexion et réessayez, ou téléchargez le fichier du moteur sur un autre PC et choisissez-le ici.",
|
||||
"fx58": "Le moteur installé ici est prévu pour une autre marque de carte graphique.",
|
||||
"fx59": "Strata ne peut pas choisir de moteur pour ce PC dans sa liste de mises à jour.",
|
||||
"fx60": "Continuer sans moteur",
|
||||
"fx61": "Le téléchargement s’est arrêté. Continuer le reprend là où il s’est arrêté.",
|
||||
"fx62": "C’est un très gros modèle. Le téléchargement fait {0} Go et il faut à peu près autant d’espace disque libre. Il tourne sur une carte graphique de 12 Go avec 46 à 64 Go de RAM, lentement : environ 5 à 7 mots par seconde. Ce qui ne tient pas en mémoire est lu depuis votre SSD, un disque NVMe compte donc beaucoup.",
|
||||
"fx63": "Vous pouvez arrêter le téléchargement et le continuer plus tard ; il reprend là où il s’est arrêté. Strata vous avertit ici mais ne vous bloque jamais.",
|
||||
"fx64": "Plus rapide",
|
||||
"fx65": "Qualité maximale",
|
||||
"fx66": "Par défaut (Plus rapide sur ce PC)",
|
||||
"fx67": "Par défaut (Qualité maximale sur ce PC)",
|
||||
"fx68": "Plus rapide (par défaut sur ce PC)",
|
||||
"fx69": "Qualité maximale (par défaut sur ce PC)",
|
||||
"fx70": "Longs prompts plus rapides",
|
||||
"fx71": "Réponses plus rapides sur Radeon RX 9000 / AI PRO R9700",
|
||||
"fx72": "Accélérations DeepSeek",
|
||||
"fx73": "Lit les très longs prompts plus vite en approximant une partie du travail. Désactivé par défaut : les réponses sur de longs documents peuvent manquer ou mélanger des détails.",
|
||||
"fx74": "Répartit le travail entre la carte graphique et le processeur d’une façon qui accélère les réponses sur ces cartes. Désactivez pour revenir au réglage précédent, plus lent.",
|
||||
"fx75": "Lit les longs prompts plus vite et charge plus vite les parties du modèle dont il a besoin. Désactivez si DeepSeek cesse de fonctionner ou si ses réponses semblent fausses ; il fonctionne alors comme dans la version 0.2.6.",
|
||||
"fx76": "Activé (recommandé)",
|
||||
"fx77": "C’est un très gros modèle. Le téléchargement fait {0} Go et il faut à peu près autant d’espace disque libre. Il tourne sur les cartes NVIDIA RTX 30, 40 et 50, sur une Radeon AI PRO R9700 (Linux) ou sur un PC Ryzen AI Max / Strix Halo (Windows) ; sa vitesse dépend de la mémoire et de la carte (la ligne ci-dessous indique à quoi s’attendre sur cet ordinateur). Ce qui ne tient pas en mémoire est lu depuis votre SSD, un disque NVMe compte donc beaucoup.",
|
||||
"fx78": "Pas encore testé. Un contexte aussi long prend de la mémoire graphique au modèle et lit lentement les longs prompts.",
|
||||
"fx79": "Désactivé (recommandé)",
|
||||
"fx80": "Il faut environ {0} Go d’espace disque libre. Ce qui ne tient pas en mémoire est lu depuis votre SSD.",
|
||||
"fx81": "{0} de RAM + VRAM, plus environ {1} Go de disque",
|
||||
"fx82": "Réponses plus rapides sur les cartes NVIDIA de 12 Go",
|
||||
"fx83": "Privilégie les parties du modèle déjà présentes sur la carte graphique, ce qui accélère nettement les réponses sur les cartes de 12 Go. Désactivez-le si les réponses semblent moins bonnes qu’avant."
|
||||
}
|
||||
@@ -67,5 +67,19 @@
|
||||
"fx66": "既定(この PC では「高速」)",
|
||||
"fx67": "既定(この PC では「フル品質」)",
|
||||
"fx68": "高速(この PC の既定)",
|
||||
"fx69": "フル品質(この PC の既定)"
|
||||
"fx69": "フル品質(この PC の既定)",
|
||||
"fx70": "長いプロンプトを高速化",
|
||||
"fx71": "Radeon RX 9000 / AI PRO R9700 での高速な応答",
|
||||
"fx72": "DeepSeek の高速化",
|
||||
"fx73": "処理の一部を近似することで、とても長いプロンプトをより速く読み込みます。既定ではオフです。長い文書についての回答で、細部が抜けたり混ざったりすることがあります。",
|
||||
"fx74": "これらのグラフィックスカードで応答が速くなるよう、グラフィックスカードとプロセッサーの間で作業を分担します。オフにすると、以前の(より遅い)設定に戻ります。",
|
||||
"fx75": "長いプロンプトと、必要なモデルの部分をより速く読み込みます。DeepSeek が動かなくなったり、回答がおかしいと感じたりしたらオフにしてください。バージョン 0.2.6 と同じ動作になります。",
|
||||
"fx76": "オン(推奨)",
|
||||
"fx77": "とても大きなモデルです。ダウンロードは {0} GB で、ほぼ同じ大きさの空き容量が必要です。NVIDIA RTX 30・40・50 シリーズ、Radeon AI PRO R9700(Linux)、または Ryzen AI Max / Strix Halo の PC(Windows)で動きます。速さはメモリとカードによって変わります(この PC での目安は下の行に表示されます)。メモリに入らない分は SSD から読むため、NVMe ドライブが重要です。",
|
||||
"fx78": "まだテストされていません。これほど長いコンテキストは、モデルが使うグラフィックスメモリを消費し、長いプロンプトの読み込みも遅くなります。",
|
||||
"fx79": "オフ(推奨)",
|
||||
"fx80": "約 {0} GB の空きディスク容量が必要です。メモリに入らない分は SSD から読み込みます。",
|
||||
"fx81": "RAM + VRAM {0}、ほかにディスク約 {1} GB",
|
||||
"fx82": "12 GB の NVIDIA カードでの高速な応答",
|
||||
"fx83": "グラフィックスカード上にすでにあるモデルの部分を優先して使うため、12 GB のカードで応答がとても速くなります。以前より回答が悪くなったと感じたらオフにしてください。"
|
||||
}
|
||||
@@ -67,5 +67,19 @@
|
||||
"fx66": "기본값(이 PC에서는 더 빠르게)",
|
||||
"fx67": "기본값(이 PC에서는 전체 품질)",
|
||||
"fx68": "더 빠르게(이 PC의 기본값)",
|
||||
"fx69": "전체 품질(이 PC의 기본값)"
|
||||
"fx69": "전체 품질(이 PC의 기본값)",
|
||||
"fx70": "긴 프롬프트 더 빠르게 읽기",
|
||||
"fx71": "Radeon RX 9000 / AI PRO R9700에서 더 빠른 응답",
|
||||
"fx72": "DeepSeek 속도 향상",
|
||||
"fx73": "작업의 일부를 근사 처리하여 매우 긴 프롬프트를 더 빠르게 읽습니다. 기본값은 꺼짐입니다. 긴 문서에 대한 답변에서 세부 내용이 빠지거나 뒤섞일 수 있습니다.",
|
||||
"fx74": "이 그래픽 카드에서 응답이 빨라지도록 그래픽 카드와 프로세서가 작업을 나눕니다. 끄면 이전의 더 느린 설정으로 돌아갑니다.",
|
||||
"fx75": "긴 프롬프트와 필요한 모델 부분을 더 빠르게 읽고 불러옵니다. DeepSeek가 멈추거나 답변이 이상해 보이면 끄세요. 그러면 0.2.6 버전처럼 동작합니다.",
|
||||
"fx76": "켬(권장)",
|
||||
"fx77": "매우 큰 모델입니다. 다운로드는 {0}GB이고, 거의 같은 크기의 여유 디스크 공간이 필요합니다. NVIDIA RTX 30·40·50 시리즈 카드, Radeon AI PRO R9700(Linux), 또는 Ryzen AI Max / Strix Halo PC(Windows)에서 실행됩니다. 속도는 메모리와 카드에 따라 달라집니다(아래 줄에 이 컴퓨터에서 기대할 수 있는 속도가 표시됩니다). 메모리에 들어가지 않는 부분은 SSD에서 읽으므로 NVMe 드라이브가 중요합니다.",
|
||||
"fx78": "아직 테스트하지 않았습니다. 이렇게 긴 컨텍스트는 모델이 쓸 그래픽 메모리를 차지하고, 긴 프롬프트를 느리게 읽습니다.",
|
||||
"fx79": "끔(권장)",
|
||||
"fx80": "약 {0}GB의 여유 디스크 공간이 필요합니다. 메모리에 들어가지 않는 부분은 SSD에서 읽습니다.",
|
||||
"fx81": "RAM + VRAM {0}, 디스크 약 {1}GB 추가",
|
||||
"fx82": "12GB NVIDIA 카드에서 더 빠른 응답",
|
||||
"fx83": "이미 그래픽 카드에 올라와 있는 모델 부분을 우선 사용해 12GB 카드에서 응답이 훨씬 빨라집니다. 답변이 이전보다 나빠 보이면 끄세요."
|
||||
}
|
||||
@@ -67,5 +67,19 @@
|
||||
"fx66": "Padrão (Mais rápido neste PC)",
|
||||
"fx67": "Padrão (Qualidade total neste PC)",
|
||||
"fx68": "Mais rápido (padrão neste PC)",
|
||||
"fx69": "Qualidade total (padrão neste PC)"
|
||||
"fx69": "Qualidade total (padrão neste PC)",
|
||||
"fx70": "Prompts longos mais rápidos",
|
||||
"fx71": "Respostas mais rápidas em Radeon RX 9000 / AI PRO R9700",
|
||||
"fx72": "Acelerações do DeepSeek",
|
||||
"fx73": "Lê prompts muito longos mais rápido aproximando parte do trabalho. Desativado por padrão: as respostas sobre documentos longos podem omitir ou misturar detalhes.",
|
||||
"fx74": "Divide o trabalho entre a placa de vídeo e o processador de um jeito que deixa as respostas mais rápidas nessas placas. Ao desativar, volta a configuração anterior, que é mais lenta.",
|
||||
"fx75": "Lê prompts longos mais rápido e carrega mais rápido as partes do modelo de que precisa. Desative se o DeepSeek parar de funcionar ou as respostas parecerem erradas; ele passa a funcionar como na versão 0.2.6.",
|
||||
"fx76": "Ativado (recomendado)",
|
||||
"fx77": "Este é um modelo muito grande. O download tem {0} GB e precisa de quase o mesmo espaço livre em disco. Ele roda em placas NVIDIA RTX das séries 30, 40 e 50, em uma Radeon AI PRO R9700 (Linux) ou em um PC com Ryzen AI Max / Strix Halo (Windows); a velocidade depende da memória e da placa (a linha abaixo diz o que esperar neste computador). O que não cabe na memória é lido do seu SSD, então um NVMe faz diferença.",
|
||||
"fx78": "Ainda não testado. Um contexto tão longo tira memória gráfica do modelo e lê prompts longos devagar.",
|
||||
"fx79": "Desativado (recomendado)",
|
||||
"fx80": "Precisa de cerca de {0} GB de espaço livre em disco. O que não cabe na memória é lido do seu SSD.",
|
||||
"fx81": "{0} de RAM + VRAM, mais cerca de {1} GB de disco",
|
||||
"fx82": "Respostas mais rápidas em placas NVIDIA de 12 GB",
|
||||
"fx83": "Prefere as partes do modelo que já estão na placa de vídeo, o que deixa as respostas bem mais rápidas em placas de 12 GB. Desative se as respostas parecerem piores do que antes."
|
||||
}
|
||||
@@ -67,5 +67,19 @@
|
||||
"fx66": "По умолчанию (на этом компьютере — «Быстрее»)",
|
||||
"fx67": "По умолчанию (на этом компьютере — «Полное качество»)",
|
||||
"fx68": "Быстрее (по умолчанию на этом компьютере)",
|
||||
"fx69": "Полное качество (по умолчанию на этом компьютере)"
|
||||
"fx69": "Полное качество (по умолчанию на этом компьютере)",
|
||||
"fx70": "Более быстрое чтение длинных запросов",
|
||||
"fx71": "Более быстрые ответы на Radeon RX 9000 / AI PRO R9700",
|
||||
"fx72": "Ускорения DeepSeek",
|
||||
"fx73": "Читает очень длинные запросы быстрее, приближённо выполняя часть работы. По умолчанию выключено: в ответах по длинным документам могут пропадать или путаться детали.",
|
||||
"fx74": "Распределяет работу между видеокартой и процессором так, что ответы на этих видеокартах получаются быстрее. Если отключить, вернётся прежняя, более медленная настройка.",
|
||||
"fx75": "Быстрее читает длинные запросы и быстрее загружает нужные части модели. Отключите, если DeepSeek перестаёт работать или его ответы выглядят неверными; тогда он работает как в версии 0.2.6.",
|
||||
"fx76": "Вкл. (рекомендуется)",
|
||||
"fx77": "Это очень большая модель. Загрузка — {0} ГБ, и нужно почти столько же свободного места на диске. Она работает на видеокартах NVIDIA RTX серий 30, 40 и 50, на Radeon AI PRO R9700 (Linux) или на ПК с Ryzen AI Max / Strix Halo (Windows); скорость зависит от памяти и видеокарты (строка ниже подскажет, чего ждать на этом компьютере). То, что не помещается в память, читается с вашего SSD, поэтому важен накопитель NVMe.",
|
||||
"fx78": "Ещё не проверено. Такой длинный контекст отнимает у модели видеопамять, а длинные запросы читаются медленно.",
|
||||
"fx79": "Выкл. (рекомендуется)",
|
||||
"fx80": "Нужно около {0} ГБ свободного места на диске. То, что не помещается в память, читается с вашего SSD.",
|
||||
"fx81": "{0} RAM + VRAM, плюс около {1} ГБ на диске",
|
||||
"fx82": "Более быстрые ответы на видеокартах NVIDIA с 12 ГБ",
|
||||
"fx83": "Отдаёт предпочтение частям модели, которые уже находятся на видеокарте, поэтому ответы на картах с 12 ГБ становятся намного быстрее. Отключите, если ответы стали выглядеть хуже, чем раньше."
|
||||
}
|
||||
@@ -67,5 +67,19 @@
|
||||
"fx66": "默认(这台电脑上为“更快”)",
|
||||
"fx67": "默认(这台电脑上为“完整质量”)",
|
||||
"fx68": "更快(这台电脑上的默认)",
|
||||
"fx69": "完整质量(这台电脑上的默认)"
|
||||
"fx69": "完整质量(这台电脑上的默认)",
|
||||
"fx70": "更快读取长提示词",
|
||||
"fx71": "Radeon RX 9000 / AI PRO R9700 上更快的回答",
|
||||
"fx72": "DeepSeek 加速",
|
||||
"fx73": "通过近似处理部分计算,更快读取很长的提示词。默认关闭:关于长文档的回答可能遗漏或混淆细节。",
|
||||
"fx74": "以更适合这些显卡的方式在显卡和处理器之间分配工作,使回答更快。关闭后恢复到之前较慢的设置。",
|
||||
"fx75": "更快读取长提示词,并更快加载所需的模型部分。如果 DeepSeek 无法运行或回答看起来不对,请关闭;关闭后它将像 0.2.6 版本那样运行。",
|
||||
"fx76": "开启(推荐)",
|
||||
"fx77": "这是一个非常大的模型。下载大小为 {0} GB,需要差不多同样大小的可用磁盘空间。它可在 NVIDIA RTX 30、40 和 50 系列显卡、Radeon AI PRO R9700(Linux)或 Ryzen AI Max / Strix Halo 电脑(Windows)上运行;速度取决于内存和显卡(下面一行会说明在这台电脑上的预期)。内存放不下的部分会从你的 SSD 读取,所以 NVMe 硬盘很重要。",
|
||||
"fx78": "尚未测试。这么长的上下文会占用本该给模型使用的显存,读取长提示词也会很慢。",
|
||||
"fx79": "关闭(推荐)",
|
||||
"fx80": "大约需要 {0} GB 可用磁盘空间。内存放不下的部分会从你的 SSD 读取。",
|
||||
"fx81": "{0} 内存 + 显存,另需约 {1} GB 磁盘空间",
|
||||
"fx82": "12 GB NVIDIA 显卡上更快的回答",
|
||||
"fx83": "优先使用已经在显卡上的模型部分,使 12 GB 显卡上的回答快很多。如果回答看起来比以前差,请关闭。"
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"gh01": "Energie seit dem Start",
|
||||
"gh02": "Kosten seit dem Start",
|
||||
"gh03": "GPU-Lüfter",
|
||||
"gh04": "GPU-Takt",
|
||||
"gh05": "CPU-Temperatur",
|
||||
"gh06": "SSD-Temperatur",
|
||||
"gh07": "Strompreis",
|
||||
"gh08": "Preis einer Kilowattstunde. Der Monitor zeigt dann, was Ihre Grafikkarte seit dem Start von Strata gekostet hat. Leer lassen, um die Kosten auszublenden.",
|
||||
"gh09": "Preis pro kWh",
|
||||
"gh10": "Ton, wenn Arbeit fertig ist",
|
||||
"gh11": "Spielt einen kurzen Ton, wenn eine Antwort oder Aufgabe, die länger gedauert hat, fertig ist oder wenn Strata im Hintergrund läuft.",
|
||||
"gh12": "Ton abspielen",
|
||||
"gh13": "Benachrichtigen, wenn Strata eine Antwort braucht",
|
||||
"gh14": "Zeigt im manuellen Modus eine Desktop-Benachrichtigung, wenn der Assistent auf Ihre Freigabe wartet und Strata im Hintergrund läuft.",
|
||||
"gh15": "Strata braucht Ihre Antwort",
|
||||
"gh16": "Der Assistent möchte {0} verwenden. Öffnen Sie Strata, um zu erlauben oder abzulehnen.",
|
||||
"gh17": "ein Werkzeug",
|
||||
"gh18": "In den Infobereich minimieren",
|
||||
"gh19": "Beim Schließen des Fensters läuft Strata im Infobereich neben der Uhr weiter. Wählen Sie im Menü des Symbols „Beenden“, um es zu stoppen. Gilt ab dem nächsten Start von Strata.",
|
||||
"gh20": "App-Port",
|
||||
"gh21": "Der Port, den das Fenster und die lokale Adresse von Strata verwenden. Leer lassen, damit Strata ihn wählt (47831, wenn frei). Gilt ab dem nächsten Start von Strata.",
|
||||
"gh22": "Port (leer = automatisch)",
|
||||
"gh23": "Aktuell wird Port {0} verwendet.",
|
||||
"gh24": "Strata konnte seinen Port nicht verwenden",
|
||||
"gh25": "Port {0} wird von einem anderen Programm verwendet, daher wurde Strata auf Port {1} gestartet. Unter Port {0} gespeicherte Chats und Einstellungen werden nicht angezeigt, bis Strata ihn wieder verwenden kann. Schließen Sie das andere Programm und starten Sie Strata neu.",
|
||||
"gh26": "Wählen Sie einen Port zwischen 1024 und 65535 oder lassen Sie das Feld leer.",
|
||||
"gh27": "Macht alle Texte, Schaltflächen und Menüs in Strata größer oder kleiner.",
|
||||
"gh28": "Energie- und Temperaturwerte",
|
||||
"gh29": "pro kWh, in {0}",
|
||||
"gh30": "pro kWh",
|
||||
"gh31": "Währung ändern",
|
||||
"gh32": "Automatisch ({0})",
|
||||
"gh33": "Währung",
|
||||
"gh34": "weniger als {0}"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"gh01": "Energy since start",
|
||||
"gh02": "Cost since start",
|
||||
"gh03": "GPU fan",
|
||||
"gh04": "GPU clock",
|
||||
"gh05": "CPU temperature",
|
||||
"gh06": "SSD temperature",
|
||||
"gh07": "Electricity price",
|
||||
"gh08": "Price of one kilowatt-hour. The Monitor then shows what your graphics card has cost since Strata started. Leave it empty to hide the cost.",
|
||||
"gh09": "Price per kWh",
|
||||
"gh10": "Sound when work finishes",
|
||||
"gh11": "Plays a short sound when an answer or task that took a while is finished, or when Strata is in the background.",
|
||||
"gh12": "Play sound",
|
||||
"gh13": "Notify me when Strata needs an answer",
|
||||
"gh14": "In Manual mode, shows a desktop notification when the assistant waits for your approval and Strata is in the background.",
|
||||
"gh15": "Strata needs your answer",
|
||||
"gh16": "The assistant wants to use {0}. Open Strata to approve or deny.",
|
||||
"gh17": "a tool",
|
||||
"gh18": "Minimise to tray",
|
||||
"gh19": "Closing the window keeps Strata running in the tray, next to the clock. Choose Quit in the tray menu to stop it. Applies the next time Strata starts.",
|
||||
"gh20": "App port",
|
||||
"gh21": "The port Strata’s window and local address use. Leave it empty to let Strata choose (47831 when it is free). Applies the next time Strata starts.",
|
||||
"gh22": "Port (empty = automatic)",
|
||||
"gh23": "Using port {0} now.",
|
||||
"gh24": "Strata could not use its port",
|
||||
"gh25": "Port {0} is used by another program, so Strata started on port {1}. Chats and settings saved on port {0} are not shown until Strata can use it again. Close the other program and restart Strata.",
|
||||
"gh26": "Choose a port between 1024 and 65535, or leave it empty.",
|
||||
"gh27": "Makes all text, buttons and menus in Strata larger or smaller.",
|
||||
"gh28": "Energy and temperature readings",
|
||||
"gh29": "per kWh, in {0}",
|
||||
"gh30": "per kWh",
|
||||
"gh31": "Change currency",
|
||||
"gh32": "Automatic ({0})",
|
||||
"gh33": "Currency",
|
||||
"gh34": "less than {0}"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"gh01": "Energía desde el inicio",
|
||||
"gh02": "Coste desde el inicio",
|
||||
"gh03": "Ventilador de la GPU",
|
||||
"gh04": "Reloj de la GPU",
|
||||
"gh05": "Temperatura de la CPU",
|
||||
"gh06": "Temperatura del SSD",
|
||||
"gh07": "Precio de la electricidad",
|
||||
"gh08": "Precio de un kilovatio hora. El Monitor mostrará entonces lo que ha costado tu tarjeta gráfica desde que se inició Strata. Déjalo vacío para ocultar el coste.",
|
||||
"gh09": "Precio por kWh",
|
||||
"gh10": "Sonido al terminar el trabajo",
|
||||
"gh11": "Reproduce un sonido breve cuando termina una respuesta o tarea que tardó un rato, o cuando Strata está en segundo plano.",
|
||||
"gh12": "Reproducir sonido",
|
||||
"gh13": "Avisarme cuando Strata necesite una respuesta",
|
||||
"gh14": "En el modo manual, muestra una notificación de escritorio cuando el asistente espera tu aprobación y Strata está en segundo plano.",
|
||||
"gh15": "Strata necesita tu respuesta",
|
||||
"gh16": "El asistente quiere usar {0}. Abre Strata para aprobar o denegar.",
|
||||
"gh17": "una herramienta",
|
||||
"gh18": "Minimizar a la bandeja",
|
||||
"gh19": "Al cerrar la ventana, Strata sigue funcionando en la bandeja, junto al reloj. Elige Salir en el menú de la bandeja para detenerlo. Se aplica la próxima vez que se inicie Strata.",
|
||||
"gh20": "Puerto de la aplicación",
|
||||
"gh21": "El puerto que usan la ventana y la dirección local de Strata. Déjalo vacío para que Strata elija (47831 si está libre). Se aplica la próxima vez que se inicie Strata.",
|
||||
"gh22": "Puerto (vacío = automático)",
|
||||
"gh23": "Ahora se usa el puerto {0}.",
|
||||
"gh24": "Strata no pudo usar su puerto",
|
||||
"gh25": "El puerto {0} lo usa otro programa, así que Strata se inició en el puerto {1}. Los chats y ajustes guardados en el puerto {0} no se muestran hasta que Strata pueda volver a usarlo. Cierra el otro programa y reinicia Strata.",
|
||||
"gh26": "Elige un puerto entre 1024 y 65535, o déjalo vacío.",
|
||||
"gh27": "Hace más grandes o más pequeños todos los textos, botones y menús de Strata.",
|
||||
"gh28": "Lecturas de energía y temperatura",
|
||||
"gh29": "por kWh, en {0}",
|
||||
"gh30": "por kWh",
|
||||
"gh31": "Cambiar moneda",
|
||||
"gh32": "Automática ({0})",
|
||||
"gh33": "Moneda",
|
||||
"gh34": "menos de {0}"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"gh01": "Énergie depuis le démarrage",
|
||||
"gh02": "Coût depuis le démarrage",
|
||||
"gh03": "Ventilateur du GPU",
|
||||
"gh04": "Fréquence du GPU",
|
||||
"gh05": "Température du CPU",
|
||||
"gh06": "Température du SSD",
|
||||
"gh07": "Prix de l’électricité",
|
||||
"gh08": "Prix d’un kilowattheure. Le Moniteur affiche alors ce que votre carte graphique a coûté depuis le démarrage de Strata. Laissez vide pour masquer le coût.",
|
||||
"gh09": "Prix par kWh",
|
||||
"gh10": "Son à la fin du travail",
|
||||
"gh11": "Joue un court son quand une réponse ou une tâche qui a pris du temps est terminée, ou quand Strata est en arrière-plan.",
|
||||
"gh12": "Jouer le son",
|
||||
"gh13": "Me prévenir quand Strata a besoin d’une réponse",
|
||||
"gh14": "En mode manuel, affiche une notification sur le bureau quand l’assistant attend votre accord et que Strata est en arrière-plan.",
|
||||
"gh15": "Strata a besoin de votre réponse",
|
||||
"gh16": "L’assistant veut utiliser {0}. Ouvrez Strata pour accepter ou refuser.",
|
||||
"gh17": "un outil",
|
||||
"gh18": "Réduire dans la zone de notification",
|
||||
"gh19": "Fermer la fenêtre garde Strata actif dans la zone de notification, près de l’horloge. Choisissez Quitter dans le menu de l’icône pour l’arrêter. S’applique au prochain démarrage de Strata.",
|
||||
"gh20": "Port de l’application",
|
||||
"gh21": "Le port que la fenêtre et l’adresse locale de Strata utilisent. Laissez vide pour que Strata choisisse (47831 s’il est libre). S’applique au prochain démarrage de Strata.",
|
||||
"gh22": "Port (vide = automatique)",
|
||||
"gh23": "Port {0} utilisé en ce moment.",
|
||||
"gh24": "Strata n’a pas pu utiliser son port",
|
||||
"gh25": "Le port {0} est utilisé par un autre programme, donc Strata a démarré sur le port {1}. Les conversations et réglages enregistrés sur le port {0} ne s’affichent pas tant que Strata ne peut pas le réutiliser. Fermez l’autre programme et relancez Strata.",
|
||||
"gh26": "Choisissez un port entre 1024 et 65535, ou laissez vide.",
|
||||
"gh27": "Agrandit ou réduit tous les textes, boutons et menus de Strata.",
|
||||
"gh28": "Relevés d’énergie et de température",
|
||||
"gh29": "par kWh, en {0}",
|
||||
"gh30": "par kWh",
|
||||
"gh31": "Changer de devise",
|
||||
"gh32": "Automatique ({0})",
|
||||
"gh33": "Devise",
|
||||
"gh34": "moins de {0}"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"gh01": "起動後の消費電力量",
|
||||
"gh02": "起動後の電気代",
|
||||
"gh03": "GPU ファン",
|
||||
"gh04": "GPU クロック",
|
||||
"gh05": "CPU 温度",
|
||||
"gh06": "SSD 温度",
|
||||
"gh07": "電気料金",
|
||||
"gh08": "1キロワット時あたりの料金です。モニターに、Strata の起動後にグラフィックスカードが使った電気代が表示されます。空欄のままなら電気代は表示されません。",
|
||||
"gh09": "1 kWh あたりの料金",
|
||||
"gh10": "作業完了時の音",
|
||||
"gh11": "回答や時間のかかった作業が終わったとき、または Strata がバックグラウンドにあるときに、短い音を鳴らします。",
|
||||
"gh12": "音を鳴らす",
|
||||
"gh13": "Strata の返答が必要なときに通知する",
|
||||
"gh14": "手動モードで、アシスタントが承認を待っていて Strata がバックグラウンドにあるときに、デスクトップ通知を表示します。",
|
||||
"gh15": "Strata があなたの返答を待っています",
|
||||
"gh16": "アシスタントが {0} を使おうとしています。Strata を開いて承認または拒否してください。",
|
||||
"gh17": "ツール",
|
||||
"gh18": "トレイに最小化",
|
||||
"gh19": "ウィンドウを閉じても、Strata は時計の横のトレイで動き続けます。止めるには、トレイのメニューで「終了」を選びます。次に Strata を起動したときから有効になります。",
|
||||
"gh20": "アプリのポート",
|
||||
"gh21": "Strata のウィンドウとローカルアドレスが使うポートです。空欄なら Strata が選びます(空いていれば 47831)。次に Strata を起動したときから有効になります。",
|
||||
"gh22": "ポート(空欄 = 自動)",
|
||||
"gh23": "現在のポートは {0} です。",
|
||||
"gh24": "Strata は指定のポートを使えませんでした",
|
||||
"gh25": "ポート {0} は別のプログラムが使っているため、Strata はポート {1} で起動しました。ポート {0} に保存されたチャットと設定は、Strata が再びそのポートを使えるようになるまで表示されません。他のプログラムを閉じて Strata を再起動してください。",
|
||||
"gh26": "1024 から 65535 の間のポートを選ぶか、空欄にしてください。",
|
||||
"gh27": "Strata のすべての文字、ボタン、メニューを大きくしたり小さくしたりします。",
|
||||
"gh28": "消費電力量と温度の値",
|
||||
"gh29": "1 kWh あたり、通貨は {0}",
|
||||
"gh30": "1 kWh あたり",
|
||||
"gh31": "通貨を変更",
|
||||
"gh32": "自動({0})",
|
||||
"gh33": "通貨",
|
||||
"gh34": "{0} 未満"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"gh01": "시작 이후 에너지",
|
||||
"gh02": "시작 이후 비용",
|
||||
"gh03": "GPU 팬",
|
||||
"gh04": "GPU 클럭",
|
||||
"gh05": "CPU 온도",
|
||||
"gh06": "SSD 온도",
|
||||
"gh07": "전기 요금",
|
||||
"gh08": "1킬로와트시의 요금입니다. 모니터에 Strata를 시작한 뒤 그래픽 카드에 든 전기 요금이 표시됩니다. 비워 두면 비용을 표시하지 않습니다.",
|
||||
"gh09": "kWh당 요금",
|
||||
"gh10": "작업이 끝났을 때 소리",
|
||||
"gh11": "답변이나 오래 걸린 작업이 끝났을 때, 또는 Strata가 백그라운드에 있을 때 짧은 소리를 재생합니다.",
|
||||
"gh12": "소리 재생",
|
||||
"gh13": "Strata가 답을 필요로 할 때 알림",
|
||||
"gh14": "수동 모드에서 어시스턴트가 승인을 기다리고 Strata가 백그라운드에 있을 때 데스크톱 알림을 표시합니다.",
|
||||
"gh15": "Strata가 답을 기다리고 있습니다",
|
||||
"gh16": "어시스턴트가 {0}을(를) 사용하려고 합니다. Strata를 열어 승인하거나 거부하세요.",
|
||||
"gh17": "도구",
|
||||
"gh18": "트레이로 최소화",
|
||||
"gh19": "창을 닫아도 Strata는 시계 옆 트레이에서 계속 실행됩니다. 끝내려면 트레이 메뉴에서 '종료'를 선택하세요. 다음에 Strata를 시작할 때부터 적용됩니다.",
|
||||
"gh20": "앱 포트",
|
||||
"gh21": "Strata의 창과 로컬 주소가 사용하는 포트입니다. 비워 두면 Strata가 고릅니다(비어 있으면 47831). 다음에 Strata를 시작할 때부터 적용됩니다.",
|
||||
"gh22": "포트(비움 = 자동)",
|
||||
"gh23": "지금은 포트 {0}을(를) 사용 중입니다.",
|
||||
"gh24": "Strata가 지정한 포트를 사용할 수 없었습니다",
|
||||
"gh25": "포트 {0}을(를) 다른 프로그램이 사용 중이어서 Strata는 포트 {1}에서 시작했습니다. 포트 {0}에 저장된 채팅과 설정은 Strata가 다시 그 포트를 쓸 수 있을 때까지 표시되지 않습니다. 다른 프로그램을 닫고 Strata를 다시 시작하세요.",
|
||||
"gh26": "1024에서 65535 사이의 포트를 고르거나 비워 두세요.",
|
||||
"gh27": "Strata의 모든 글자, 버튼, 메뉴를 더 크게 또는 더 작게 만듭니다.",
|
||||
"gh28": "에너지 및 온도 측정값",
|
||||
"gh29": "kWh당, 통화 {0}",
|
||||
"gh30": "kWh당",
|
||||
"gh31": "통화 변경",
|
||||
"gh32": "자동({0})",
|
||||
"gh33": "통화",
|
||||
"gh34": "{0} 미만"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"gh01": "Energia desde o início",
|
||||
"gh02": "Custo desde o início",
|
||||
"gh03": "Ventoinha da GPU",
|
||||
"gh04": "Clock da GPU",
|
||||
"gh05": "Temperatura da CPU",
|
||||
"gh06": "Temperatura do SSD",
|
||||
"gh07": "Preço da eletricidade",
|
||||
"gh08": "Preço de um quilowatt-hora. O Monitor passa a mostrar quanto a sua placa de vídeo custou desde que o Strata iniciou. Deixe vazio para ocultar o custo.",
|
||||
"gh09": "Preço por kWh",
|
||||
"gh10": "Som ao terminar o trabalho",
|
||||
"gh11": "Toca um som curto quando uma resposta ou tarefa que demorou termina, ou quando o Strata está em segundo plano.",
|
||||
"gh12": "Tocar som",
|
||||
"gh13": "Avisar quando o Strata precisar de uma resposta",
|
||||
"gh14": "No modo manual, mostra uma notificação na área de trabalho quando o assistente espera sua aprovação e o Strata está em segundo plano.",
|
||||
"gh15": "O Strata precisa da sua resposta",
|
||||
"gh16": "O assistente quer usar {0}. Abra o Strata para aprovar ou negar.",
|
||||
"gh17": "uma ferramenta",
|
||||
"gh18": "Minimizar para a bandeja",
|
||||
"gh19": "Ao fechar a janela, o Strata continua em execução na bandeja, ao lado do relógio. Escolha Sair no menu da bandeja para encerrá-lo. Vale a partir da próxima vez que o Strata iniciar.",
|
||||
"gh20": "Porta do aplicativo",
|
||||
"gh21": "A porta usada pela janela e pelo endereço local do Strata. Deixe vazio para o Strata escolher (47831 quando estiver livre). Vale a partir da próxima vez que o Strata iniciar.",
|
||||
"gh22": "Porta (vazio = automático)",
|
||||
"gh23": "Usando a porta {0} agora.",
|
||||
"gh24": "O Strata não conseguiu usar a sua porta",
|
||||
"gh25": "A porta {0} está em uso por outro programa, então o Strata iniciou na porta {1}. Os chats e as configurações salvos na porta {0} não aparecem até o Strata poder usá-la de novo. Feche o outro programa e reinicie o Strata.",
|
||||
"gh26": "Escolha uma porta entre 1024 e 65535, ou deixe vazio.",
|
||||
"gh27": "Deixa todos os textos, botões e menus do Strata maiores ou menores.",
|
||||
"gh28": "Leituras de energia e temperatura",
|
||||
"gh29": "por kWh, em {0}",
|
||||
"gh30": "por kWh",
|
||||
"gh31": "Alterar moeda",
|
||||
"gh32": "Automática ({0})",
|
||||
"gh33": "Moeda",
|
||||
"gh34": "menos de {0}"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"gh01": "Энергия с запуска",
|
||||
"gh02": "Стоимость с запуска",
|
||||
"gh03": "Вентилятор GPU",
|
||||
"gh04": "Частота GPU",
|
||||
"gh05": "Температура CPU",
|
||||
"gh06": "Температура SSD",
|
||||
"gh07": "Цена электричества",
|
||||
"gh08": "Цена одного киловатт-часа. Тогда монитор покажет, во сколько видеокарта обошлась с запуска Strata. Оставьте поле пустым, чтобы скрыть стоимость.",
|
||||
"gh09": "Цена за кВт·ч",
|
||||
"gh10": "Звук по окончании работы",
|
||||
"gh11": "Проигрывает короткий звук, когда закончился долгий ответ или задача, а также когда Strata работает в фоне.",
|
||||
"gh12": "Проиграть звук",
|
||||
"gh13": "Уведомлять, когда Strata нужен ответ",
|
||||
"gh14": "В ручном режиме показывает уведомление на рабочем столе, когда помощник ждёт вашего подтверждения, а Strata работает в фоне.",
|
||||
"gh15": "Strata нужен ваш ответ",
|
||||
"gh16": "Помощник хочет использовать {0}. Откройте Strata, чтобы разрешить или отклонить.",
|
||||
"gh17": "инструмент",
|
||||
"gh18": "Сворачивать в трей",
|
||||
"gh19": "При закрытии окна Strata продолжает работать в трее рядом с часами. Чтобы остановить её, выберите «Выход» в меню значка. Действует со следующего запуска Strata.",
|
||||
"gh20": "Порт приложения",
|
||||
"gh21": "Порт, который используют окно и локальный адрес Strata. Оставьте поле пустым, чтобы Strata выбрала сама (47831, если свободен). Действует со следующего запуска Strata.",
|
||||
"gh22": "Порт (пусто = автоматически)",
|
||||
"gh23": "Сейчас используется порт {0}.",
|
||||
"gh24": "Strata не смогла использовать свой порт",
|
||||
"gh25": "Порт {0} занят другой программой, поэтому Strata запустилась на порту {1}. Чаты и настройки, сохранённые на порту {0}, не показываются, пока Strata не сможет снова его использовать. Закройте другую программу и перезапустите Strata.",
|
||||
"gh26": "Выберите порт от 1024 до 65535 или оставьте поле пустым.",
|
||||
"gh27": "Увеличивает или уменьшает весь текст, кнопки и меню в Strata.",
|
||||
"gh28": "Показания энергии и температуры",
|
||||
"gh29": "за кВт·ч, в валюте {0}",
|
||||
"gh30": "за кВт·ч",
|
||||
"gh31": "Сменить валюту",
|
||||
"gh32": "Автоматически ({0})",
|
||||
"gh33": "Валюта",
|
||||
"gh34": "меньше {0}"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"gh01": "启动以来的能耗",
|
||||
"gh02": "启动以来的费用",
|
||||
"gh03": "GPU 风扇",
|
||||
"gh04": "GPU 频率",
|
||||
"gh05": "CPU 温度",
|
||||
"gh06": "SSD 温度",
|
||||
"gh07": "电价",
|
||||
"gh08": "每千瓦时的价格。监控页会据此显示 Strata 启动以来显卡用电的费用。留空则不显示费用。",
|
||||
"gh09": "每千瓦时价格",
|
||||
"gh10": "工作完成时的提示音",
|
||||
"gh11": "回答或耗时较长的任务完成时,或 Strata 在后台时,播放一段简短的提示音。",
|
||||
"gh12": "播放提示音",
|
||||
"gh13": "Strata 需要您答复时通知我",
|
||||
"gh14": "在手动模式下,当助手等待您批准而 Strata 在后台时,显示桌面通知。",
|
||||
"gh15": "Strata 需要您的答复",
|
||||
"gh16": "助手想使用 {0}。打开 Strata 以批准或拒绝。",
|
||||
"gh17": "一个工具",
|
||||
"gh18": "最小化到托盘",
|
||||
"gh19": "关闭窗口后,Strata 会继续在托盘(时钟旁边)运行。要停止它,请在托盘菜单中选择“退出”。下次启动 Strata 时生效。",
|
||||
"gh20": "应用端口",
|
||||
"gh21": "Strata 窗口和本地地址使用的端口。留空则由 Strata 自行选择(空闲时为 47831)。下次启动 Strata 时生效。",
|
||||
"gh22": "端口(留空 = 自动)",
|
||||
"gh23": "当前使用端口 {0}。",
|
||||
"gh24": "Strata 无法使用它的端口",
|
||||
"gh25": "端口 {0} 已被其他程序占用,因此 Strata 改用端口 {1} 启动。在端口 {0} 上保存的聊天和设置在 Strata 能再次使用该端口之前不会显示。请关闭其他程序并重新启动 Strata。",
|
||||
"gh26": "请选择 1024 到 65535 之间的端口,或留空。",
|
||||
"gh27": "让 Strata 中的所有文字、按钮和菜单变大或变小。",
|
||||
"gh28": "能耗和温度读数",
|
||||
"gh29": "每千瓦时,币种 {0}",
|
||||
"gh30": "每千瓦时",
|
||||
"gh31": "更改货币",
|
||||
"gh32": "自动({0})",
|
||||
"gh33": "货币",
|
||||
"gh34": "不足 {0}"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"tl01": "Utilisez un nom de serveur simple de 32 caractères maximum : lettres, chiffres, tirets ou tirets bas.",
|
||||
"tl02": "Ces serveurs d’outils ont été laissés de côté car leurs noms ne sont pas autorisés : {0}. Un nom peut contenir jusqu’à 32 lettres, chiffres, tirets ou tirets bas.",
|
||||
"tl03": "Ces serveurs d’outils ont été laissés de côté car ils n’ont ni programme ni adresse web : {0}.",
|
||||
"tl04": "Ces serveurs d’outils ont été laissés de côté car un autre a presque le même nom : {0}."
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"tune01": "Optimiser ce modèle",
|
||||
"tune02": "Recherche des réglages les plus rapides pour votre PC…",
|
||||
"tune03": "environ {0} min restantes",
|
||||
"tune04": "Cela prend environ 20 minutes.",
|
||||
"tune05": "Arrêté. Vos réglages n’ont pas été modifiés.",
|
||||
"tune06": "L’optimisation n’est pas terminée. Vos réglages n’ont pas été modifiés.",
|
||||
"tune07": "Toujours aussi rapide qu’à l’enregistrement.",
|
||||
"tune08": "C’est plus lent qu’avant. Vous pouvez optimiser à nouveau.",
|
||||
"tune09": "Impossible de savoir pour l’instant. Votre optimisation a été conservée.",
|
||||
"tune10": "Réponses {0} % plus rapides, enregistré sous « {1} »",
|
||||
"tune11": "Vos réglages actuels sont déjà les plus rapides que nous ayons trouvés.",
|
||||
"tune12": "Rien n’a été modifié.",
|
||||
"tune13": "Enregistré sous « {0} », il ne sera donc pas réoptimisé à la réouverture de l’application.",
|
||||
"tune14": "Vitesse : {0} → {1} tok/s",
|
||||
"tune15": "Les réponses sont identiques à la configuration standard.",
|
||||
"tune16": "Les réponses peuvent différer légèrement de la configuration standard.",
|
||||
"tune17": "Les réponses ne sont pas reproductibles d’une exécution à l’autre.",
|
||||
"tune18": "Comprend un réglage de qualité qui modifie les chiffres.",
|
||||
"tune19": "Optimisation enregistrée utilisée : {0}",
|
||||
"tune20": "Réponses {0} % plus rapides",
|
||||
"tune21": "Ce PC a changé depuis l’optimisation, ces réglages ne sont donc pas utilisés tant que vous n’optimisez pas à nouveau.",
|
||||
"tune22": "Ce PC ou le moteur a changé depuis l’optimisation. Une vérification courte prend environ 2 minutes.",
|
||||
"tune23": "Pas encore d’optimisation enregistrée pour ces choix.",
|
||||
"tune24": "Strata essaie différents réglages sur ce PC et garde ceux qui accélèrent les réponses. Votre modèle est déchargé pendant ce temps, et l’ordinateur sera occupé.",
|
||||
"tune25": "Terminez d’abord l’opération en cours.",
|
||||
"tune26": "Durée à y consacrer",
|
||||
"tune27": "Rapide (environ 20 minutes)",
|
||||
"tune28": "Approfondie (jusqu’à 2 heures)",
|
||||
"tune29": "Ce que vous faites surtout",
|
||||
"tune30": "Programmation avec de longs prompts",
|
||||
"tune31": "Autoriser les réglages qui changent très légèrement les réponses (même qualité)",
|
||||
"tune32": "Optimisations enregistrées",
|
||||
"tune33": "Utilisée",
|
||||
"tune34": "Utiliser",
|
||||
"tune35": "Optimiser à nouveau",
|
||||
"tune36": "Lancer l’optimisation",
|
||||
"tune37": "Optimisation de votre modèle",
|
||||
"tune38": "Les réponses peuvent différer d’une exécution à l’autre sur ce PC même avec la configuration standard ; ce réglage n’y ajoute rien.",
|
||||
"tune39": "Utilisation de votre optimisation « {0} »",
|
||||
"tune40": "Désactiver",
|
||||
"tune41": "Activer",
|
||||
"tune42": "Votre optimisation est désactivée. Les réglages standard sont utilisés.",
|
||||
"tune43": "Votre PC était occupé, les mesures n’étaient donc pas fiables. Rien n’a été modifié. Réessayez quand il est au calme."
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import { composerCap, scrollBehavior } from '../lib/responsive';
|
||||
import { relTime, cx, duration, tokens, tps } from '../lib/format';
|
||||
import { createSubagentExample, pushToast, setPreviewTabClosed, branchFrom, continueLast, markRead, resolveTool, retryLast, selectSession, rewindTo, sendMessage, stopStreaming, useStore, } from '../lib/store';
|
||||
import { copyText } from '../lib/clipboard';
|
||||
import { KEYS_UP, PILL_DISTANCE, distanceFromBottom, followAfterScroll, wheelLeavesBottom } from '../lib/follow-scroll';
|
||||
import { getSlashCommands } from '../lib/commands';
|
||||
import { useLocation } from '../lib/router';
|
||||
import { composerSends } from '../lib/shortcut-bindings';
|
||||
@@ -212,6 +213,9 @@ export function ChatScreen() {
|
||||
}, [streaming]);
|
||||
/** Whether the reader is following the newest message rather than reading back. */
|
||||
const pinned = useRef(true);
|
||||
/** When the reader last used the wheel, a finger, a key or the scrollbar on the transcript. */
|
||||
const readerInput = useRef(0);
|
||||
const seenMessages = useRef(0);
|
||||
/**
|
||||
* Scrolls to the newest message and stays there.
|
||||
*
|
||||
@@ -266,8 +270,13 @@ export function ChatScreen() {
|
||||
const el = scroller.current;
|
||||
if (!el || (session?.showcase && !streaming))
|
||||
return;
|
||||
const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 120;
|
||||
if (nearBottom)
|
||||
// The reader's own scrolling decides (#4014): a wheel notch is about 100 px and used to land inside a 120 px band that pulled
|
||||
// the view back on every token. `pinned` is set by the scroller's wheel, touch, key and scroll handlers below.
|
||||
const count = session?.messages.length ?? 0;
|
||||
const sent = count > seenMessages.current && session?.messages.at(-1)?.role === 'user';
|
||||
seenMessages.current = count;
|
||||
// Sending a message always brings the reader to it, as it did before; only the answer's growth waits for the reader.
|
||||
if (pinned.current || sent)
|
||||
scrollToLatest();
|
||||
}, [session?.messages.length, session?.messages.at(-1)?.content, scrollToLatest]);
|
||||
// Scroll position is remembered per conversation, so returning to one puts you
|
||||
@@ -421,9 +430,17 @@ export function ChatScreen() {
|
||||
// the way back is worth offering.
|
||||
// The threshold has to tolerate the height estimate moving under
|
||||
// us, or the pill flickers as turns render during a scroll.
|
||||
const away = el.scrollHeight - el.scrollTop - el.clientHeight > 400;
|
||||
pinned.current = !away;
|
||||
setAwayFromBottom(away);
|
||||
const distance = distanceFromBottom(el);
|
||||
pinned.current = followAfterScroll(distance, pinned.current, Date.now() - readerInput.current);
|
||||
setAwayFromBottom(distance > PILL_DISTANCE);
|
||||
}} onWheel={(e) => {
|
||||
readerInput.current = Date.now();
|
||||
if (wheelLeavesBottom(e.deltaY) && e.currentTarget.scrollTop > 0)
|
||||
pinned.current = false;
|
||||
}} onTouchMove={() => { readerInput.current = Date.now(); }} onPointerDown={() => { readerInput.current = Date.now(); }} onKeyDown={(e) => {
|
||||
readerInput.current = Date.now();
|
||||
if (KEYS_UP.has(e.key) && e.target === e.currentTarget && e.currentTarget.scrollTop > 0)
|
||||
pinned.current = false;
|
||||
}}>
|
||||
|
||||
<div className="chat__inner">
|
||||
|
||||
Loaded 100 of 565 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user