Compare commits

...
450 Commits
Author SHA1 Message Date
Niko1221 4b9e05a2a8 Windows uninstaller: remove the app's own folders (control, host, node, harness) recursively; files the app wrote there while it ran (Python bytecode caches under control) are not in the installer's file list, so those folders and the program folder stayed behind after use (found on strix with a standard user after the process fix); test 2026-10-25 13:56:36 +01:00
Niko1221 af197f96db Windows uninstaller: find Strata's own running processes with the 64-bit PowerShell (the installer is 32-bit, whose Get-Process cannot read a 64-bit process's path) and with Get-Process first, WMI only as a second source: a standard user's WMI query was denied, so the host kept the folder open and it stayed behind (found on strix with a standard user) 2026-10-25 12:32:41 +01:00
Niko1221 53548c8af4 Merge main (41cf05a06: README model order, docs only) into f-026-rel 2026-10-25 11:37:25 +01:00
Niko1221 41cf05a06a README (all 7 languages): models in the order Qwen3.8-Flash-Next, Qwen3.6-35B-A3B, DeepSeek V4.1-Flash 2026-10-25 11:35:36 +01:00
Niko1221 4f0f933941 Merge main (ed4b4db40: README, docs and icons) into f-026-rel; AGENTS.md keeps main's text with Version 0.2.6, engine 0.1.51; the installer names in the READMEs and docs are the 0.2.6 ones 2026-10-25 11:29:12 +01:00
Niko1221 ed4b4db405 docs and packaging: the owner's 2026-10-23 guide set brought in for 0.2.5 (one installer per system, engine downloaded on first start, DeepSeek in the app and the Terminal with three sets and the Speed choice, install.sh form kept, versions 0.2.5 and 0.1.50, source setup labelled as the developer path) 2026-10-25 11:28:02 +01:00
Niko1221 af8c6f8ec7 README translations (zh-CN, ja, de, fr, es, pt-BR) rebuilt in the structure of the new README; the README links source setup to SOURCE_INSTALL.md and names the three 0.2.5 installers 2026-10-25 11:28:01 +01:00
Niko1221 e99c4d2d37 AGENTS.md and AI_SETUP.md: lead an agent to the installer, install.sh/install.ps1 and strata launch --plan then --yes --no-tui; source setup only as the fallback; provider order ISTA-DASLab, Unsloth, Gyro, others 2026-10-25 11:28:01 +01:00
Niko1221 b8b4ca0e32 docs: add the guide index, the source-install page, the AI reference and the media kit (the owner's pages, updated to 0.2.5) 2026-10-25 11:28:01 +01:00
Niko1221 562a0c2ed7 Version 0.2.6: the Qwen engine packages and Metal stay 0.1.50 (no C++ change since 0.2.5: only the DeepSeek engine and a generate.cpp comment changed); Control and the DeepSeek engine are 0.1.51 as the CMake project version, the shape of 0.2.4; build_hip_linux.sh keeps PKGVER 0.1.50 2026-10-25 11:21:36 +01:00
Niko1221 79ec8e2786 Host test: the source check for 'a version that is not downloaded starts with Read pictures on' accepts the braced else that the DeepSeek 8K context fix introduced (test only) 2026-10-25 11:16:25 +01:00
Niko1221 b5d41babfa README: the owner's layout (model dropdowns with Qwen and DeepSeek marks, provider and set tables, short install, hardware and tools tables), updated for 0.2.5: DeepSeek in the app and the Terminal with three sets and the free disk each needs, 0.2.5 speeds, one installer per system; model marks from LobeHub Icons (MIT) 2026-10-25 11:14:21 +01:00
Niko1221 27a8ed96b6 Version 0.2.6: Terminal and app 0.2.6, engine and Control 0.1.51 (every Qwen and DeepSeek package, Metal included) 2026-10-25 11:14:16 +01:00
Niko1221 8aee4538f2 generate.cpp: the --spec-split comment no longer calls its windows exact (they are not bit-exact against no-split: batch-shape divergence); comment only 2026-10-25 11:14:00 +01:00
Niko1221 e9e22db6a7 Setup: the DeepSeek engine step does not stop on 'can't reach GitHub' when the update feed is a local one (STRATA_UPDATE_FEED or --feed naming a folder, file or loopback address); a transient GitHub timeout no longer fails a verify run; tests 2026-10-25 11:13:51 +01:00
Niko1221 7025a99799 WSL on AMD: setup and launch say plainly that the AMD (HIP) engines do not run under WSL yet (no /dev/kfd, the GPU bridge never finishes a command) and name the Windows app or Terminal as the next step; a warning, nothing is refused; tests 2026-10-25 11:12:59 +01:00
Niko1221 36c6dd9ef5 Windows uninstaller: stop the app, host, node and engine that run from the install folder before files are removed, so the program folder no longer stays behind after use; only processes whose image path is inside the install folder; test with a real process 2026-10-25 11:11:37 +01:00
Niko1221 4a6a5fb699 Merge f-026-copyall (DeepSeek copy-all CPU-lane experts) into f-026-rel 2026-10-25 11:10:16 +01:00
Niko1221 d51d71e7c0 Merge f-025-fix (6 owner-QA fixes) into f-026-rel 2026-10-25 11:10:13 +01:00
Niko1221 fa663fabc5 Merge f-0243-rel (721c6f7ff) into f-025-readme 2026-10-25 10:41:20 +01:00
Niko1221 721c6f7ff5 Mac: the engine bundle inside a signed .app is not removed after the first start (deleting it broke the app's signature); outside an .app and on other systems the cleanup is unchanged; test 2026-10-25 10:37:21 +01:00
Niko1221 025cf7d954 DeepSeek engine: the copy-all fit check is one tested rule (copy_all_fits) and also needs the Windows commit room (RAM + page file beside its margin), not only the free RAM, so a 64 GB PC with a 12 GB card, or a big-RAM PC with a small page file, never copies the lane in by itself; the auto store says (verbose) when it holds less than the CPU lane 2026-10-25 10:18:53 +01:00
Niko1221 5d5c26d5bc Monitor/serve: a one-token reply has no decode span, so no tok/s is shown for it (a 1-token probe read 1,250 tok/s); test 2026-10-25 09:29:40 +01:00
Niko1221 8a3ca07c4e serve: a chat request with no message is a 400 (messages must be a non-empty list), as OpenAI answers it; test 2026-10-25 09:28:47 +01:00
Niko1221 dd9b9f746d About/Updates: never show the host's placeholder engine version (strata-engine 0.1.0) while the real one is still being read; a forced status read that meets one already running waits and reads again, so the version after an engine update is not the old one; test 2026-10-25 09:27:38 +01:00
Niko1221 6b5d593639 DeepSeek engine: --mmap-experts auto copies all the CPU lane's experts into RAM at load when they fit beside 8 GiB, with no demand store (all or none; --mmap-experts on/off, --ssd-cache N and DS4_STORE_GB stand as given); the auto store logs when it holds less than the lane. 1 x R9700 + 251 GB, 2.00 pack, warm: decode code 24.5 -> 27.6, agent 21.9 -> 27.1 tok/s, same ids as --ssd-cache 0 --mmap-experts off, load 34 -> 55 s. Rule ported from Project Maya v1.1.27 (MIT), mmap-experts auto copy-all 2026-10-25 09:27:16 +01:00
Niko1221 4d6721822a Storage and Manage models: a model whose files are all gone (old config, 0.0 GB, not ready) is no longer listed as a download 2026-10-25 09:26:19 +01:00
Niko1221 ad66a758a7 Models screen: DeepSeek is installed and shown at its one 8K context (as the guided setup), not the 32K default 2026-10-25 09:25:53 +01:00
Niko1221 59599f69f8 Monitor: All GPUs leaves out a small integrated Radeon/Intel chip beside a real card (totals and count); the hardware report gives each card a stable id since each make numbers from 0; tests with the RTX 5070 + Radeon iGPU pair 2026-10-25 09:25:17 +01:00
Niko1221 a04f5505aa Merge f-0243-rel (ad638b93e) into f-025-readme 2026-10-25 02:12:48 +02:00
Niko1221 36f580a5c3 UPDATES.md: the Mac dmg keeps its Metal engine; only the Windows and Linux installers are engine-less 2026-10-25 02:12:42 +02:00
Niko1221 ad638b93e5 Merge f-0243-dsimport (the app's model-folder preview recognises official DeepSeek sets) into f-0243-rel 2026-10-25 01:07:30 +02:00
Niko1221 32ad109816 Model preview: recognise official DeepSeek-V4.1-Flash IST-DAS sets (1.75/2.00/2.25 bpw, official and first-release names, with or without the Engram) as family deepseek41 2026-10-25 01:07:05 +02:00
Niko1221 6270e2d9d6 collect_libs: a library an earlier binary's pass already put in --out is not copied onto itself (SameFileError stopped the AMD packages' second pass) 2026-10-24 22:51:19 +02:00
Niko1221 ce27d13539 Merge f-0250-tui-uma (Faster is the default Speed on shared-RAM GPUs) into f-0243-rel 2026-10-24 22:31:11 +02:00
Niko1221 2dbdea2a77 Merge f-0243-strixspeed into f-0243-rel 2026-10-24 22:13:52 +02:00
Niko1221 da1eb6bc69 Merge f-0243-copyfix into f-0243-rel 2026-10-24 22:13:51 +02:00
Niko1221 2cea5afbc5 Merge f-0243-locales into f-0243-rel 2026-10-24 22:13:51 +02:00
Niko1221 0b9539877f README and docs for the single installer: three download buttons (Windows, Linux, Mac) with the 0.2.5 file names, one plain line instead of the per-GPU sentence (7 languages); docs/AMD_HIP.md and packaging/UPDATES.md name the single installers 2026-10-24 22:08:00 +02:00
Niko1221 9551b4168d Metal and SYCL builds: the Metal engine gets stubs for the 512-bit i-quant rows native_expert.cpp now references (x86-only file), and SYCL compiles iq_avx512.cpp with -mbmi2 and -mavx512vpopcntdq like the root CMakeLists.txt 2026-10-24 22:01:11 +02:00
Niko1221 1aff6d1411 Version 0.2.5: Terminal and app 0.2.5, engine and Control 0.1.50 (every Qwen and DeepSeek package, Metal included) 2026-10-24 21:52:49 +02:00
Niko1221 3f8f9aeaa6 Translations (de, zh, ja, es, ru, pt, ko) for the 26 new fix-catalog keys: first-run card, DeepSeek note, Speed labels 2026-10-24 21:47:01 +02:00
Niko1221 684881246d DeepSeek Speed label: the default is named by the engine's own rule, integrated / unified-memory GPU (Strix Halo) or cards totalling 16 GiB or less; '(default on this PC)' on the matching choice; test 2026-10-24 21:45:44 +02:00
Niko1221 d44144d7bb English catalog keys for the first-run card, the DeepSeek note and the Speed labels (the locale-gaps test failed on the merged branch); the other seven fix catalogs carry the English text until translated 2026-10-24 21:45:44 +02:00
Niko1221 df7d407742 Merge f-025-fast (Faster is the default on shared-RAM GPUs; ZC warning) into f-0243-rel 2026-10-24 21:40:42 +02:00
Niko1221 0597aaaeb7 launch: an integrated / unified-memory GPU (Strix Halo, gfx1151) is Faster by default, as the engine's --experts auto says: the 'default on this PC' label and the plan mirror 'integrated GPU, or cards totalling 16 GiB or less'; the engine 0.1.50 guard stays; tests 2026-10-24 21:40:29 +02:00
Niko1221 c4ce40bf5e DeepSeek engine: faster mode (5 experts a word) is the default on a GPU that shares the PC's RAM (Strix Halo: decode +11..13%, prompts +5..7% against 6); the 16 GiB rule is unchanged, --experts N wins; rule test rows 2026-10-24 21:40:06 +02:00
Niko1221 add4362139 Merge branch 'f-025-strix' into f-025-fast 2026-10-24 21:39:41 +02:00
Niko1221 ff91bc3729 DeepSeek engine: DS4_STORE_ZC=1 on a GPU that shares the PC's RAM, whose budget the weights already fill, prints one plain line that it will likely fail with a GPU fault and gains nothing (no cap) 2026-10-24 21:36:27 +02:00
Niko1221 67996022cd build_dsv41_windows.bat: configure every time so the build id is the current commit 2026-10-24 21:25:48 +02:00
Niko1221 7988837183 release build scripts: a failed step is a non-zero exit (build_hip_linux.sh), a stale binary or an old configure cannot pass, and strata-dsv41 must carry the release commit (BUILD_ID for a tarball, checked in the binary) 2026-10-24 21:24:53 +02:00
Niko1221 77b1a50541 launch: flow_for reads the settings only when the context has them (a context with only a computer, as test_mixed_vendors passes, raised AttributeError after the Speed step) 2026-10-24 21:18:56 +02:00
Niko1221 902239e7f7 ds4_gg_bench --k512: the two printf strings end in a newline escape (they held literal line breaks) 2026-10-24 20:57:49 +02:00
Niko1221 c1c3eab540 Merge f-026-5070 (in-place chunk 4096 on non-HIP cards under 16 GiB) into f-0243-rel 2026-10-24 20:41:36 +02:00
Niko1221 1fac68d6f2 DeepSeek engine: the experts-read-in-place default prompt chunk is 4096 on non-HIP cards under 16 GiB (was 8192): decode +2% agent / +0.4..9% code (5 pairs on RTX 5070 and RTX 3060, 120 more experts resident), an agent turn of 3K new tokens on 16K takes the same time, a cold 8K prompt 9% slower; DS4_FEEDLOG prints each feed's time; test rows for 12 GB, 15.9 / 16 / 24 GiB, 8 GB, unknown VRAM and HIP 2026-10-24 20:41:08 +02:00
Niko1221 aa4b225237 Merge f-025-strix (gfx11 WMMA expert kernels, iq test) into f-0243-rel 2026-10-24 20:31:03 +02:00
Niko1221 bd71ea81b9 Merge f-0243-pick (prompt-path rule, post-ready warm-up) into f-0243-rel 2026-10-24 20:25:56 +02:00
Niko1221 4d8ff9646f DeepSeek engine tests: the gfx11 / gfx12 matrix-core prompt tiles are forced on and off in ds4_iq_test (launch counted, both against the CPU reference and each other); umaprobe m1/ver probes; test knobs DS4_ZC_COARSE and DS4_STORE_CHUNK_SLOTS for the zero-copy registration 2026-10-24 20:11:27 +02:00
Niko1221 536460c3d8 Copy feedback: one helper (lib/clipboard.ts copyText with a toast); Markdown and Serving use it, lib/copy-text.ts is gone (the toast store is loaded only when a code block is copied) 2026-10-24 20:05:05 +02:00
Niko1221 061a285832 Merge f-0243-gui into f-0243-rel (Chat.tsx and DropZone.tsx: the guifix clipboard helpers kept at the conflicts, copyAndTell stays for Markdown and Serving) 2026-10-24 20:03:44 +02:00
Niko1221 e752c65455 ds4_gg_bench: --k512 calls the AVX-512 rows (iq512x_rows) directly against ggml-cpu's dot, bit for bit, and says whether the CPU and type allow them 2026-10-24 20:02:50 +02:00
Niko1221 dfd1b0f291 DeepSeek engine: the prompt path is chosen from the prompt's length alone (96 tokens and more through the chunked pass, windows of 16 below), one number for every box - the same prompt always takes the same path and gives the same bits (the old per-request timing prediction flipped the logits between runs and took 7 s on a 28-token prompt); the chunked pass is warmed up after the model is ready, on a thread, so the BLAS library's ~3.6 s start is not paid by the first prompt a person types; startup line; DS4_PICK=adaptive, DS4_PICK_MIN=N, DS4_WARM=0 as switches; rule test 2026-10-24 19:56:11 +02:00
Niko1221 8b8f8a10ee Merge f-026-5070 final chunk rule (7bf6e4d3b) into f-0243-rel 2026-10-24 19:15:07 +02:00
Niko1221 7bf6e4d3b7 DeepSeek engine: one default prompt chunk rule, prefill_chunk_default(): 1024 only for a single-card default run where the GPUs hold at least half of the routed experts (4 x R9700; the R9700 worker's gpu_expert_share() from 4cde62a35), 4096 when a GGUF pack's experts are copied into RAM (--mmap-experts off) on a non-HIP card with 10 GiB or more, 8192 for the experts read in place (5070, 3060, 1 x R9700: what a launch on one card already ran), 2048 for a pack that is not a GGUF; real launch logs on the RTX 5070 and the RTX 3060 show no --tp / --threads and 'prompts in chunks of 8192'; one unit test with the 5070, 3060, 5090-class, 1 x and 4 x R9700 cases 2026-10-24 19:14:17 +02:00
Niko1221 56ac21a217 Merge f-026-5070 (prefill chunk default rule) into f-0243-rel 2026-10-24 19:00:37 +02:00
Niko1221 829913ea59 DeepSeek engine: 4096 chunk rule applies to a GGUF pack only when it is copied into RAM (--mmap-experts off) and to an explicit --tp 1 single card; the experts-read-in-place default stays 8192; other packs keep 2048 2026-10-24 19:00:07 +02:00
Niko1221 dba7d0e0ab DeepSeek engine: the prompt chunk of the experts-copied-into-RAM case (2048) and of an explicit --tp 1 single card (1024) becomes 4096 on cards with 10 GiB or more (HIP builds and smaller cards keep theirs; the in-place 8192 is unchanged); startup line; rule test incl. 12/16/24/32 GB, short-VRAM, HIP and max-context cases 2026-10-24 18:49:20 +02:00
Niko1221 535cacb052 Tests never write the owner's real Strata profile: pytest gets scratch LOCALAPPDATA/XDG state and data folders; the host test runner and the native-control test files get a scratch settings file, components folder and app data 2026-10-24 18:43:52 +02:00
Niko1221 67a6ee76f4 Engine bundle cleanup: one function (cleanupAfterInstall) writes the removed and kept lines to the host output, with a test that both reach it 2026-10-24 18:36:50 +02:00
Niko1221 a264168390 One size figure: engine sizes are decimal MB, rounded half up, in the Terminal (strata update) and the app; both tested on the same numbers 2026-10-24 18:14:36 +02:00
Niko1221 e6c5997091 DeepSeek engine: gfx11 WMMA expert kernels for the prompt sets, on by default on gfx1151 (Strix Halo: prompts +22..29% at the same KL), opt-in on other gfx11 parts; STRATA_DS4_HIPK=0 turns them off 2026-10-24 18:13:43 +02:00
Niko1221 7063f3adca Sizes in decimal MB on the first-run card and the update check (one helper); the engine bundle of a per-GPU install is removed after an update only when the same engine is complete in the store (size and SHA-256), else kept with one log line 2026-10-24 18:10:05 +02:00
Niko1221 1c69e23658 Merge f-0243-guifix final head into f-0243-rel 2026-10-24 17:50:44 +02:00
Niko1221 f613637cbd Merge f-026-5070 (experts N|auto, IQ1_M row) into f-0243-rel 2026-10-24 17:50:01 +02:00
Niko1221 4f47009d08 Merge f-0243-fuse into f-0243-rel 2026-10-24 17:48:34 +02:00
Niko1221 94a4a34b05 Merge f-026-qwen into f-0243-rel 2026-10-24 17:46:13 +02:00
Niko1221 8993997e02 Merge f-025-strix into f-0243-rel 2026-10-24 17:46:13 +02:00
Niko1221 9741a61d7b Merge f-0250-tui into f-0243-rel 2026-10-24 17:46:13 +02:00
Niko1221 06f313b019 CPU lane IQ1_M AVX-512 rows: the 64-weight step's scale words by one vpermw and the delta signs by a masked subtract (bit-identical to ggml's dot: 0 of 2.7M rows differ at 1 and 2 tokens on Zen 4 and Sapphire Rapids); +9% on the rows, the 1-token policy is unchanged 2026-10-24 17:41:33 +02:00
Niko1221 236e13b4af Settings update check: a failed check of the app itself says so in one plain line, with the raw error behind Details (it used to show only Control's new-version text) 2026-10-24 17:25:53 +02:00
Niko1221 c893eeebf1 App update: an update job an older host left running is finished when the installer has written uninstall.exe or changed the program file after the job began (the installer keeps the build time as the modified time) 2026-10-24 17:11:55 +02:00
Niko1221 9b4092a94d launch: the Speed choice and --experts need a DeepSeek engine of 0.1.50 or newer (0.1.49.2 does not accept the flag): an older or unknown engine gets no flag, no Speed step and no plan line, and no error; the version comes from the package folder, else --version; tests 2026-10-24 16:47:17 +02:00
Niko1221 3283d2ae49 DeepSeek Speed: offered and passed only when the installed DeepSeek engine is 0.1.50 or newer (an older one ends the start with unknown option --experts) 2026-10-24 16:46:06 +02:00
Niko1221 b79d6930c1 test: the own-GGUF check loads its helper by path (another tests package can already be loaded in a full tools run) 2026-10-24 16:43:46 +02:00
Niko1221 883e21bf2e tests: the app-owned settings list includes the DeepSeek Speed choice 2026-10-24 16:43:31 +02:00
Niko1221 4e4921f63e DeepSeek settings: Speed (Faster / Full quality), default shown by the engine's own 16 GB rule, passed as --experts 5|6 only when chosen 2026-10-24 16:42:29 +02:00
Niko1221 bd7acc0e6d launch: the 16 GiB rule for the Speed label is compared in whole MiB; the catalog's vram_gb is already GiB (nvidia-smi MiB / 1024, AMD bytes / 2**30), so a 16 GB card (16376 MiB) counts as 16 GiB or less; tests with exact MiB values 2026-10-24 16:39:53 +02:00
Niko1221 c4413c0b54 launch: one rule for DeepSeek's speed. Nothing is passed to the engine unless the person chose (its --experts auto decides: 5 at 16 GiB of cards or less); the 'default on this PC' words use the same 16 GiB rule summed over the cards the launch uses, and the summary reads the engine's own 'experts per word: N' line back; tests follow 2026-10-24 16:36:49 +02:00
Niko1221 bc1a3ba2ed launch: DeepSeek Speed choice, Faster (--experts 5, the default on a PC with 16 GB of graphics memory or less) or Full quality (--experts 6): a Speed step in the guided screens, --speed and --full-quality, plain words in --plan and the summary, the quality note; tests, docs 2026-10-24 16:33:51 +02:00
Niko1221 3dd11c6dc1 DeepSeek engine: --experts N (the router's top N, weights renormalised); default 5 where the cards hold 16 GiB or less in all, the model's 6 elsewhere; startup line 'experts per word: 5 (faster mode; full quality: --experts 6)'; DS4_TOPK=N stays as a test override; rule test; DS4_PROF prints the CPU lane's stage split 2026-10-24 16:31:09 +02:00
Niko1221 eb4736c45e DeepSeek engine: on a Windows integrated GPU the experts leave room for the context's later GPU allocations
The Windows commit limit decides how many experts an integrated GPU may hold (place_experts). With AMD driver 26.10.1 the limit
went from 74 GB to 133 GB on a Strix Halo, the engine put 9506 experts (86 GB) on the GPU instead of 8105, every byte of the
GPU's address space was gone, and the 32K prompt's later allocations spilled into RAM: free RAM fell to 0 and the engine died
("rocBLAS: No devices found"). The experts now leave 4 GiB + 0.2 MB a token of context above 16384 (DS4_UMA_GPU_RESERVE_GB sets
it, tests); only an integrated GPU on Windows is affected.

Strix Halo (gfx1151, 2.00 pack, shipped profile), driver 26.10.1:
  32K prompt: killed (min free RAM 0) before, 113.2 tok/s with min free 13.6 GB now; 64K prompt (70000 context): 122.2 tok/s, min free 14.4 GB
  4K / 8K prompt: 62.8 / 83.9 tok/s (64.9 / 86.2 with the 9506 experts: -3%), min free 13.4-13.8 GB
  KL, code segment, 999 positions: 0.1020 (0.1028 before)
A smaller reserve (3 GiB + 0.1 MB a token) gained 0.0% on 4K / 8K / code decode and left 0.9 GB less free at 64K, so it was not taken.
2026-10-24 16:22:28 +02:00
Niko1221 66b43d4a7f Monitor: with All GPUs, a card that cannot report a value is left out and the card that does report is named (35 C . RTX 5070) instead of blanking the figure 2026-10-24 16:09:59 +02:00
Niko1221 91eb05a228 App update: a job an older host left running is finished when the app's program file is newer than the job 2026-10-24 16:07:45 +02:00
Niko1221 4228e6ee11 chat model picker: a downloaded model that is not loaded uses the same two-line row as the API models 2026-10-24 15:59:39 +02:00
Niko1221 9caf39495c Merge f-0243-guifix into f-0243-rel 2026-10-24 15:44:30 +02:00
Niko1221 0c53372a0e Merge f-0250-tui into f-0243-rel 2026-10-24 15:44:30 +02:00
Niko1221 a3e4cb64fd test: serving never reaches the DeepSeek engine check (only launch and setup import it), and 'serve --plan runs no program' means no installer or fetch: a PC's read-only hardware probes (nvidia-smi, Get-CimInstance) no longer fail it 2026-10-24 15:26:07 +02:00
Niko1221 dc2c4229e9 test: launch_engine_first gives the runner a Qwen catalog (a bare Mock answered 'DeepSeek' to the 0.2.4 multi-file question and skipped the engine step); the launch code was right 2026-10-24 15:25:07 +02:00
Niko1221 60d856a18e DeepSeek decode: fewer launches a layer - the int8 roundings of one GEMV call in one launch (identical inputs once) and hc_mix + hc_split in one launch (the token's last mix block runs the split); same arithmetic
R9700 1 card 2.00: 21.30 -> 21.82 tok/s (+2.4%), 4 cards 35.39 -> 37.00 (+4.5%), 3 pairs each, same generated ids. DS4_QUANT_SINGLE=1 / DS4_QUANT_NODEDUP=1 go back to one launch an item / no sharing.
2026-10-24 14:47:48 +02:00
Niko1221 abab458d74 Feed reads: one more try when the first times out or the connection fails (the first wait is 12 s, the second the full 30 s); about one github.com request in 25 hung for 30 s and the first-run card said offline 2026-10-24 14:44:12 +02:00
Niko1221 9cf91f3555 Load of DeepSeek: the start wait is at most the 600 s Control allows 2026-10-24 14:36:14 +02:00
Niko1221 f30d191d54 Load of DeepSeek: the app adds no Flash-Next options (request slots, RAM budget, output clamp, resource profile, memory retry) to the DeepSeek engine and waits as long as its config says 2026-10-24 14:34:47 +02:00
Niko1221 75ee28fd3d Host: the per-model settings routes accept a dotted id; a test walks a dotted model id through list, load and unload 2026-10-24 14:33:40 +02:00
Niko1221 fe2b9f4094 Host: run config file names with a dot (DeepSeek) pass the app's own checks, so the model can be loaded 2026-10-24 14:31:43 +02:00
Niko1221 54768d25b1 test: the own-GGUF check finds tests.terminal when tools/ is run on its own 2026-10-24 14:29:49 +02:00
Niko1221 4499c69681 Installed models: an id with a dot (deepseek-v4.1-flash-ist-das-2.00bpw) is listed; the install finished but the app never showed the model 2026-10-24 14:22:39 +02:00
Niko1221 beeea0abf3 test: the Mac build-tools test follows find_tool's version check 2026-10-24 14:19:31 +02:00
Niko1221 569f7c739d test: the --kv choices check reads the last line of the error (Python 3.12 and 3.13 print the list differently) 2026-10-24 14:18:47 +02:00
Niko1221 ff84ff8164 First-run card: one plain line when the drive for the engine is short on space 2026-10-24 14:14:32 +02:00
Niko1221 61a4b6bd02 First-run card: no dash in the engine size line 2026-10-24 14:14:10 +02:00
Niko1221 192ab03813 DeepSeek-V4.1-Flash in the app: the model picker lists the three official sizes (not the 508 GB multi-card set), an 8K context only, a plain size/speed/disk note that warns and never blocks, no calibration step; the install tool accepts the own-path sizes 2026-10-24 14:11:45 +02:00
Niko1221 16c4da9443 CPU expert rows: 512-bit grid lookups by gather where a gather is cheap; one IQ3 token on the exact 512-bit rows
The 512-bit i-quant rows (IQ2_XXS/XS/S, IQ3_XXS/S) assembled their grid words from scalar loads (~30 uops a 64-value chunk). Where cpu_gather_fast()
(Intel Sapphire Rapids and later) they now load them by one gather, 32-bit for IQ3 and 64-bit for IQ2 (STRATA_IQ_GATHER=1/0 force it; Zen 4 stays scalar:
its gathers are microcoded). The same words are loaded, so every output bit is unchanged. One token of an IQ3_XXS / IQ3_S expert takes the exact 512-bit rows (ggml-cpu's
single-token AVX2 dot bit for bit) with the gather, after a first-use bit check against this ggml; STRATA_NO_IQ512X1=1 turns it off.

Xeon 8468, native_gu_rows (gate+up of an expert), us: IQ3_XXS 1 token 295 -> 219, 2 tokens 462 -> 300, 4 tokens 558 -> 389; IQ3_S 415 -> 305, 603 -> 342, 562 -> 296;
IQ2_S 2 tokens 377 -> 235, 4 tokens 437 -> 327 (one IQ2_S token stays on ggml: 239 against 234). Output checksums identical with and without.
Test iq512_gather_parity (both lookup modes): exact rows == ggml vec_dot at 1-4 tokens, engine rows within float rounding.
2026-10-24 14:09:08 +02:00
Niko1221 9030da09f9 Merge f-026-5070 into f-0243-rel 2026-10-24 14:01:05 +02:00
Niko1221 980c95a6d7 Merge f-0243-store into f-0243-rel 2026-10-24 14:01:04 +02:00
Niko1221 86a751af27 Merge f-025-strix into f-0243-rel 2026-10-24 14:01:04 +02:00
Niko1221 84b712a3d8 Merge f-0243-sm89 into f-0243-rel 2026-10-24 14:01:04 +02:00
Niko1221 db42e59930 First-run card stays until Continue; a processor's own graphics is not counted as a card 2026-10-24 13:58:36 +02:00
Niko1221 c1e80f7952 Markdown: a single newline inside a paragraph is a soft break (a space); two spaces or a backslash at the end of a line keeps a line break (#3818) 2026-10-24 13:55:42 +02:00
Niko1221 ea349eb8b8 Language: 43 interface strings that had no key (/plan /research /new hints, tuning, goal, telemetry, task and picture texts, the new fix texts) now exist in all 8 languages; the four Open hints translate their label; a test fails when a tr() literal or hint has no key (#3479, #3825) 2026-10-24 13:51:18 +02:00
Niko1221 08a5e6b34a packaging smoke: the packaged Terminal carries the installer since 0.2.4, so 'strata install' without a keyboard stops at the first question instead of saying 'no installer'; the check accepts both 2026-10-24 13:48:09 +02:00
Niko1221 353c9f2556 chat: a PDF is added to the chat folder and named in the next message; the file dialog lists PDFs and text on Linux (#3838) 2026-10-24 13:48:06 +02:00
Niko1221 ec03ae0106 First-run engine card: GPU, engine size, one Download button, progress with resume, 'I already have the file' (offline), plain states; host routes /api/engine/first-run, /job and /from-file; install-engine-file job 2026-10-24 13:47:22 +02:00
Niko1221 780a69b41b Expert settings: decimals can be typed in sampling and number fields; Ctrl+V of a screenshot reads clipboard items (#3736, #3748) 2026-10-24 13:47:18 +02:00
Niko1221 c7899f9c89 chat: Copy response and Copy message answer Copied or say they could not (with an older-copy fallback); a typed message keeps its line breaks (#3731, #3720) 2026-10-24 13:45:58 +02:00
Niko1221 e61b7024cf Models page: a neutral placeholder until the engine's first answer, not the bundled Starter (#3740) 2026-10-24 13:45:12 +02:00
Niko1221 25abbdd90c Models page: the chosen conversation context and cache format count in the memory estimate (#3832) 2026-10-24 13:43:25 +02:00
Niko1221 f750086183 Models page: the chosen conversation context and cache format count in the memory estimate (#3832) 2026-10-24 13:43:02 +02:00
Niko1221 31adebc8b7 strata components install-file: install an engine package the person downloaded (signature, every file, fit, test start, switch; no network); the engine switch is shared with the download path 2026-10-24 13:42:41 +02:00
Niko1221 4ccfd7ca30 Windows AMD/Intel cards: real memory size from the DirectX adapter list, load and memory in use from the Windows GPU counters, virtual monitors no longer listed as cards (#3727, #3836) 2026-10-24 13:40:59 +02:00
Niko1221 b750d757f6 CUDA engine selector: sm_MN machine code also covers a card sm_MK of the same major version with K >= N (CUDA cubin compatibility), so an RTX 40 card (sm_89) takes an sm_86 package; tests for the Qwen and DeepSeek packages; DeepSeek card text says RTX 30, 40 and 50 2026-10-24 13:40:27 +02:00
Niko1221 d9d033be86 launch: at 80 columns the model list keeps its verdict column (fits, tight, too big and the recommendation) instead of cutting it off; test 2026-10-24 13:40:22 +02:00
Niko1221 9c375ccb7d launch: setup's DeepSeek engine upgrade lines (the failure one 'A newer DeepSeek engine is available but could not be installed') are shown on the setup path too; test 2026-10-24 13:38:49 +02:00
chicco83 90a7aeea51 Setup wizard: show the VRAM of AMD cards on Windows from the engine's list when the host probe reports 0 (#3727, PR #3798 by chicco83) 2026-10-24 13:38:38 +02:00
Niko1221 d6c1936021 setup: a source build on Windows needs Visual Studio 2022 (2019 cannot compile the C++20 sources) and the stop says so, with the ready-made engine as the way around (#3566); tests follow 2026-10-24 13:37:35 +02:00
Niko1221 fb3bae20c6 setup: the list of installed packages remembers which system wrote it, so a .venv shared by Windows and Linux installs again instead of saying 'already installed' (#3634); test 2026-10-24 13:36:55 +02:00
Niko1221 1d20d3e2b0 chat: reopening a chat restores its workspace and the turn runs in that folder (#3754) 2026-10-24 13:36:35 +02:00
Niko1221 13a4153be4 copy-text: load the toast store only when copying, so the Markdown renderer still renders without a browser 2026-10-24 13:35:56 +02:00
Niko1221 ea897551ee folder picker: a PowerShell that ends before its window opens (security software) falls back to typing the path (#3831) 2026-10-24 13:35:54 +02:00
Niko1221 ed0c8939aa setup: a download that gets no data for two minutes says so with the percentage and the one next step, instead of a line that never moves (#3567); test 2026-10-24 13:35:43 +02:00
Niko1221 47ccec7c6a Delete a model: files Strata downloaded and packs it made go with it even in another models folder; the person's own files stay (#3716) 2026-10-24 13:35:04 +02:00
Niko1221 aef053e353 launch: the Access screen's help names --show-key and --api-key, so a person can see or set a key of their own (#3713 #3951); test 2026-10-24 13:34:18 +02:00
Niko1221 c22e5e7633 missing files: name the engine program in words once, not as component:strata-engine (#3900) 2026-10-24 13:34:11 +02:00
Niko1221 27cbbfe6d6 launch: the start waits longer for a big model by itself (10 minutes, about 8 s more per GB above 40 GB, at most 30), the server's own engine wait follows, and the runtime accepts start waits up to 1800 s (#3949 #3459). Own fix instead of PR #3817: that one needs people to set STRATA_ENGINE_READY_S; tests 2026-10-24 13:34:04 +02:00
Niko1221 97c31168aa chat model picker lists downloaded optimized models that are not loaded yet (#3814) 2026-10-24 13:33:56 +02:00
Niko1221 5faf357b49 Chat: Copy response/message/code say Copied or Couldn't copy and fall back to the older copy route (#3731); a pasted screenshot is also read from clipboard items (#3748); the user bubble keeps its line breaks (#3720) 2026-10-24 13:33:17 +02:00
Niko1221 77284b5e33 update: the test start runs the new engine with a short context and no picture reader (#3842), and an update keeps the CUDA build that runs here (#3835); launch: the summary names the engine log (#3656) and a tuned model says how to measure again (#3874); tests 2026-10-24 13:32:44 +02:00
Niko1221 4b49729a1e monitor: read Windows disk counters from one long-lived PowerShell instead of a cold one every 3 s (#3939) 2026-10-24 13:32:09 +02:00
Niko1221 014fd6b5db App update: log lines for every update step, an alert when the app came back as the old version, and Update engine works before the native snapshot has connected 2026-10-24 13:30:13 +02:00
Niko1221 5bfce4bea0 setup: the build tools come from the virtualenv first and an older system cmake (below the 3.24 CMakeLists.txt needs) counts as missing, so pip installs the right one (#3822); tests 2026-10-24 13:28:02 +02:00
Niko1221 15fdb69563 In-app update: a finished app update is no longer shown as an interrupted operation; a test update feed used once is not remembered as the feed for later checks
After 'Update Strata' installed a new app version, the host that asked for it was ended by the installer, so the job stayed saved as running and the next start turned it into a red 'Interrupted operation' alert in Settings > Update and Diagnostics. The job now records the app version it started from, and a start under a different version marks it done ('Strata is updated to X').

strata update and the app's check reused the feed used last, including a loopback address or a folder from a test of the update feed; every later check on that PC then said 'No internet connection'. Only an https address is remembered now.
2026-10-24 13:27:28 +02:00
Niko1221 ad9bb57ddc own model files: a qwen4exp file set that is no tested build but has the layout of one is used as that model with a plain notice, and setup keeps a complete GGUF of another size or hash with a warning instead of stopping (#3651 #3777); tests 2026-10-24 13:27:08 +02:00
Niko1221 b18700c8f7 Adaptive tier: the engine chooses the asynchronous tier by measurement (arena, one CUDA GPU)
Without --adapt-async on the command line, the expert arena on one CUDA GPU (--serve, adaptive tier on, no --batch, no layer
split, no pipelined windows, no peer or remote tier) starts on the blocking tier, ignores 64 steady decode windows, measures 64,
and when the tier's wait is at least 5% of a window runs 64 windows on the asynchronous tier, going back (for the session) only if
that was more than 2% slower per token. One log line at each switch. --adapt-async 0|1 and STRATA_ADAPT_ASYNC=0 keep their meaning;
the identity gates run with --adapt-every 100000 and never reach it. Decision logic and its unit test are header-only
(adapt_async_default.hpp, adapt_async_default_test).
2026-10-24 13:27:00 +02:00
Niko1221 c02a0b8b0d UPDATE.bat / update.sh: an engine that stayed older than the minimum is said as 'the engine was NOT updated' with one next step, not '[ok] Strata is updated' (#3653); test 2026-10-24 13:08:46 +02:00
demetree 7a03fc9414 strata connect: scale the app configs' compaction point with the server's context 2026-10-24 13:06:11 +02:00
Niko1221 d924beffe5 setup: a Mac with an Intel (Rosetta) Python is still an Apple Silicon Mac; the chip name and hw.optional.arm64 decide, one plain line says the Python is an Intel build (#3894); test 2026-10-24 13:06:00 +02:00
Niko1221 bdaa93edf0 launch: a setup option error ('setup.py: error: argument --kv: invalid choice') is the cause shown, not only 'could not be installed'; setup accepts --kv fp16 as DOCKER.md says (#3785); tests 2026-10-24 13:05:30 +02:00
Niko1221 1736b558bd setup: a NVIDIA driver older than CUDA 13 needs (528 to 579) runs the CUDA 12 engine by itself and says so in one line instead of stopping and naming a flag strata launch does not have (#3843); tests 2026-10-24 13:04:44 +02:00
Niko1221 0cd44323e4 Windows: a card taken out of the PC no longer decides the engine; only the display adapters Windows reports as present count when two makes are in the registry (#3892); test 2026-10-24 13:03:56 +02:00
Niko1221 e3eeb93972 DeepSeek engine: the RAM store's background fill is opt-in (DS4_STORE_BG=1): ready 9 s sooner but the first prompt waits for the same SSD and decode measured 3.7% lower 2026-10-24 12:40:19 +02:00
Niko1221 58f2acc05b DeepSeek engine: the RAM store's fill beyond the first 48 GB is read in the background after the model is ready (hottest first, behind the model's own reads); DS4_STORE_BG=0 reads it all first 2026-10-24 12:33:10 +02:00
Niko1221 6fcde7e305 DeepSeek CPU lane: compute the store hits while the misses are read, each missed expert as its read lands (DS4_MISS_SPLIT=0 restores the wait-for-all order); ExpertStore::ensure split into begin / wait_slots 2026-10-24 12:29:56 +02:00
Niko1221 b3c86c783f DeepSeek engine: the automatic RAM store may take every routed expert, not 48 GB at most, where the free RAM holds them (1 R9700 + 251 GB, 2.00 set: 12.6 -> 21.4 tokens/s); free - 12 GB and the explicit sizes unchanged 2026-10-24 11:58:35 +02:00
Niko1221 f1e41cdee2 DeepSeek engine: the automatic RAM store is sized again after the weights are on the GPU, and keeps room for the context's buffers, so a unified-memory PC (Strix Halo) keeps 12 GB free; explicit sizes unchanged 2026-10-24 11:28:09 +02:00
Niko1221 7dd5cf18e6 release notes 0.2.4.2: the picture-file and already-have-files fixes (#3927, #3917, #3914); no environment variable in the conversations line 2026-10-24 10:52:25 +02:00
Niko1221 8aacdbc7a0 release notes 0.2.4.2: the list of new files 2026-10-24 10:51:27 +02:00
Niko1221 3eabee6049 test: the restyled READMEs state the app version through their download buttons (Strata-0.2.3-...), not the old '0.2.3 · ' header line 2026-10-24 10:51:13 +02:00
Niko1221 c896f73934 release notes 0.2.4.2: the DeepSeek engine packages are 0.1.49.2, the same number the binary prints and the Control has 2026-10-24 10:36:34 +02:00
Niko1221 93a91a2767 README translations (zh-CN, ja, de, fr, es, pt-BR): brought up to the restyled English README (download buttons per system, section menu, coffee badge, plain-word DeepSeek table, shorter Using it) 2026-10-24 10:36:09 +02:00
Niko1221 12939aa64d README: restyled to the owner's GitHub layout (download buttons per system, a section menu and a coffee badge at the top, plain-word DeepSeek table with the measured 0.2.4 speeds, shorter Using it, jargon cut); DEEPSEEK41.md: 4x R9700 speed updated to the 0.2.4 measurements 2026-10-24 10:29:36 +02:00
Niko1221 dfa3f4025c DeepSeek: pack_gguf takes an automatically found mmproj only when its projector is deepseek41v (a Qwen mmproj in the shared models folder is ignored, no error: #3927 #3917); files the person already has are recognised as DeepSeek by the pinned shard names in the guided screen too, not by the generic GGUF check that called the UD n-gram shard unreadable (#3914); tests 2026-10-24 10:27:57 +02:00
Niko1221 ac08a0820d release notes 0.2.4.2: conversations and a shared system prompt are kept on disk 2026-10-24 10:12:46 +02:00
Niko1221 05041e5db0 Merge branch 'f-024-on' into f-0242-rel 2026-10-24 10:12:41 +02:00
Niko1221 3157c38c68 DeepSeek engine: an installed package older than the signed feed's is updated by itself on launch and setup (fetched and checked, put next to the old one, started once, then switched to and the old one removed; one plain line and the old one keeps running on a failure; silent without a network; never for --dsv41-package or STRATA_DSV41_EXE); tests; release notes 2026-10-24 09:35:15 +02:00
Niko1221 d2d198a492 release notes 0.2.4.2: the DeepSeek engine 0.1.49.1 (faster on AMD Zen 1/2 CPUs, answers unchanged) and how to get it 2026-10-24 09:30:42 +02:00
Niko1221 8c9bd206ce Merge branch 'f-0243-iq1m' into f-0242-rel 2026-10-24 09:29:52 +02:00
Niko1221 46bddbbb74 dsv41: BMI2-off ggml-cpu applies only to the DeepSeek engine build (STRATA_BUILD_DSV41); ds4_gg_bench --dump FILE to compare two ggml builds bit for bit (IQ1_S/IQ1_M with and without BMI2: identical at 1,2,4,8,16 tokens) 2026-10-24 09:14:06 +02:00
Niko1221 6953d896d0 dsv41 CPU lane: IQ1_M uses Strata's exact rows from one token (ggml's IQ1_M dot runs at 1.8 GB/s on AMD Zen 2: _pdep_u64 is microcoded there); portable ggml-cpu build has BMI2 off (IQ1_S 2.3 -> 37 GB/s, IQ1_M 1.8 -> 32 GB/s on an EPYC 7452) 2026-10-24 09:09:23 +02:00
Niko1221 baa8f616de DeepSeek-V4.1: conversation slots and the shared-start checkpoint on by default (STRATA_DS4_SLOT_KEEP=0 / STRATA_DS4_AUTO_PIN=0 turn them off) 2026-10-24 08:25:17 +02:00
Niko1221 ac669f199d Terminal 0.2.4.2 / Control 0.1.49.2: version bump for the DeepSeek launch and install fixes (app stays 0.2.3, engine packages unchanged); release notes 2026-10-24 06:29:42 +02:00
Niko1221 0003604859 launch: the second sentence of 'The model could not be installed.' starts with a capital; test 2026-10-24 06:29:42 +02:00
Niko1221 e168b76b8c Merge branch 'f-0242' into f-0242-rel 2026-10-24 06:28:20 +02:00
Niko1221 f53664c159 launch: a setup message that ends with a full stop does not get a second one ('engine..'); test 2026-10-24 05:43:54 +02:00
Niko1221 44f8c334de model list: the DeepSeek disk check counts only what is still to be downloaded (files already in the models folder, also under the first release's names, are done), plus the room for the pack; tests 2026-10-24 05:41:50 +02:00
Niko1221 a22d557eee launch --plan: a DeepSeek plan no longer promises a Strata engine download; test 2026-10-24 05:39:30 +02:00
Niko1221 51d23fdd1e P2003 findings: DeepSeek setup says in one line when GitHub cannot be reached (a 10 s check before any download, not minutes of retries) and no longer fetches or test-starts the Strata engine, llama.cpp source or CUDA libraries it does not use; install.sh is one function called on the last line (a half-downloaded file runs nothing) and every documented one-liner is 'curl ... -o install.sh && sh install.sh' so a failed curl cannot look like success; tests 2026-10-24 05:37:41 +02:00
Niko1221 a62ca064a3 launch: the DeepSeek engine package that runs is the newest of the running Terminal's own install (STRATA_INSTALL_DIR, else its own folder) and the install the model's settings came from (the first remembered install, which can be an older Terminal folder or another checkout); the running Terminal's own on a tie; the choice is logged; versions compare as numbers; tests 2026-10-24 05:09:58 +02:00
Niko1221 8183c6b671 docs: RTX 3060 speed with the release defaults (8 to 10 tokens/s, 149 tokens/s at 8K); DSpark stays off on 12 GB cards and adds 2-13% when turned on; README x7, DEEPSEEK41.md, release notes 2026-10-24 04:59:46 +02:00
Niko1221 c33fa7ba6d Terminal 0.2.4.1 / Control 0.1.49.1: version bump for the DeepSeek-setup hotfix (app stays 0.2.3, engine packages unchanged); release notes 2026-10-24 04:36:57 +02:00
Niko1221 b7958c044f Packaged Terminal: tools/dsv41/pack_gguf.py, smoke.py, tune.py (and strata_inspect, strata_bench, nvfp4_local_pack) are in the bundle; a test reads every tools/*.py that setup.py, strata_cli and strata_control run by path and fails when one is missing from the bundle, and starts the DeepSeek scripts from the bundle alone 2026-10-24 04:36:57 +02:00
Niko1221 06e51dfaef test: the failed-update reason is capitalised where it joins the sentence (follows 00a2cfa07) 2026-10-24 04:13:07 +02:00
Niko1221 96ca4383c7 Merge branch 'f-dsv41-tune' into f-024-rel 2026-10-24 03:17:28 +02:00
Niko1221 2ecdc2e511 launch: an installed DeepSeek model whose config names a gone engine, with no package in this install, is not 'already installed': setup runs (feed download, or one plain line with the next step) instead of the start failing on a missing file; tests 2026-10-24 02:58:01 +02:00
Niko1221 5353097c90 DeepSeek quality numbers from the final-tip scores (H100, 4,995 positions): 2.00 KL 0.170 ppl +15.5% (code +8.6%, agent +5.3%), 2.25 KL 0.107 ppl +7.3% (code +3.2%, agent +2.2%); README x7, DEEPSEEK41.md, release notes, setup and fit text 2026-10-24 02:57:14 +02:00
Niko1221 9011bd107a launch: DeepSeek runs on this install's own engine package, not the exe path baked into an installed model's config (which can come from another Strata install on the computer or name a package folder an update replaced); tests 2026-10-24 02:56:16 +02:00
Niko1221 52b104b1ac IQ1_M quality from the H100 (KL 0.217, ppl 3.80, +21%; code +8.9%, agent +10.2%, weaker on plain prose) in setup, the fit text, README x7, DEEPSEEK41.md and the release notes 2026-10-24 02:37:08 +02:00
Niko1221 00a2cfa079 launch: a reason joined after 'The engine could not be installed.' starts with a capital; test 2026-10-24 02:36:17 +02:00
Niko1221 6eaf32075c Merge f-024-maya2 (6c6267f33) into f-024-rel: Windows batch submit, flow pump, Stop during a long prompt, lane-free, opt-in switches (all in src/dsv41)
Five conflicts, resolved keeping every f-024-rel behaviour (slots pin, long-prompt skip, HIPK, ZC fix) and maya2's:
- ds4_main.cpp, request struct: both fields kept (pin=N from f-024-slots and dspark_cum from maya2).
- ds4_main.cpp, request parser: both keys kept (pin, dspark_cum/spec_cum).
- ds4_main.cpp, prompt feed loop: slots' pin_at (the read stops at N for the shared-start checkpoint 3) and maya2's Stop handling (checkpoint 1 before the prompt, set_cancel, the stop check at the top of each feed) combined: the loop checks Stop first, then sizes the feed with the pin cut (n is no longer const).
- ds4_model.cu, forward chunk loop (two places): the skip's 'run.runs(k, il)' (an early chunk skipped past the yoco layer) kept, and maya2's halt_now() / halt_end(di, err) check added after it, so a skipped layer does no halt work.
win_submit (ds4_kernels.cu) is compiled to a no-op outside _WIN32 (#if defined(_WIN32) ... else (void) s), so it is already out of the Linux CUDA and Linux HIP builds.
2026-10-24 02:35:40 +02:00
Niko1221 698a87d999 Merge branch 'f-024-tui' into f-024-rel 2026-10-24 02:34:57 +02:00
Niko1221 6c6267f33c dsv41: Maya-port defaults from the owner-PC A/Bs (ON: win_submit, flow pump, Stop, lane-free; OFF with switch: timer, Engram readers, unbuffered Engram, cold prefetch, DSpark cum)
Owner PC (RTX 5070 12 GB, Ryzen 5 7600, official 2.00 pack, shipped profile-2.00.bin, 3 alternating pairs, medians, off = every switch off):
 4K prompt, off 92.64 tok/s: win_submit 99.12 (+7.0%, 3/3 pairs), flow pump 97.37 (+5.1%, 3/3), 64 Engram readers 96.64 (+4.3%, 3/3),
   unbuffered Engram 85.56 (-7.6%, 0/3), all on 86.84 (-6.3%).
 code decode, off 4.51: timer 4.51 (1/3), 64 readers 4.41 (2/3), unbuffered 4.67 (2/3, one pair 3.83), all 4.57.
 mmap decode, off 1.95: cold-page prefetch 1.71 (-12%, 0/3).
 Cold (no profile) agreed on win_submit and flow pump (+4.5%, +2.7%, 2/2) and showed the timer +12%, readers + unbuffered +22% on decode (2/2, off arm noisy).
 Identity: --vram-experts 0, off vs all IDENTICAL (4K prompt, code decode); text equal to off in every warm arm.
 Stop during a long prompt (H100, 8K and 16K): STOP answered in 0.12 / 0.16 s, the conversation before the prompt kept, the next request identical.
 DSpark cum cut: H100 code 20.13 tok/s off vs 19.60 at 0.5, no gain.
ON: DS4_WIN_SUBMIT, DS4_FLOW_PUMP_WAIT, Stop, STRATA_DS4_LANE_FREE (a no-op on the physical-core lane).
OFF, switch turns it on: DS4_TIMER_MS=1, DS4_ENGRAM_THREADS=64, DS4_EG_DIRECT=1, DS4_IQ_PF_COLD=1, STRATA_DS4_DSPARK_CUM=0.5.
2026-10-24 02:29:57 +02:00
Niko1221 cec75af6ae launch: the Save screen shows DeepSeek's own tuning length (5-10 / 10-15 min) like the Tuning screen; walk test: save text, run paths, error paths 2026-10-24 02:29:04 +02:00
Niko1221 8ac2a8385e tests: the 1.75 bpw set also runs with --tp 1 on 4 AMD cards; release notes: set up again on 4+ AMD cards 2026-10-24 02:24:31 +02:00
Niko1221 894dc24c6c Merge branch 'f-024-mgpu' into f-024-rel 2026-10-24 02:23:57 +02:00
Niko1221 c4dcde7145 dsv41 iq test: IQ1_S set gate_up (f16) tolerance 1.5e-2 (data-order dependent, 7.1e-3..1.08e-2, same on the older build); fix a stale comment in ds4_model.cu (IQ1_S/IQ1_M/IQ4_NL have device kernels) 2026-10-24 02:18:45 +02:00
Niko1221 67d5f44bf8 dsv41 plan: four or more AMD cards run the IST-DAS sets with --tp 1 (8K prompt 370 against 196 tokens a second on 4 x R9700) 2026-10-24 02:17:25 +02:00
Niko1221 6a35eb6c5f release notes: three sets everywhere, the DeepSeek engine is separate, Mac Control 0.1.49 2026-10-24 02:15:32 +02:00
Niko1221 12d0a4a051 Merge f-dsv41-tune (c5e0e1482) into f-024-rel: the third set IST-DAS 1.75 bpw (IQ1_M) in setup, fit and the launch list
One conflict, docs/DEEPSEEK41.md (both sides edited the three-set paragraph and the file table). Resolution: f-024-rel's text kept (IQ1_M first, the model-card sentence, 533 GB for all three, the 'not a model to pick' Engram row), with the setup worker's sentence that setup recommends 1.75 where 2.00 is tight in memory or disk added to the 1.75 entry.
2026-10-24 02:09:11 +02:00
Niko1221 c5e0e14823 DeepSeek: IST-DAS 1.75 bpw (IQ1_M), the smallest official set, as model ist-das-1.75bpw: pins at ISTA-DASLab cb02422c (the 9 earlier files are unchanged there), fit numbers, setup recommends it where 2.00 is tight in memory or disk (warn, never refuse), catalog/TUI text, docs, tests 2026-10-24 02:08:43 +02:00
Niko1221 9bb00346a2 release notes: R9700 prompt numbers from the r97 command (4 cards 8K 505, 32K 818; kernels +36% / +49%) 2026-10-24 02:04:13 +02:00
Niko1221 672612ac1c DeepSeek tables: the IQ1_M (1.75 bpw, ist-das-1.75bpw) row first, 239 GB; quality 'being measured' plus ISTA-DASLab's model-card figure; README + 6 translations, DEEPSEEK41.md, release notes (placeholder until the KL is in) 2026-10-24 02:03:56 +02:00
Niko1221 f4e7685ccf Merge f-024-rel into f-024-tui 2026-10-24 01:59:28 +02:00
Niko1221 267a2f4462 launch: a Runner that cannot be built is shown on the run screen as a plain failure instead of crashing the screens; tests: launch TUI walk (every screen, every key, Qwen and DeepSeek, 80x24 and wide, run paths, error paths) 2026-10-24 01:59:28 +02:00
Niko1221 eec705cc98 release notes: the prompt-skip agreement at the final tip, 99.5% of 799 positions 2026-10-24 01:58:45 +02:00
Niko1221 b0d5f6e57c Merge branch 'f-024-slots' into f-024-rel 2026-10-24 01:52:42 +02:00
Niko1221 f2413d77e9 Merge branch 'f-dsv41-tune' into f-024-rel 2026-10-24 01:52:27 +02:00
Niko1221 4f8770d13e release notes: R9700 prompt numbers with the new kernels (4 cards 8K about 320, 32K about 750) 2026-10-24 01:52:08 +02:00
Niko1221 984f52c827 Merge branch 'f-024-rel' into f-dsv41-tune 2026-10-24 01:51:58 +02:00
Niko1221 aebdc298f9 Merge f-024-hipk (cdf8b98f5) into f-024-rel: gfx12 WMMA i-quant prompt tiles for DeepSeek V4.1 (HIP only, default on for gfx12, STRATA_DS4_HIPK=0 off)
One conflict, src/dsv41/ds4_model.cu (forward_chunks): f-024-rel has the long-prompt skip (kv_only layers), f-024-hipk added DS4_PROF stage marks around the same lines.
Resolution: the f-024-rel code kept as it is, and hipk's three prof_mark(d, 5/6/7) calls added at the same places (before the q/kv gemms, before the snapshot, after the kv_only return). They are no-ops with DS4_PROF unset.
2026-10-24 01:51:58 +02:00
Niko1221 fd5823ab83 Merge branch 'f-dsv41-cuk-zcfix' into f-024-rel 2026-10-24 01:48:40 +02:00
Niko1221 d50da4e3a1 release notes: the run-to-run difference is near-tied words, not only expert placement 2026-10-24 01:48:18 +02:00
Niko1221 624a226d13 dsv41 store zero-copy: clear the device's zc_tab entry when VRAM takes an expert over (a stale pointer into a reused slot made zc_stage copy another expert's blob into the plan: garbage, NaN routing, illegal access) 2026-10-24 01:44:40 +02:00
Niko1221 fa3252ad5d dsv41 serve: with slots off the loop is as before (no anchor copy, same continuation test on an empty engine) 2026-10-24 01:43:35 +02:00
Niko1221 9797056e4d Merge branch 'f-024-rel' into f-024-slots
# Conflicts:
#	serve/test_dsv41.py
2026-10-24 01:43:15 +02:00
Niko1221 e2361d80d2 DeepSeek-V4.1: slot keep and auto-pin default off (switches STRATA_DS4_SLOT_KEEP=1, STRATA_DS4_AUTO_PIN=1) 2026-10-24 01:39:15 +02:00
Niko1221 afd24ea6ae launch: the last lines suggest 'strata launch --stop' (and --save-preset NAME to keep the settings) when no preset was saved; --preset "<name>" only when one exists; tests 2026-10-24 01:27:07 +02:00
Niko1221 4ebba26767 pack_gguf: an Engram file ('engram' or 'deepseek41-engram') is never taken for a model shard or for the model's metadata; real header fixture and tests; setup test: the first release's files are packed as they are with no shard download 2026-10-24 01:25:23 +02:00
Niko1221 598372c3fd setup: repin the DeepSeek 2.00/2.25 sets to ISTA-DASLab's final files (flat repository 0d8f49fb, IQ2_XXS = 2.00 bpw, IQ2_XS = 2.25 bpw, DeepSeek-V4.1-Flash-Engram-IQ4_NL.gguf); the first release's local files (model-2.00bpw-..., engram-IQ4_NL.gguf, in 2.00bpw/ folders) stay accepted by their old size and SHA-256 and are never downloaded again; tests 2026-10-24 01:24:14 +02:00
Niko1221 2e477ecd40 release notes: the 99.75% is agreement between skip on and off over 799 positions 2026-10-24 01:21:59 +02:00
Niko1221 0e121c7a13 DeepSeek table: the IQ4_NL Engram table row (README and 6 translations, DEEPSEEK41.md, release notes); notes: long-prompt skip and the 32,768 thinking budget 2026-10-24 01:21:32 +02:00
Niko1221 415e618085 Merge branch 'f-dsv41-maya' into f-024-rel 2026-10-24 01:20:31 +02:00
Niko1221 dfe29bce61 serve: DeepSeek-V4.1 thinks at most 32768 tokens when neither the request nor the config names a budget
Same default as Maya #363. For the deepseek41 family only: Service.reasoning_budget() answers 32768 when the request has
no reasoning_budget_tokens / thinking_budget_tokens and the config has no reasoning_budget_tokens at all (a config value,
0 included, wins; a request's 0 turns it off). The existing wrap-up (#123) closes the thinking at the budget, and the
answer-room clamp still cuts it to leave the answer its share of max_tokens. Thinking off and other families unchanged.
Tests in serve/test_dsv41.py: the default, a request's 0 and own value, the config's value and 0, another family, and a
5000-token cap that is wrapped up and answered. serve suite: 1027 tests OK (15 skipped).
2026-10-24 01:20:04 +02:00
Niko1221 f51b80c222 README: R9700 numbers in the DeepSeek block (11 tokens/s on one card, 4x about 31, 55 to 69 with DSpark); the same block in the six translated READMEs; release notes: one-card R9700 row 2026-10-24 01:19:04 +02:00
Niko1221 17c585c402 Merge branch 'f-024-hsamsg' into f-024-rel 2026-10-24 01:18:51 +02:00
Niko1221 41e804d93c launch: the hsa_exception sentence ignores hsaKmt lines, and a real cause in the same log (out of memory, no permission on the card) still wins; tests for both 2026-10-24 01:18:16 +02:00
Niko1221 524ff90e1a docs: the final DeepSeek quants (ISTA-DASLab repository) in the release notes and DEEPSEEK41.md 2026-10-24 01:17:00 +02:00
Niko1221 86834afedc Merge branch 'f-024-hsamsg' into f-024-rel 2026-10-24 01:16:46 +02:00
Niko1221 fa77717a57 README: DeepSeek V4.1-Flash (ISTA-DASLab IQ2_XXS / IQ2_XS) in the model list, Terminal only 2026-10-24 01:16:46 +02:00
Niko1221 60d847ba33 launch: a GPU runtime start failure (hsa_exception) says the card was not ready, with a next step; the runtime text stays in the log 2026-10-24 01:16:19 +02:00
Niko1221 a3f76dfd63 Merge branch 'f-dsv41-tune' into f-024-rel 2026-10-24 01:13:51 +02:00
Niko1221 b4418e30df serve: DeepSeek-V4.1 with a short max_tokens (48-1023) caps the thinking at half of it, so a text block always arrives
/v1/messages and /v1/chat/completions with max_tokens 200-600 and the thinking on spent all of max_tokens on the
thinking: the 0.1.45 answer room starts at max_tokens 1024 and a client that gave no budget of its own got no text block.
For the deepseek41 family only, with no budget from the client or the config, the thinking is now capped at max_tokens / 2
for max_tokens from 48 up to 1023 (the existing wrap-up, </think> and the answer follow in the other half). A client's
own reasoning_budget_tokens / budget_tokens wins, "answer_room": false turns it off, thinking off is unchanged, other
families (Qwen) are untouched. Maya #363 closes the thinking in the engine instead; the server's #123 budget already does
that for every engine, so only the short-cap gap needed closing.

Tests (serve/test_dsv41.py SmallCap, fake engine that thinks without end): max_tokens 200, 400 and 600 on both APIs return a
non-empty text block with the thinking wrapped up at half; thinking off answers directly; a client budget wins; a tool
call under a short cap still parses; another family keeps the old behaviour. serve suite: 1025 tests OK (15 skipped).
2026-10-24 01:11:34 +02:00
Niko1221 8a7102ec4d dsv41: a long prompt skips layers 20-39 on all but its last 2,668 rows (STRATA_DS4_PROMPT_SKIP, on by default)
Layers 20-39 of DeepSeek-V4.1-Flash read a prompt's earlier tokens only through layer 20's compressed KV and index
keys, and each looks back one 128-token window more than the layer before. So for the answer and for the rings the
decode reads they need their own work only on the last (40 - 20) x 127 + 128 = 2,668 rows. A prompt of ~3,700 or more
new tokens on one GPU now runs its earlier chunks through layers 0-19 plus layer 20's kv_only part (the hyper-connection
pre-norm, the compressor latent, the index keys, the compressed KV) and nothing of the later layers' attention or
experts; the chunk the cut falls in runs layers 20-39 from the cut row on; later chunks run every layer. The expert flow
plans nothing for the skipped (chunk, layer)s. STRATA_DS4_PROMPT_SKIP: 1 (default) the skip, 2 only the last 128 rows
(DeepSeek's own serving; answers shift a little), 0 off. Same approach as Maya #318.

H100 emulation (64 GB cgroup, 12 GiB VRAM, store 41 GB, 3.6 GB/s SSD cap): 16K prompt 69.3 s -> 51.5 s (236 -> 318
tok/s, two alternating pairs), 4K 32.5 s -> 30.7 s; 800 teacher-forced positions after a 4,200-token skipped prompt: KL
0.00029 against the full work, top-1 against the text 88.11% against 87.98%, ppl 1.5110 against 1.5111.

--score gets DS4_SCORE_PREFIX=N (the segments joined, the first N tokens read as one prompt with the last row's logits,
the rest scored) so the skip can be scored; tools/dsv41/test_prompt_skip.py runs it against a pack when one is named.
The Maya notice file now says later Maya changes are covered by the same notice.
2026-10-24 01:11:34 +02:00
Niko1221 be26cb7995 DeepSeek-V4.1: slots on by default (2 GB cap, one week), docs 2026-10-24 01:10:21 +02:00
Niko1221 736a4136e1 serve: DeepSeek-V4.1 engine gets the slot switch and a slot folder in Strata's data folder (other families untouched) 2026-10-24 01:09:06 +02:00
Niko1221 1d31e2a1e4 tools/dsv41/slot_keep_test.py: two agents taking turns, fresh reads vs shared start vs slots vs restart vs folder cap 2026-10-24 00:59:52 +02:00
Niko1221 b8f9c63ae7 serve: DeepSeek-V4.1 marks the system prompt and tools as the shared start (pin=N) so a sub-agent goes back to it
Only the deepseek41 family; STRATA_DS4_AUTO_PIN=0 switches it off, a client's own strata_prefix wins, Qwen is untouched.
2026-10-24 00:59:52 +02:00
Niko1221 cdc3a26577 dsv41: keep a conversation another request displaces and take it back at its checkpoints (STRATA_DS4_SLOT_KEEP), shared-start checkpoint (pin=N)
A prompt that does not continue the conversation in the model first sets that one aside (position, window rings,
compressor partial groups, each KV source's compressed KV and index keys, its checkpoints), then takes back the kept
conversation it does continue, in RAM or from the slot folder, also across restarts. The latest checkpoint the prompt
still agrees with is used (end of the last prompt, before it, the anchor of the first prompt of a run, and the shared
start the server marks with pin=N). The .meta format and the prefix matching are in ds4_slotmeta.hpp with a GPU-free
test (ds4_slot_test). STRATA_DS4_REUSE=0 reads every request from token 0 (reference for tests). Off unless
STRATA_DS4_SLOT_KEEP=1.
2026-10-24 00:59:52 +02:00
Niko1221 cdf8b98f54 Merge f-024-rel d914211e6 into f-024-hipk 2026-10-24 00:58:00 +02:00
Niko1221 5be4964c2b launch for DeepSeek: the tune text says what it really does and its result lists what was timed (or that nothing could be compared); the plan names an installed DeepSeek engine ('installed, 0.1.49') instead of saying setup will download it; preset test; tests 2026-10-24 00:56:08 +02:00
Niko1221 d914211e64 release notes 0.2.4: the manual DeepSeek engine command 2026-10-24 00:41:47 +02:00
Niko1221 71c674dcdb DeepSeek engine by hand: --dsv41-package FILE (setup and strata launch) or a file put in engine-dsv41/; the package is checked (DeepSeek engine for this system, signed manifest, every file's hash) before it is installed; plain steps in the missing-engine message and docs; tests 2026-10-24 00:41:28 +02:00
Niko1221 0d38517888 dsv41 prompts: expert copies keep going while the host waits for the routing (flow_pump on the busy stream); DS4_FLOW_PUMP_WAIT=0 as before 2026-10-24 00:40:48 +02:00
Niko1221 e51e84e22a dsv41 CPU lane: two cores left free beside the host thread when the lane would take every logical CPU; STRATA_DS4_LANE_FREE=0 as before 2026-10-24 00:40:42 +02:00
Niko1221 cffd240834 dsv41 serve: Stop ends a long prompt's reading at a layer/chunk and keeps the conversation before it (set_cancel / cancelled) 2026-10-24 00:40:09 +02:00
Niko1221 062fd4a95e release notes 0.2.4: the DeepSeek engine is a separate download (automatic and by hand), Qwen engines unchanged 2026-10-24 00:38:34 +02:00
Niko1221 f73ae5a62e dsv41 DSpark: cumulative cut on the drafts (product of confidences >= 0.5); STRATA_DS4_DSPARK_CUM, request key dspark_cum, 0 off 2026-10-24 00:38:21 +02:00
Niko1221 e6e14f2714 dsv41 CPU lane: prefetch only the IQ experts' pages not in RAM (os::prefetch_cold); DS4_IQ_PF_ALL=1 as before 2026-10-24 00:38:01 +02:00
Niko1221 a6cbc3621e dsv41 Windows: Engram rows read unbuffered from their unmapped file; DS4_EG_DIRECT=0 as before, DS4_EG_CHECK compares against a buffered read 2026-10-24 00:37:44 +02:00
Niko1221 aee5db5eb1 dsv41 Windows: 64 Engram reader threads (each ReadFile is synchronous, so the threads are the queue depth); DS4_ENGRAM_THREADS overrides 2026-10-24 00:37:14 +02:00
Niko1221 611c1701d3 dsv41 Windows: 1 ms timer resolution while the engine runs; DS4_TIMER_MS=0 leaves it 2026-10-24 00:37:04 +02:00
Niko1221 5ec845f6bc dsv41 Windows: queued GPU work submitted at once on NVIDIA too (win_submit), after every flag hand-off; DS4_WIN_SUBMIT=0 turns it off 2026-10-24 00:36:40 +02:00
Niko1221 a23b5f209c dsv41: gfx12 WMMA expert tiles on by default (STRATA_DS4_HIPK=0 turns them off) 2026-10-24 00:35:47 +02:00
Niko1221 94413c7057 dsv41: gfx12 WMMA prompt tiles for the i-quant expert sets (STRATA_DS4_HIPK=1, default off) 2026-10-24 00:35:38 +02:00
Niko1221 8fda835383 dsv41: DS4_PROF expert-kernel timers on both set paths 2026-10-24 00:35:37 +02:00
Niko1221 4278304914 dsv41: finer DS4_PROF for the chunked prompt (stages, expert kernels by type) 2026-10-24 00:35:37 +02:00
Niko1221 75c57be831 release notes 0.2.4: Thanks names IST-DAS and Unsloth only 2026-10-24 00:34:09 +02:00
Niko1221 2dbb263b02 launch/setup for DeepSeek: setup installs the screens packages (textual) and launch goes on in plain lines without them; the Tuning screen shows the DeepSeek tuner's real length and what it tries; the Advanced list knows strata-dsv41's own flags; the model menu counts the disk (download + preparation) in 'fits'; docs: tuning and the separate engine package; tests 2026-10-24 00:18:48 +02:00
Niko1221 955c709d96 Merge branch 'f-024-dsml' into f-024-rel 2026-10-24 00:12:55 +02:00
Niko1221 70efffb5b6 serve: a DeepSeek-V4.1 pack that ships the V4-Flash chat template (tool_calls, no space) is rendered with the V4.1 template, so tool calls come back as real calls; STRATA_DEBUG prints the raw ids; test 2026-10-24 00:07:13 +02:00
Niko1221 027691b33d release notes 0.2.4: RTX 3060 row 6-9 tokens/s 2026-10-24 00:06:34 +02:00
Niko1221 00ce92068a setup: banner names the chosen family; the user's own home (root's included) is allowed as a data folder; a missing DeepSeek engine package is said plainly (supported cards), the developer hint only with --verbose; tests 2026-10-24 00:05:11 +02:00
Niko1221 18022ceaf3 launch: the plan says 'drafting DSpark' for DeepSeek, not MTP; test 2026-10-23 23:48:59 +02:00
Niko1221 1904c0261b dsv41 tune: with the drafter on, also try a draft block of 3 against the pack's own 2026-10-23 23:47:48 +02:00
Niko1221 642e76948e setup: shared DeepSeek files (drafter, Engram) are also looked for two folders above the set's own folder (strata launch passes .../off/2.00bpw); test 2026-10-23 23:47:30 +02:00
Niko1221 1d0a8051d5 setup: DeepSeek's engine comes from the signed feed's dsv41 member (strata_control/dsv41_engine.py: same download, resume and verification as an engine update) into engine-dsv41/, beside the engine; the flat-zip route is dropped; tests 2026-10-23 23:46:46 +02:00
Niko1221 9f04da3afc Merge branch 'f-024-rel' into f-dsv41-tune 2026-10-23 23:37:40 +02:00
Niko1221 134f24f199 setup: DeepSeek's engine is its own package (engine-dsv41/ beside the engine, never inside it): downloaded with resume and a SHA-256 check from the --prebuilt base, found before the old all-in-one place; launch's note says so; tests 2026-10-23 23:37:19 +02:00
Niko1221 6655733d5e feed: a separate dsv41 member for the DeepSeek engine packages (Feed.dsv41, pack-engine --prefix, build-feed --dsv41); Qwen engine list untouched; tests 2026-10-23 23:35:00 +02:00
Niko1221 f9f297bf90 Merge branch 'f-dsv41-tune' into f-024-rel 2026-10-23 23:33:46 +02:00
Niko1221 ec5b2bdc4e dsv41 win: --dspark-block N (at most N drafts a round) 2026-10-23 23:33:40 +02:00
Niko1221 a2aeb4ecb1 launch: a real short tune for DeepSeek-V4.1-Flash (tools/dsv41/tune.py: the drafter on or off, CPU threads; gains over 5% only, written into the launch config; any failure leaves the standard settings with a plain line); the launch no longer forces --tune skip for it; tests 2026-10-23 23:31:51 +02:00
Niko1221 3feaa2aa3f Merge branch 'f-dsv41-setup' into f-dsv41-tune 2026-10-23 23:30:35 +02:00
Niko1221 8ea7fef827 Terminal 0.2.4 / Control and engine 0.1.49: version bump for the DeepSeek V4.1-Flash release (app stays 0.2.3, Mac stays 0.1.48); release notes 2026-10-23 23:29:00 +02:00
Niko1221 36a94857a1 dsv41_fit: remove a duplicated constants block; one unit (tokens a second) 2026-10-23 23:28:05 +02:00
Niko1221 28ce98bf16 Merge f-dsv41-rel (0be62ccad) into v0.2.3.1: DeepSeek V4.1-Flash on top of the released 0.2.3 line
f-dsv41-rel was based on the 0.2.2-era tree (merge-base 1f727f4b5), so the merge brings it onto 7ddc17a38 without dropping 0.2.3 / 0.2.3.1.
Conflict resolutions (0.2.3.1's behaviour kept everywhere that is not DeepSeek):
- packaging/package_engine.py: build() keeps 0.2.3.1's check_reader / allow_stale_vision / allow_no_reader and adds require_dsv41; both the
  picture-reader check and the strata-dsv41 presence check run; the CLI call passes both sets of arguments.
- packaging/ENGINE-COMPONENT.md: 0.2.3.1's cuda13 / cuda12 rows (vision built per variant), with strata-dsv41.exe added to the cuda13 contents.
- serve/server.py: the parser is self.parser_cls (OutputParser, or the DeepSeek DSML parser) and keeps 0.2.3.1's declared_only argument.
- strata_cli/launch/cmd.py: the --plan output prints 0.2.3.1's drafting line and then the DeepSeek placement lines.
- strata_cli/launch/runner.py: plan() returns 0.2.3.1's dict with "drafting", plus "placement" for multi-file (DeepSeek) models;
  drafting_plan_line() kept; dsv41_engine() and placement() added.
2026-10-23 23:27:29 +02:00
Niko1221 0be62ccad7 Merge f-dsv41-setup 86ba33845 into f-dsv41-rel (serve: DeepSeek turn without </think> returns its text as content; pack recipe noise token/markov rank; exec bits in packages; console-safe smoke) 2026-10-23 23:00:32 +02:00
Niko1221 86ba33845f serve: a DeepSeek turn that stops without </think> and wrote no answer returns its text as the answer (streaming ends with it as content; the non-streaming OpenAI and Anthropic replies drop the identical reasoning); tests 2026-10-23 23:00:05 +02:00
Niko1221 6f507fb210 packaging: strata-dsv41 and strata-vision get execute bits in the package and in the tar (ELF header decides when packed on Windows); setup restores them on an engine copied without modes 2026-10-23 22:49:29 +02:00
Niko1221 1a7b4ae236 setup: pack recipe states --dspark-noise-token 128799 and --dspark-markov-rank 256; plain wording (nothing to download, one unit: tokens a second, the one-time SHA-256 check and its time, the remembered-folder line) 2026-10-23 22:48:21 +02:00
Niko1221 a9dd554e13 setup: the smoke reply is made printable for the console code page before it is shown 2026-10-23 21:54:52 +02:00
Niko1221 3724818db5 Merge f-dsv41-setup into f-dsv41-rel (setup installs the official IST-DAS 2.00/2.25 sets; strata-dsv41 in the engine packages) 2026-10-23 21:54:42 +02:00
Niko1221 e9546cb564 Merge f-dsv41-cpul e5f536a6c into f-dsv41-rel (AVX-512 one-token rows for Zen 4/5, bit-identical to ggml, per-type start-up self-check, chosen by the detected CPU) 2026-10-23 21:10:53 +02:00
Niko1221 8de8d90287 dsv41 smoke: utf-8 output (the reply has DeepSeek's special tokens; a Windows console code page failed on them) 2026-10-23 21:09:46 +02:00
Niko1221 e5f536a6cb dsv41 cpu lane: AVX-512 rows from 1 token on Zen 4 as well (IQ2_XXS, IQ2_XS; IQ3_XXS and IQ1_M from 2), measured 2026-10-23 21:02:09 +02:00
Niko1221 b6cc7e681b setup: DeepSeek family line names IST-DAS for the official 2-bit GGUFs and Unsloth for UD-Q8_K_XL; says who made the files of the chosen version 2026-10-23 20:41:48 +02:00
Niko1221 f2323d876d setup test + docs: no --prefill-chunk, one plain line about the engine's own chunked prompt 2026-10-23 20:37:24 +02:00
Niko1221 10af5bb1e4 setup: drop --prefill-chunk (the chunked prompt is the engine's default now); doc line 2026-10-23 20:37:14 +02:00
Niko1221 237eb667d3 Merge branch 'f-dsv41-rel' into f-dsv41-setup 2026-10-23 20:37:12 +02:00
Niko1221 d6f18babe9 Merge f-dsv41-r97 fb321d62c into f-dsv41-rel (layer-major chunked prompt as the engine default) 2026-10-23 20:12:55 +02:00
Niko1221 1747c8e538 dsv41 cpu lane: one-token AVX-512 rows - sign masks by popcount (IQ2_XXS, IQ2_XS, IQ3_XXS), IQ1_M AVX-512 rows with pdep indices; chosen per CPU (Zen 5 from 1 token) 2026-10-23 20:09:59 +02:00
Niko1221 f1cb66d09e dsv41: strata-dsv41 --version and a 'ds4: build <commit>' line with --verbose (commit from git at configure, a BUILD_ID file in an archive, or -DSTRATA_BUILD_ID) 2026-10-23 20:03:24 +02:00
Niko1221 4eedb86b4f Merge f-dsv41-h100 (store pre-grow: register the whole arena before the first kernel on Linux, fixes the --ssd-cache zero-copy hang) 2026-10-23 20:01:41 +02:00
Niko1221 e917cf5b37 Merge branch 'f-dsv41-rel' into f-dsv41-setup 2026-10-23 20:01:11 +02:00
Niko1221 c506238ce2 setup: DeepSeek official sets start with --dspark-conf 0.65 (H100 measured default) 2026-10-23 20:00:50 +02:00
Niko1221 c35710e388 dsv41: store arena pre-grown by default on Linux when the GPU maps it 2026-10-23 19:59:29 +02:00
Niko1221 fb321d62cb dsv41: the layer-major (chunked) prompt is the default on every run: chunks of 1024 for the single-card default run, staging ring 16 MB where the cards hold most of the experts 2026-10-23 19:59:13 +02:00
Niko1221 f7d7072841 dsv41: DS4_STORE_PREGROW=1 (map the whole store arena before the first kernel; cuMemHostRegister mid-decode hangs on Linux) 2026-10-23 19:58:22 +02:00
Niko1221 1c1005952a Merge f-dsv41-cuk 86bdfbd0d (HIP keeps the release grids and kernels). One conflict: the identical IQ1_M code-table HIP guard (9c46ab821 vs cuk); took cuk's wording. 2026-10-23 19:52:52 +02:00
Niko1221 41856f6594 dsv41: staging ring 64 MB when the experts are within 30% of fitting the cards (R9700 x4 plain 2.00: ratio 1.16) 2026-10-23 19:45:21 +02:00
Niko1221 0beae0bca6 dsv41: the store fill after a chunked prompt reaches the store's whole capacity, not only its allocated slots 2026-10-23 19:44:54 +02:00
Niko1221 9469412db2 setup: DeepSeek official sets start with --prefill-chunk 1024 (layer-major prompt path, measured 231 tok/s at 8K on a 12 GB PC) 2026-10-23 19:44:40 +02:00
Niko1221 a345d9f8cf Merge branch 'f-dsv41-rel' into f-dsv41-setup 2026-10-23 19:44:39 +02:00
Niko1221 a82477069b Merge branch 'f-dsv41-pkg' into f-dsv41-setup 2026-10-23 19:41:53 +02:00
Niko1221 81dd215de9 setup: the two official IST-DAS DeepSeek-V4.1-Flash sets (2.00 / 2.25 bpw) as sizes: pinned files, shared Engram + DSpark, plain pack recipe, shipped profiles, pack-info and a 16-token text check, --dspark auto; fit words from the measured numbers 2026-10-23 19:41:49 +02:00
Niko1221 4b5f8d89f2 tests + ENGINE-COMPONENT.md: strata-dsv41 in the engine package and the --require-dsv41 refusal 2026-10-23 19:39:24 +02:00
Niko1221 dc352b839b dsv41: say how the chunk staging ring was sized 2026-10-23 19:36:53 +02:00
Niko1221 e838e2beca Merge f-dsv41-cpul 02b07b686 into f-dsv41-rel (exact multi-row CPU-lane rows for IQ types, bit-identical to ggml-cpu, self-check per type, DS4_GG_IQ256=0 switch) 2026-10-23 19:35:58 +02:00
Niko1221 6053a43841 packaging: build_dsv41_windows.bat and build_dsv41_cuda.sh build strata-dsv41 for the NVIDIA release packages (sm_86 + sm_120 fat) 2026-10-23 19:35:51 +02:00
Niko1221 faaf82acd7 build_hip_linux.sh: build strata-dsv41 (own build tree) and ship it in the rx9070 package; DSV41=1|0 overrides, --require-dsv41 on the package 2026-10-23 19:35:51 +02:00
Niko1221 8fee63f4f0 package_engine: ship strata-dsv41 beside the engine (picked up like the picture readers); --require-dsv41 refuses a package or a --verify without it 2026-10-23 19:34:59 +02:00
Niko1221 7519b1c75c pack_gguf: warn loudly when --skip-tensors / --dense-override are used on an official IST-DAS set (engram_k/q/wkv present): the rebuilt pack loads but gives garbage text; the plain recipe is right 2026-10-23 19:27:17 +02:00
Niko1221 dbf3bb2e64 dsv41: the layer-major run's RAM is given back before the cache catch-up and the store fill 2026-10-23 19:24:01 +02:00
Niko1221 86bdfbd0da dsv41 iq: HIP keeps the release grids and kernels (no persistent work-item grid there) 2026-10-23 19:23:13 +02:00
Niko1221 796dbb77b4 dsv41 iq: the IQ1_M code-table copy is CUDA only (kIq1mCodes does not exist on HIP; found by building the HIP-guarded paths under nvcc with -DSTRATA_USE_HIP) 2026-10-23 19:17:14 +02:00
Niko1221 96a47b79d5 dsv41: after a chunked prompt the RAM store is filled with the prompt's hottest CPU-lane experts (a window prompt fills it through the CPU lane; the chunk path streams past it) 2026-10-23 19:14:44 +02:00
Niko1221 9c46ab821a dsv41 iq: kIq1mCodes is CUDA-only, so its use in load_tab_fast is under #if !STRATA_USE_HIP (HIP failed 'undeclared identifier kIq1mCodes': non-dependent names are looked up in the dead constexpr branch) 2026-10-23 19:13:07 +02:00
Niko1221 12595032e7 Merge f-dsv41-cuk a3d955201 into f-dsv41-rel (persistent grids only for codebooks >= 8 KiB: fixes 2.5x slowdown with 1 active expert) 2026-10-23 19:06:12 +02:00
Niko1221 a3d9552015 dsv41 iq test: golden compare also covers dynamic-count window groups (5/1/3 entries, ne_max 5; 9 groups with 4 live) 2026-10-23 19:05:14 +02:00
Niko1221 01220edb67 dsv41 iq test: DS4_BENCH_WIN (window path / DSpark verify bench: n entries a group) 2026-10-23 19:01:30 +02:00
Niko1221 02b07b686a dsv41 cpu lane: AVX-512 rows chosen by the detected CPU (Zen 5 family 0x1A: IQ2_XXS and IQ3_XXS; Zen 4 0x19: IQ2_XXS; others none), prompt chunks of 7+ tokens only 2026-10-23 18:51:19 +02:00
Niko1221 a70b006929 dsv41 iq (CUDA): persistent work-item grids only for codebooks of 8 KiB+ (IQ1_M/IQ1_S/IQ2_S), lazy codebook load, small-codebook types keep the release grids and kernels (no regression with few active experts) 2026-10-23 18:49:14 +02:00
Niko1221 4cca3fac20 dsv41: --spec-eval honours --dspark off 2026-10-23 18:44:21 +02:00
Niko1221 995ff61f3c dsv41 cpu lane: AVX-512 rows by default for IQ2_XXS prompt chunks (7+ tokens), measured on Zen 4 2026-10-23 18:43:28 +02:00
Niko1221 f5fc88c599 dsv41 iq (CUDA): token and light kernels take work items (group, row tile) round a persistent grid (any number of active groups shares all blocks), IQ1_M codes from a generated table (ds4_iq1m_codes.inc), persistence only for codebooks of 8 KiB+; many-row light sets keep the release kernel; test: --bench-tiles, DS4_BENCH_ACTIVE 2026-10-23 18:41:55 +02:00
Niko1221 428950b96c Merge catch-up: f-dsv41-win fd19f9d6b, f-dsv41-h100 c0f944e12, f-dsv41-cuk 2026-10-23 18:35:43 +02:00
Niko1221 0553ba6c3f dsv41 iq (CUDA): light sets (DSpark verify, short prompts) take the persistent grid and the IQ1_M code table for experts of <= 4 rows, 4-block occupancy kept for 8+ rows; test: --bench-light 2026-10-23 18:16:11 +02:00
Niko1221 fd19f9d6bf dsv41 win: store off the lookahead by default 2026-10-23 18:09:27 +02:00
Niko1221 b43b0bb518 dsv41: kEntMax declared before Ent 2026-10-23 18:06:26 +02:00
Niko1221 d2411af9fd dsv41: i-quant CPU-lane entries take a whole window's tokens of an expert (cap 16, was 8) for the exact multi-token rows; the chunk staging ring is 64 MB where the cards hold 90% of the experts (decode after a chunked prompt back to the windowed path's speed) 2026-10-23 18:04:51 +02:00
Niko1221 49e35e8bb7 dsv41 cpu lane: exact AVX-512 rows (iq512x_rows, 1-6 tokens and prompt tiles), opt-in DS4_GG_IQ512=1 until measured on Zen 4 2026-10-23 18:04:12 +02:00
Niko1221 c25724a222 Merge branch 'f-dsv41-cpul' into f-dsv41-r97 2026-10-23 18:01:40 +02:00
Niko1221 d689e6080d Merge f-dsv41-cuk into f-dsv41-rel (per-device light-set scratch and tensor-core check, faster IQ1_M/IQ2_S/IQ3_XXS token kernels) 2026-10-23 17:59:37 +02:00
Niko1221 cf4f8befe7 dsv41 cpu lane: take the AVX2 rows by ggml-cpu's own AVX2/FMA/F16C answer (MSVC builds have no __builtin_cpu_supports) 2026-10-23 17:59:03 +02:00
Niko1221 1cace5c864 dsv41 iq (CUDA): per-device light-set scratch and tensor-core check (stream 0 is one handle on every device: the scratch of the first GPU was reused on the second), block/sub-block loop form in the token kernels; test: --devices (in-process multi-GPU, bit-compared) 2026-10-23 17:58:59 +02:00
Niko1221 7e9b0dcc25 dsv41 cpu lane: exact AVX2 rows for 7..64+ tokens an expert (the prompt path): decode once, u8 activations, no per-token sign; IQ1_M too
T tokens in tiles of 16 and rows in groups of 8 (activation slice and accumulators in L1): 7.7-9 ns per block and token on IQ2/IQ3 rows against
28-60 for ggml's dot (x3.6-6.6), IQ1_M 16 ns (x1.8); bit-identical to ggml (ds4_gg_bench --exact at 7, 8, 16, 33, 64 tokens, all six types).
gg_cpu_gate_up takes up to 64 tokens a call (and any number in pieces); ds4_gg_bench --expert times the whole expert pipeline.
2026-10-23 17:57:53 +02:00
Niko1221 0b48e23fa5 dsv41 win: --dspark auto (not loaded when the experts do not fit the GPU), --ids @file, h100sim 2026-10-23 17:57:34 +02:00
Niko1221 f6e4cd6899 dsv41: chunked prompt for a single card (--prefill-chunk N), the expert cache learns from chunks and catches up after a prompt, chunk buffers fall back by halving, CLI feeds a whole prompt
The layer-major chunked prompt path only ran with --threads on / --tp > 1. --prefill-chunk N (N > 0) now enables it on one card too (kChunkedPromptDefault stays false until the decode gate is signed off). A chunk's routing bumps the layers' heat and CPU-lane hits, and cache_settle() runs the swap rule in rounds after the prompt so the decode that follows finds the hot experts in VRAM (8K prompt: 1699 swaps, 5.6 s). If the chunk buffers do not fit, the chunk is halved down to 256 rows, then windows of 16 with one line saying so. ds4_main: --window 0 (default) feeds a whole prompt when the chunk path is on.
2026-10-23 17:47:43 +02:00
Niko1221 c0f944e127 dsv41: DS4_PRED_DUMP / DS4_XIN_DUMP (router logits + projected router input; router inputs for the expert-similarity scan) 2026-10-23 17:44:34 +02:00
Niko1221 6aba354c16 dsv41 cpu lane: the exact AVX2 rows on by default from 2 tokens (1 on IQ3_S, 3 on IQ1_M), 5-6 tokens in one pass, IQ1_M rows rewritten
The rows are ggml-cpu's vec_dot bit for bit at any token count, so the default cannot change an answer; a one-time per-type check against the
ggml-cpu build in use turns them off if they ever differ (DS4_GG_IQ256=0 never, =1 always).  EX rows: 5 and 6 tokens in one decode pass
(were 4+rest), independent integer chains for 1-2 tokens, IQ1_M: delta dot from per-call pair sums of the activation, group signs from one
shifted load.  The main engine's iq256_rows hash is unchlanged (phaseA-tests/cpul/hash_iq256.cpp).
2026-10-23 17:42:52 +02:00
Niko1221 71abb6c163 dsv41 iq (CUDA): persistent token kernels (codebook loaded once a block) and IQ1_M decode through a 4096-entry code table (delta sign folded in): bit-identical, IQ1_M layer -15% sm_120 / -30% sm_86 2026-10-23 17:39:02 +02:00
Niko1221 cf45533cf6 dsv41 cpu lane: AVX2 multi-token i-quant rows in ggml's float order (bit-identical to ggml-cpu at 1..8 tokens), IQ1_M rows
The main engine's iq256_* rows keep their float order (EX=false, the default; hash of their outputs on all types and 1..8 tokens is
identical before/after, phaseA-tests/cpul/hash_iq256.cpp).  The new iq256x_rows (EX=true) sums as ggml-cpu does - block scale f16(d)*y.d, the
format constant after the sum, ggml's hsum - so a token's dot no longer depends on how many tokens share the pass (the greedy ids of a
DSpark run changed with DS4_GG_IQ256=1 because the old order differed in the last bits).  IQ1_M (29), single-token in ggml, gets a
multi-token AVX2 row.  ds4_gg_bench --exact: 0 differing bits for IQ1_M, IQ2_XXS/XS/S, IQ3_XXS/S at 1..8 tokens.
2026-10-23 17:28:19 +02:00
Niko1221 95994c45c5 dsv41 iq test: --bench-big (>=640 MiB working set, rotated experts: DRAM rates), --dump/--cmp (bit-exact golden compare incl. real shapes and grouped path) 2026-10-23 17:18:21 +02:00
Niko1221 ea081e3312 dsv41 rel: one IQ1_M implementation in ds4_iq.cu (the textual merge kept both; port's kernels stay), win's dequant_rows (chunked-prompt dense dequant) kept and takes IQ1_S too; strix add4 dropped with win's IQ1_M decode (port's decode has no __vadd4) 2026-10-23 17:06:08 +02:00
Niko1221 8cfe988383 Merge f-dsv41-strix into f-dsv41-rel (HIP build fixes: portable add4 for the IQ1_M decode, strata_core link for the DXGI budget, HIP build scripts, umaprobe)
No conflicts. The strix add4 edit applies to the merged IQ1_M decode; no __vadd4 is left in ds4_iq.cu.
2026-10-23 17:02:44 +02:00
Niko1221 e5c70bbaa0 Merge f-dsv41-h100 into f-dsv41-rel (--profile-run, DS4_TRACE on the chunked path, --spec-eval) 2026-10-23 17:02:30 +02:00
Niko1221 c70ae83d4c Merge f-dsv41-port into f-dsv41-rel (release integration)
Conflict decisions (win side = f-dsv41-win, port side = f-dsv41-port):
- ds4_iq.cu: kept port. Its IQ1_S/IQ1_M/IQ4_NL kernels are verified bit-exact on HIP gfx1201, sm_86 and sm_120; win's
  IQ1_M table load and supported() list are a subset (IQ1_S, IQ4_NL added).
- ds4_kernels.cu/.cuh: kept port's engram_dequant fmt int (0 MXFP8, 1 Q6_K, 2 IQ4_NL) and its iq4nl_value, replacing
  win's q6k flag and iq4nl_value; same arithmetic.
- ds4_model.cu: kept port's eg_fmt vector and its Engram type check (unknown type exits 2 instead of segfaulting);
  win's eg_q6k is gone, the duplicate IQ4_NL row branch from the textual merge is removed.
- ds4_packinfo.cpp: kept port (device types derived from iq::supported()).
- iq_expert_test.cpp: kept port (hip_or_cuda.h); win's tests/dsv41/hip_as_cuda.h removed (nothing else used it).
  The IQ1_M test-d placement is the same bytes in both versions.
- CMakeLists.txt: kept port's test-language block (HIP, CUDA or CXX for the CPU-only build); win's
  _strata_dsv41_test_lang variable dropped.
- pack_gguf.py: kept port's type table (IQ1_S/IQ1_M comment) and combined both missing-Engram checks
  (has_part from win's split Engram layout AND port's dense-override names).
2026-10-23 17:02:22 +02:00
Niko1221 f232cbfed6 dsv41 win: store test covers cancel and shrink; shrink finalizes read slots 2026-10-23 16:59:01 +02:00
Niko1221 08224f228f dsv41 win: priority SSD queue with cancellation (demand before guesses), partial GPU visibility, store shrink on RAM pressure, predsim 2026-10-23 16:53:02 +02:00
Niko1221 c8ead78419 dsv41 win: store init last, zc off by default, flow reads the store, release notes 2026-10-23 16:37:01 +02:00
Niko1221 55e1a53cfd dsv41: --spec-eval and DS4_ROUND_LOG (DSpark acceptance per round, every draft verified) 2026-10-23 16:32:59 +02:00
Niko1221 8da42765dd dsv41: DS4_TRACE also records the chunked prompt path's picks 2026-10-23 16:29:22 +02:00
Niko1221 75c9f2d81e dsv41: profile-run accepted without --ids 2026-10-23 16:25:51 +02:00
Niko1221 2e61fef4d1 dsv41 win: store auto size, profile preload, decode staging zc_tab (T=1), release plumbing 2026-10-23 16:21:59 +02:00
Niko1221 fb141be3e0 dsv41: profile-run string fix 2026-10-23 16:21:19 +02:00
Niko1221 9051f31306 dsv41: --profile-run mode (feed id segments without logits, expert profile saved as it goes) 2026-10-23 16:20:57 +02:00
Niko1221 17bc654b6d dsv41 strix: umaprobe (UMA budget, hipMalloc reach, host registration, zero-copy read bandwidth), build script 2026-10-23 16:10:12 +02:00
Niko1221 084e654bcb dsv41 on Windows HIP (gfx1151): build script for ROCm 10.2 + MSVC, Linux HIP build script, portable per-byte add for the IQ1_M decode (no __vadd4 on HIP), strata_core link for the DXGI-budgeted cudaMemGetInfo 2026-10-23 15:38:17 +02:00
Niko1221 7d2307deb0 dsv41 win: the store's slots in blob layout, GPU-visible (zero-copy mapped) with a PCIe staging copy for decode picks, node-free reserved arena 2026-10-23 15:33:54 +02:00
Niko1221 226d73b45b tools/dsv41/engram_fold.py --gate-kv-from: make the ple table the only variable, and the answer it gave
Deriving their engram.gate from their k*q and transposing their wkv scored 0.409 then 2.996 KL - both my arithmetic, not
their data (their k/q/wkv are stored transposed and the gate is not k*q in the engine's layout).  --gate-kv-from <pack>
copies those two tensors from a pack that is known good, so a pack can carry their ple table and nothing else of theirs
differ.  That is how the comparison the plan asks for was finally made (numbers in EACH-PC-PLAN/R9700-BOX.md, step 2):
their 110.6 GB IQ4_NL table scores KL 0.16787 / ppl 3.627 against 0.16502 / 3.588 for our 203 GB MXFP8 one - +0.003 KL,
inside this engine's run-to-run placement noise - while saving 92 GB of disk.

Also fixes the fold's reads: Gguf's tensor offsets are relative to the shard's data section (pack_gguf adds data_start),
so the fold must add it too; without that every tensor came out as NaN.
2026-10-23 15:10:00 +02:00
Niko1221 52543768c7 dsv41: one runtime header for the tests on both backends (HIP or CUDA), so a CUDA box needs no shim on -I
tests/dsv41/{iq,tq}_expert_test.cpp included <hip/hip_runtime.h> unconditionally, which no CUDA toolkit ships - every CUDA
box had to pass a hand-written shim on -I (found on l620 and z440, 2026-10-23, and reported as the one thing worth
upstreaming).  include/strata/dsv41/hip_or_cuda.h now provides the handful of HIP names those tests use on a HIP build and
maps them onto the CUDA runtime otherwise (hipMalloc/Free/Memcpy, device sync, the error type and the events).

Verified on r9700 that the HIP build is untouched: incremental build rc=0, ds4_iq_test --type 29 ALL OK, ds4_tq_test ALL OK.
2026-10-23 15:00:19 +02:00
Niko1221 3826885f4b tools/dsv41/engram_fold.py: transpose their Engram layout, but the fold's reader is still wrong
Their engram_k/q/wkv are transposed against the engine's layout (wkv [6144, 25600] against [25600, 6144], k/q [5120, 4]
against a [4, 5120] gate), so the fold now transposes and writes the engine's shape.  The pack builds (their IQ4_NL table,
all files present) and runs without crashing, but it scores KL 0.40915 - and that is this tool's bug, not their data: a
direct read of their BF16 engram_k through Gguf's offsets already comes out as NaN, while the engine reads the same tensors
through a pack fine, so the fault is in how this code turns an offset into bytes (the Q5_K dequant inherits it).

Do not quote 0.409 as the IQ4_NL-vs-MXFP8 comparison; that measurement is still open.  Next step is to read the tensors the
way pack_gguf writes them into tensors.txt and check one row against the engine's own dump.
2026-10-23 14:53:02 +02:00
Niko1221 e7b3f15240 dsv41 win: RAM expert store (unbuffered reads, policy, lookahead fill), Windows direct reads, dense dequant for the chunked prompt, SMT option, store test 2026-10-23 14:44:51 +02:00
Niko1221 7ddc17a38f tests: the old-launcher test expects the 0.2.3.1 version line 2026-10-23 14:37:21 +02:00
Niko1221 d62ff4a608 tools/dsv41: engram_fold.py (their k*q gate and wkv from their own shards) and pack_gguf accepts an override for the Engram tensors
Their sets keep the Engram as three per-layer tensors (blk.N.engram_k/q/wkv) while the engine wants engram.gate (k*q
folded) and engram.wkv; their engram-IQ4_NL.gguf carries only the ple table, so a pack built around their Engram stopped at
'blk.1.engram_kv.weight: missing'.  engram_fold.py folds the gate and renames the kv into a dense_requant-style folder, and
pack_gguf now treats a tensor a --dense-override supplies as present (it checked the quant's shards and the Engram set only).

Result: packs/ista_2.00_native - 48784 tensors, their IQ4_NL table, Engram layers 1,14, 2896 Engram reads for a 32-token
prompt, no crash.

NOT yet a measure of their table: it scored KL 0.40915 / ppl 4.769 against 0.16502 / 3.588 for the same set with our MXFP8
table, because their engram_k/q/wkv are stored TRANSPOSED relative to the engine's layout (their engram_wkv is [6144, 25600]
against [25600, 6144]; their k/q [5120, 4] against the [4, 5120] gate).  Copying the bytes as they are yields a valid pack
with the wrong data.  engram_fold.py still needs the transpose (gate in float; the Q5_K kv dequantised, transposed, written
BF16) - Phase 1.1's 'load their layout as published'.
2026-10-23 14:36:12 +02:00
Niko1221 488dd8459c dsv41: --pack-info's device-type list is derived from iq::supported() instead of copied
It was a hand-kept copy of the supported() switch and fell behind when IQ1_S, IQ1_M and IQ4_NL got kernels: the 2.00 set's
60 IQ1_M expert tensors (59.5 GB) were reported as 'no GPU kernel ... the CPU lane takes them' while the kernels were in the
same binary and ds4_iq_test passed them.  That is exactly the line an operator trusts before a download, so it now walks
ids 0..63, asks iq::name()/iq::supported(), and adds MXFP4 and the trellis types by hand (they are not in that switch).
Verified: packs/ista_2.00bpw_9e2528f and packs/h225 both report 'no GPU kernel: none'.  Also fixes the intake's eval call
(session16.sh takes the pack directory name, not a path - the 2.00 score had failed with an empty runs/s16_<path>.tsv).
2026-10-23 14:21:38 +02:00
Niko1221 b2d2cf22eb dsv41: the Engram reader takes IST-DAS's IQ4_NL table, and a pack with an unknown Engram type stops cleanly instead of segfaulting
Found and fixed on p620, the only box with their engram-IQ4_NL.gguf (110.6 GB). Before this, a pack built around their
file loaded for 38 s and then died in Model::Impl::eg_issue at the first token (3 of 3 runs, addr2line 0x6d3450).

- Model::Impl::load: one guard line and exit(2) for an Engram tensor whose type the reader does not handle, naming the
  layer and the type (verified with a pack retyped to IQ4_XS, rc 2; a missing embed tensor also exits 2). The guard stays
  for the next unknown type.
- eg_q6k becomes eg_fmt per layer: 0 MXFP8 ([scale, 32 codes] blocks, 264 B a row), 1 ggml Q6_K (210 B a 256), 2 ggml
  IQ4_NL (18 B a 32: an fp16 d and 16 nibble bytes), 3 an old pack's split FP8. The load loop sniffs the type from the
  pack's tensor table, eg_issue builds the row descriptor from it (144 B a row, off + id*144, no scale tensor), and
  EgReader::read() needed no new branch: its contiguous, scale-free path already served Q6_K.
- ds4_kernels.cu: engram_dequant_kernel and engram_dequant_tok_kernel take the format and add the IQ4_NL branch, a literal
  port of ggml's dequantize_row_iq4_nl (fp16 d per 18-byte block, low nibbles 0-15, high 16-31, kvalues_iq4nl).
- The Engram row slot stays cols*(hd+hd/32) for every format, so existing packs are byte-identical: --pack-info on the
  MXFP8 h225 pack and on a Q6_K zero pack is unchanged, and a Q6_K pack's 32-token ids are identical old against new.

Verified with their real table: the pack that segfaulted runs 32 tokens in 1692-1711 ms (18.1-18.3 tok/s) and produces
coherent Python (GGML_TYPE_Q2_0 = 42, GGML_TYPE_NAMES: dict[int, str]); 64 tokens on the code prompt, DSpark off 17.16
and on 23.36 tok/s (44 of 53 drafts accepted). IQ4_NL decode is bit-exact against ggml on 8 of 8 sampled rows, and the row
addresses match the pack's tensor table (data_start 320, 144 B a row).

Not covered: no non-zero MXFP8 table existed on the box to re-check that path in-engine (the Q6_K and synthetic-zero MXFP8
packs were), there is no in-engine float dump against the Python reference (the proof is formula- and address-level), and
the legacy split-FP8 format (3) is kept but untested.
2026-10-23 14:13:26 +02:00
Niko1221 7b3e2eaa0b Control 0.1.48.1 / Terminal 0.2.3.1: version bump for the Control-only hotfix of #3810 (app stays 0.2.3, engine packages stay 0.1.48); release notes 2026-10-23 14:10:33 +02:00
Niko1221 f5f205bee0 setup: the HIP ready-made-engine checks read gfx names with arch_names, not arch_numbers (an installed AMD engine was never recognised); tests for archs as a list and as a string 2026-10-23 14:07:48 +02:00
Niko1221 0edd96ccf6 setup: BUILD.json archs / vision_archs are read as a list or as the ';'-separated string the packaging writes (setup stopped on the released Windows CUDA package: int(';'), #3810) 2026-10-23 14:05:32 +02:00
Niko1221 eb6c0a23f0 dsv41 win: pack_gguf reads the IST-DAS Engram parameters from tensors, IQ1_M type, packinfo 64-bit sizes, DS4_TRACE_AHEAD, cachesim 2026-10-23 13:17:02 +02:00
Niko1221 c523890228 dsv41: a CPU-only build path, and the intake's pack recipe fixed
1. CMakeLists: -DSTRATA_BUILD_DSV41=ON was silently dropped when no GPU backend was on, because the whole DSV41 block
   sits inside if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP).  Found on p1004 (2026-10-23), whose P100 is not a DeepSeek
   target but which is exactly the box the CPU-lane A/B belongs on.  A new branch builds ds4_gg_bench with no GPU
   runtime at all.  Verified on r9700 with both backends off: configure rc=0, build rc=0, the benchmark runs IQ3_XXS at
   60.5 GB/s (ggml-cpu) against 64.1 (AVX2 rows, x1.06) and 8.02e-09 against a double reference.

2. tools/dsv41/ista_intake.py: the pack step now uses the recipe that actually builds a loadable pack for these sets -
   --engram-meta (the UD set's first shard carries the Engram hash parameters, without it pack_gguf sees no Engram layers
   and refuses per_layer_token_embd), --mmproj, the combined --skip-tensors regex, and the DSpark noise token and markov
   rank (pack_gguf fills the rest of the geometry from the drafter but not those two, so a pack came out with
   dspark_noise_token_id 0).  Verified end to end on the 2.25 set: the intake rebuilds packs/h225's content exactly -
   same 48794 tensor lines, --pack-info 'files: 10, all present', 'no GPU kernel: none', dspark geometry 5/128799/256/128/3.
2026-10-23 12:56:24 +02:00
Niko1221 5c523d55f7 dsv41: device kernels for IQ1_S, IQ4_NL and IQ1_M (the three IST-DAS types that had none)
Found and implemented on z440's CUDA bring-up, applied here and re-verified on HIP.

ds4_iq.cu: decode<T>/deq8<T> for kIQ1_S, kIQ4_NL, kIQ1_M, plus iq1_codes() (iq1s_grid_gpu's 4-bit nibbles -> the int8 codes
8*(n-1)+-1 through two __vsub4) and iq1m_d() (IQ1_M carries its F16 d in the top nibbles of its four u16 scales - there is no d
field).  kQK<T> is 32 for IQ4_NL as well as MXFP4; load_tab uses iq1s_grid_gpu / kvalues_iq4nl; DS4_IQ_SWITCH and
supported() cover the three types, so they leave the CPU lane and join the device path.

tests/dsv41/iq_expert_test.cpp: make_mat now writes IQ1_M's scale where the format keeps it (bytes 49/51/53/55, high nibbles).
Without that the derived d is random and roughly 1/32 of the blocks come out NaN, which std::max then hides.

Parity (z440, one A4000, ds4_iq_test --type 19,20,29): token gate_up 6.70e-08 / 2.04e-08 / 5.97e-08, token down 3.62e-08 /
4.71e-08 / 3.71e-08 against ggml's to_float and a double reference; CPU lane 3.4e-8..8.2e-8; F16 prompt tiles inside their
own tolerances.  Bandwidth: IQ4_NL 376 GB/s (84% of the A4000's 448, bandwidth-bound), IQ1_S 214, IQ1_M 202 (both
compute-bound at 0.92-1.10e12 weight-values/s against IQ2_XXS's 1.22e12).

Not covered: no end-to-end run with a pack that uses these expert types (none on z440); a combined-seed run put IQ1_M's
light-path gate_up at 1.10e-2 against its 1e-2 tolerance, which is the int8 activation rounding of that path (it does not
move with tensor cores off), not the dequant.
2026-10-23 12:34:16 +02:00
Niko1221 45dc5e3400 dsv41: the CUDA build of the tree needs two portable edits (found on z440's sm_86 bring-up)
1. ds4_tq.cu:262 used the two-argument __shfl_xor, which is HIP-only; the rest of the tree already uses
   __shfl_xor_sync(0xffffffffu, v, lane), which both backends have.
2. tests/dsv41/{tq,iq}_expert_test.cpp were forced to LANGUAGE HIP, so a CUDA configure failed at generate time
   ('The language HIP was requested for compilation but was not enabled'). The language now follows the enabled
   backend: HIP, CUDA, else CXX.

Verified on r9700 that the HIP build is untouched: incremental rebuild rc=0, ds4_iq_test --type 17 ALL OK, the engine
--pack-info clean. Verified on z440 (CUDA 12.9, sm_86) by the bring-up agent: HEAD builds, ds4_iq_test ALL OK for
IQ2_XXS/XXS/IQ2_XS/IQ2_S/IQ3_XXS/IQ3_S/IQ4_XS at 5e-8 against ggml, 272-369 GB/s on one A4000. IQ1_M, IQ1_S and
IQ4_NL still have no CUDA kernel (CPU lane only) - that is the Phase 5 kernel list.
2026-10-23 12:08:49 +02:00
Niko1221 c42f4f2969 dsv41 on Windows+CUDA: IQ1_M device kernels (bit-exact vs ggml), IQ4_NL Engram rows, CUDA build fixes (strtok_r, shfl_xor, test language), IST-DAS pack names, hardware bench 2026-10-23 11:57:51 +02:00
Niko1221 734ada7e1b requant_gguf: check the output's free space against the plan before writing (or in --dry-run, return 3)
The release runbook promises the pack/requant builder checks free space before it writes (DEEPSEEK-RELEASE.MD 9.6 item 5);
it did not.  --dry-run now prints 'space: N GB planned, M GB free on <fs>' and returns 3 when 98% of the free space does
not cover the plan, so 'requant_dry && requant' cannot start a run that will fill the disk; a real run raises instead.
With --resume only the bytes still missing are counted against the free space.

Verified on the box with two recipes that do not fit /data today: rq3 (224.3 GB planned against 128.6 GB free) and rq_gu3
(251.2 GB against 128.6 GB) both refuse with the numbers printed, rc 3 in dry-run.  pack_gguf's own output is a few MB
(tensors.txt, engram.bin, tokenizer) and needs no guard.
2026-10-22 22:38:16 +02:00
Niko1221 996b8a4044 dsv41: a type with no device kernel is known, laid out, and kept on the CPU lane (stream H1, second half)
The rule from DEEPSEEK-RELEASE.MD 6.5.2 - every ggml type works, a tensor whose type has no GPU kernel stays on the CPU
lane with one warning instead of stopping the load - is now implemented for IQ1_S (19), IQ1_M (29) and IQ4_NL (20):

- iq::known(t) (a layout exists: block_values/block_bytes/name) is separated from iq::supported(t) (a device kernel
  exists).  type_row_bytes/type_block use known(), so a pack lays out such a tensor instead of refusing it;
  gg::type_on_device(t) is what the VRAM tier tests.
- the CPU lane takes them: gg_cpu_type() covers 10-14, 16-23 and 29 (ggml-cpu's row dots).
- a layer whose experts are such a type is flagged cpu_only at load, printed once ('no GPU kernel for it, its experts
  stay on the CPU lane') and excluded from the VRAM budget in place_experts().
- iq::block_values(IQ4_NL) = 32 (QK4_NL): the only block here that is not 256 values.  Without it the CPU lane read
  IQ4_NL at the wrong row stride (2.4e-02 and 1.4e+05 error against the reference).

Verified: ds4_iq_test --type 29,19,20 runs the CPU lane against ggml's own decode and a double reference - IQ1_M
1.6e-08 / 4.6e-08, IQ1_S 5.4e-08 / 4.0e-08, IQ4_NL 3.1e-08 / 3.2e-08, ALL OK; the device types still pass; --pack-info
reports the layer and role for each such tensor.  A full model with real IQ1_M weights has not been run (none on the
box): the arithmetic of the lane is what is verified.
2026-10-22 20:27:19 +02:00
Niko1221 cf4bef2f78 dsv41: --pack-info prints the DSpark and Engram geometry a pack needs at run time
Today a pack built by another tool had dspark_block_size 0: DSpark ran one token a round and was worth +0.8% instead of
+28-77%.  The report now shows block_size / noise_token_id / markov_rank / the drafter's expert counts, the mtp tensor
count, and the Engram layer ids, with a <- marker when the block size is 0 or there is no drafter.  Verified on h225,
c0q3hQ6_K, c0q2d5 and mxfp4 (all: block 5, noise 128799, rank 256, 1242 mtp tensors, engram layers 1,14).
2026-10-22 19:33:47 +02:00
Niko1221 2f3f363c33 dsv41: --pack-info checks the files the pack points at (a cleaned-up model_dir is a dead pack, not a description)
The pack is pointers: ds4.txt + tensors.txt + the weights where model_dir says.  Today /home/niko/dsv41-c0/t3 was empty
and the c0q3 pack (all IQ3_XXS) described itself perfectly while being unable to load.  --pack-info now stats every file,
compares the length with the largest tensor end, prints MISSING/SHORT lines and exits 2 when any is bad; a healthy pack
prints 'files: N, all present and long enough'.  Verified: h225 10/10, c0q3hQ6_K 19/19, c0q2d5 12/12, c0q3 fails with
six MISSING lines.
2026-10-22 18:31:53 +02:00
Niko1221 31785cce47 pack_gguf: build a pack from a quant that was made by another tool (the IST-DAS sets)
The IST-DAS 2.25bpw GGUF differs from ours in five ways that stopped pack_gguf, each found by building its pack:

- geometry(): ALIASES for the keys they spell differently (kv_source_layer_ids, index_source_layer_ids, the indexer's
  candidate_*, dspark_target_layer_ids, engram_layer_ids).  Without the source ids the engine refused the pack
  ('layer 2: compress ratio 2 without a KV source of that ratio before it').
- --engram-meta FILE: their GGUF carries no Engram hash parameters (no ple.*/engram.layers, no token map, no
  multipliers/primes/offsets - those are tensors of integer type in their file); the parameters come from a GGUF of the
  same model that still has them, the tables from --engram.
- integer tensors (I8/I16/I32/I64/F64) are left out with a line instead of stopping the build (engram_token_map.weight,
  engram_primes.weight, ...).
- --skip-tensors REGEX: their attn_output_a is one interleaved 3-D tensor ([8,1024,4096] ne) that this engine's
  o_groups slices cannot take; our Q5_K requant of the same BF16 weights supplies it through --dense-override, which
  now also *adds* a tensor the quant leaves out (it could only replace before).
- the DSpark geometry (dspark_block_size / n_routed_experts / num_experts_per_tok / target_layer_ids / markov_rank) is
  filled from the drafter's dflash.* keys and the markov tensor's shape, and --dspark-noise-token sets the model's noise
  token (128799).  Without it d_block stayed 0 and DSpark ran one token a round: h225 measured 16.77 tok/s off vs
  25.93 on (3.16 tokens a round) after the fix, 1.01 before.
2026-10-22 18:04:57 +02:00
Niko1221 9ee8b0cf79 dsv41: --pack-info (the pack as a description: type map, bytes an expert/a token, tiers, tensors with no GPU kernel)
strata-dsv41 --pack-info --pack <dir> reads only ds4.txt and tensors.txt: no device, no weights, no lock.  It prints the
layer x role type map with an expert's bytes and a token's MB (top-k picks), the type totals, all-experts GB at the mean
bpw, the dense side split into embed / head / mtp / vision / engram, and every tensor whose type has no device kernel
(IQ1_S, IQ1_M, IQ4_NL today) with the layer and role - the H1 rule's report, and the first line of every benchmark log.
Verified on c0q3 (208.1 GB at 3.06 bpw, 3.251 GB a token - the document's figures) and c0q3d5.
2026-10-22 16:57:11 +02:00
Niko1221 49c157fae3 dsv41: IQ2_XS device kernels (bit-exact vs ggml), Q5_K/Q4_K/Q6_K embedding rows on the host, i-quant expert test, CPU-lane bench reference
- ds4_iq.cu: IQ2_XS (type 17) through the same kernels as IQ2_XXS: Ty<17> (74 B, 1024-word grid),
  decode<17> and deq8<17>, block_bytes/name/dispatch.  ds4_iq_test compares the token path, the window
  path, the prompt sets and the CPU lane against ggml's own to_float + a double reference: worst
  4.6e-08 (token gate_up), 4.6e-08 (down), 8.9e-03 (F16 tiles), CPU lane 3.5e-08.  IQ2_XXS / IQ2_S /
  IQ3_XXS / IQ3_S / IQ4_XS pass the same test unchanged.
- ds4_model.cu: gg_type_of takes 17; embed.weight may be any ggml block type (Q5_K / Q4_K / Q6_K ...)
  through gg_row_to_bf16 (ggml's to_float, the same BF16 rounding as the Q8_0 path) - the IST-DAS sets'
  Q5_K token_embd needs no dense override now.
- gg_dot_bench: the A/B's pass criterion was |x-y|/|x| on single outputs, which fails on cancellation
  while the two paths agree to a few ulps; it now also compares 64 sampled rows against a double
  reference scaled by the L1 norm (worst 1.1e-08) and requires |x-y| <= 1e-6 of the output scale.
  A/B at 32 threads: single token x1.03 (IQ2_XXS) / x1.13 (IQ2_S); 4 rows x2.4 / x2.3.
- CMake: ds4_iq_test target; ds4_tq_test and ds4_tq_test_portable link ds4_gg_cpu.cpp (the ggml CPU lane
  ds4_iq_cpu.cpp refers to since 49e82263a, missing from those targets).
2026-10-22 16:53:39 +02:00
Niko1221 20fed551ba dsv41: Strata AVX2 i-quant rows opt-in (DS4_GG_IQ256=1) until the engine A/B 2026-10-22 16:21:25 +02:00
Niko1221 49e82263ad dsv41: CPU lane takes Strata's AVX2 i-quant rows (switchable), ds4_gg_bench, pack_gguf repeatable --dense-override 2026-10-22 10:31:03 +02:00
Niko1221 736d7a840c dsv41: pack_gguf --override-experts takes several folders with roles 2026-10-22 10:16:05 +02:00
Niko1221 5e52cf569f dsv41: pack_gguf --override-experts (mixed-bit packs from two requantized sets); stageB tooling 2026-10-22 07:03:10 +02:00
Niko1221 5278ba0043 dsv41: dense K-quant GEMV (Q5_K/Q6_K) + dense_requant, ggml-type CPU lane, imatrix builder, requant fixes (C0 experiments) 2026-10-22 04:03:32 +02:00
Niko1221 54f81315bf dsv41: DS4_TRACE routing trace (env), routestats / budget model scripts live in phaseA-tests 2026-10-21 14:12:19 +02:00
Niko1221 2355265fdc dsv41: pack_gguf --tq-layers (a subset of the converted layers) 2026-10-21 08:24:45 +02:00
Niko1221 1c2388876c dsv41: trellis gate/up fused in one pass sharing the activation fragments (token path 454 -> ~355 us a layer, outputs unchanged: ds4_tq_test ALL OK); optional 32-bit stream loads (no gain) 2026-10-21 01:30:01 +02:00
Niko1221 e8cb1cba7f dsv41: tq_convert resumes (skips finished layers), pack_gguf --tq-experts takes several folders 2026-10-21 00:26:31 +02:00
Niko1221 da70196350 dsv41: tq_convert --act (input-column weighted encode from the gate-3 calibration sums) 2026-10-20 23:24:48 +02:00
Niko1221 88fcf2b443 dsv41: engine-kernel bench in ds4_tq_test (--bench) 2026-10-20 23:21:41 +02:00
Niko1221 12a3c0739e dsv41: portable (FMA) tile test target, docs for the trellis experts 2026-10-20 22:31:25 +02:00
Niko1221 7df82763e3 dsv41: tq_convert.py (re-encode a pack's MXFP4 experts with the trellis encoder into the engine's file layout) and pack_gguf --tq-experts 2026-10-20 21:52:59 +02:00
Niko1221 e5b4021c6f dsv41: trellis routed experts end to end on the device and the CPU lane (TQK6/7, TQK5X/4X computed, TQK5T/4T table): ds4_tq.cu (gfx12 WMMA + portable FMA tiles over the file layout), ds4_tq_cpu.cpp (Strata tq_cpu kernels), iq entry points route the types; test ds4_tq_test (all six types pass: device and CPU lane within 1e-6 of a double reference) 2026-10-20 21:49:29 +02:00
Niko1221 d6a6c1c797 Merge f-dsv41-enc (trellis encoder, K4/K5 computed-code types 224/225, table versions 226/227) into f-dsv41-port; conflicting trellis files taken from f-dsv41-enc 2026-10-20 21:23:42 +02:00
Niko1221 a5f54c9796 dsv41: test includes point at src/dsv41 2026-10-20 21:02:33 +02:00
Niko1221 e38025769d dsv41: picture test (mkimg.py) and serve_prompts GENI / text output 2026-10-20 20:40:41 +02:00
Niko1221 3d8d1079bc dsv41: gfx12 WMMA trellis GEMV (ds4_tqw.cuh, from the CUDA lab's recipe) with a parity + bench test: all K4-K7, widths 1/3/5, table and computed codes within 8e-8 of the double reference on the R9700; ~1.07 Tw/s 2026-10-20 20:09:32 +02:00
Niko1221 6c8247af60 Merge f-dsv41-serve into f-dsv41-port 2026-10-20 20:03:45 +02:00
Niko1221 2713121725 trellis: computed codebook on the GPU encoder, README for the K5/K4 versions 2026-10-20 19:59:48 +02:00
Niko1221 c11e66aa8b tqe_table takes a type 2026-10-20 19:37:54 +02:00
Niko1221 9fcc101844 docs: DEEPSEEK41.md (where the model goes, install, the API) 2026-10-20 19:26:21 +02:00
Niko1221 a7c50df002 K5/K4 version 2: computed (LUT-free) codebook for TQK5X/TQK4X, point-table versions kept as TQK5T/TQK4T; CPU decoder tables per type; tests 2026-10-20 19:25:16 +02:00
Niko1221 a4c9336416 serve: engine_ready_s in the run config (DeepSeek-V4.1-Flash reads 300 GB at start; setup writes 3600) 2026-10-20 19:23:44 +02:00
Niko1221 2189884a59 dsv41: DS4_PROF events per device (invalid resource handle on several GPUs), exl3_legacy.cu in CUDA builds (reported by the CUDA lab) 2026-10-20 19:22:51 +02:00
Niko1221 f7c645c065 DeepSeek-V4.1-Flash in the catalog, setup and strata launch
- setup.py: the nine pinned shards of unsloth/DeepSeek-V4.1-Flash-1017-GGUF UD-Q8_K_XL (revision f924b94f, sizes and SHA-256 from the reference run; the DSpark drafter and picture reader pinned but not fetched), MULTI_FILE_FAMILIES, install_dsv41: plain warnings for the 508 GB download, RAM and disk (a refusal only where the disk cannot hold the files), the strata-dsv41 check before any download, --gguf-dir for files you already have, pack_gguf.py, config with --pack --max-context 8192 --eos 1 --tp 2 and the cards; menu entry and a --check line.
- strata_control/dsv41_fit.py: where the model goes on the cards and in RAM, from the measured run (6,240 experts in graphics memory predicted on 4 x 32 GB, 6,197 measured), the n-gram table's SSD note.
- tools/strata_mcp.py and strata_cli/launch: the model is in the list; strata launch --plan prints the placement (layer blocks per card, experts on the cards and on the CPU, RAM, disk, the SSD note); --model-path takes the nine shards where they are; parallel, pictures and tuning are said, not passed to an engine that does not take them.
- tests: tools/test_setup_dsv41.py (22), tests/terminal/test_launch_dsv41.py (13).
2026-10-20 19:22:11 +02:00
Niko1221 4455c8bd1d tqenc.py: GPU lock wait with CPU fallback 2026-10-20 19:18:57 +02:00
Niko1221 d9220bd1a5 Merge f-dsv41-cudalab (trellis GEMV lab, tqenc) into f-dsv41-port 2026-10-20 18:54:44 +02:00
Niko1221 d251b2c0db dsv41 cudalab: gfx12 WMMA port recipe of the trellis GEMV: host emulation of the data flow (matches the reference), HIP kernel source, layout probe 2026-10-20 18:45:22 +02:00
Niko1221 5a52007005 serve: DeepSeek-V4.1 through the server (deepseek-v3 pre-tokenizer, DSML tool calls, effort words, end token, card sampling)
- tools/strata_tokenizer.py: the deepseek-v3 pre-tokenizer (llama.cpp's DEEPSEEK3_LLM list as a sequence of splits) chosen by the pack's tokenizer.json 'pre'; special tokens decode to their literal text (the DSML token is not in the byte alphabet). 20 of 20 reference prompts tokenize to the ids llama.cpp made.
- serve/dsml.py: DSML calls (the GGUF template's spelling) translated as they stream into the form OutputParser reads, so tool streaming, tool_choice and calls in the thinking work as for the other families; string=true values stay strings.
- serve/server.py: Service.set_family('deepseek41'): end token from the pack, DsmlOutputParser, forced-call opening in DSML, effort words low/medium/high/max/1..100, model name and card sampling.
- serve/test_dsv41.py (39 tests, fake engine) and fixtures from the GGUF's own template and the llama.cpp reference prompts.
2026-10-20 18:45:21 +02:00
Niko1221 6f703a6dbe dsv41 cudalab: computed (LUT-free) codebooks for the 15-bit trellis (1mad, 3inst, 3sum, 3single), encoder hook TQE_CODE, LUT-free GEMV kernel, code SQNR tool, pilot scripts from f-dsv41-enc 2026-10-20 18:41:32 +02:00
Niko1221 befdd9d3e1 tqenc.py: optional per-call GPU locks (TQENC_GPU_LOCK) 2026-10-20 18:30:12 +02:00
Niko1221andmw00 2d718defc0 dsv41: pack_gguf takes unsloth's DSpark drafter GGUF (dflash) and the mmproj vision GGUF into the pack; the vision loader reads F32 rows and gate/up halves
Co-authored-by: mw00 <[email protected]>
2026-10-20 18:28:30 +02:00
Niko1221 9f78a77e01 Merge f-022-int into f-dsv41-port 2026-10-20 18:22:49 +02:00
Niko1221 01389205e9 dsv41: split-KV attention opt-in (DS4_ATTN_SPLIT=1): A/B on 4x R9700 shows no decode gain, KL unchanged 2026-10-20 18:22:02 +02:00
Niko1221 a63b2d4dae dsv41: --score segment file really carries end_line 2026-10-20 18:16:03 +02:00
Niko1221 8e13982710 build_swap: ldl damping 0.3 2026-10-20 17:58:45 +02:00
Niko1221andmw00 ff5577ad70 dsv41: split-KV sparse attention from the CUDA lab for decode / short verify windows (packed rows, T<=5), DS4_ATTN_SPLIT=0 off
Co-authored-by: mw00 <[email protected]>
2026-10-20 17:54:04 +02:00
Niko1221 d17c32d6a3 dsv41: --score writes end_line in the segment file (quant_quality.py compare reads it) 2026-10-20 17:51:12 +02:00
Niko1221 1c20a59675 dsv41 cudalab: split-KV sparse attention (2.9x at T=1), CPU-kernel cross-check of the trellis GEMV, build for the repo CPU kernels 2026-10-20 17:47:29 +02:00
Niko1221 cb725bb209 dsv41 cudalab: DeepSeek kernel parity + speed harness (mHC, router, compressor, indexer, top-k, sparse attention, Engram) over src/dsv41/ds4_kernels.cu 2026-10-20 17:39:16 +02:00
Niko1221 770292e41c dsv41 cudalab: tensor-core trellis GEMV (fp16 point table, TQL coalesced layout, prepared B fragments), K4-K7, widths 1-5 2026-10-20 17:34:48 +02:00
Niko1221 090121c31b dsv41 cudalab: trellis K4-K7 dequant + GEMV v1 on sm_86 with parity and speed harness (tqenc and the K5/K4 CPU decoder taken from f-dsv41-enc) 2026-10-20 17:20:22 +02:00
Niko1221 6b5ad4f27d trellis: proxy_ldl 2026-10-20 17:11:51 +02:00
Niko1221 c3e9a998ad dsv41: tools to turn the llama.cpp reference's 10 greedy prompts into ids and compare the engine's greedy output (serve protocol) 2026-10-20 16:50:46 +02:00
Niko1221andmw00 a0dbad8078 dsv41: pack_gguf uses the GGUF's own chat template (fixes Maya #299), drop Maya's template and the EXL3 packer
Maya's tools/ds4/chat_template.jinja lost tool associations and later system instructions; the GGUF's embedded
template matched DeepSeek's encoding.py byte for byte on 9 cases (notes-dsv41-ref.md).

Co-authored-by: mw00 <[email protected]>
2026-10-20 16:36:35 +02:00
453cc0d56f dsv41: build the imported DeepSeek-V4.1 engine path in Strata (HIP gfx1201), add --score
CMake option STRATA_BUILD_DSV41 (default OFF, adds the target strata-dsv41 and a private static kernel lib; no
existing target changes). The vendored exl3 / sampler / window-grouping kernels live in namespace strata::dsv41k so
no symbol is shared with strata_kernels. Missing CUDA names for HIP come from a force-included private header
(include/strata/dsv41/hip_extra.h), the IQ CPU lane stays the stub branch, MAYA_* env aliases are gone.
New --score IDS.json --score-out RUN.tsv: teacher-forced top-64 log-probabilities in tools/quant_quality.py's
row format (the gate against the llama.cpp reference). MIT notice added to every ported file.

Co-authored-by: mw00 <[email protected]>
Co-authored-by: needmorevram <[email protected]>
2026-10-20 16:34:12 +02:00
Niko1221 b73bd26a5c trellis: run_parts (engine runs that survive a failed segment) 2026-10-20 16:26:00 +02:00
mw00andneedmorevram bbd0a2c673 dsv41: import Project Maya's DeepSeek-V4.1 engine unmodified (v1.1.17, 8d9a9b5d)
Verbatim copy of mw00/project-maya src/ds4, tests/ds4_*, tools/ds4 and the exl3 / sampler
kernel sources it needs, MIT, with their LICENSE and a NOTICE. Not built yet; the next
commits adapt it to Strata's tree.

Co-authored-by: needmorevram <[email protected]>
2026-10-20 16:19:45 +02:00
Niko1221 a7eb99bba9 trellis: block-feedback (BlockLDLQ) encoder and the ldl mode of build_swap 2026-10-20 15:59:36 +02:00
Niko1221 431803abb9 STRATA_ACT_DUMP: rarer flush with the covariance on; merge_dumps keeps it 2026-10-20 15:57:45 +02:00
Niko1221 adf7ca9729 trellis: swap comparison and greedy sanity scripts 2026-10-20 15:34:41 +02:00
Niko1221 0b4e603cfd STRATA_ACT_DUMP: keep the accumulator alive for the exit flush 2026-10-20 14:36:38 +02:00
Niko1221 2ec3d2e381 trellis: corpus builder, original-weight fetch, swap builder for Gyro-S layer variants 2026-10-20 14:36:38 +02:00
Niko1221 a775b04695 native experts: STRATA_ACT_DUMP, per-layer second moments of the CPU expert inputs (research, env-gated, off by default) 2026-10-20 14:36:38 +02:00
Niko1221 6f99190f82 trellis: README, column-profile check of the Gyro error 2026-10-20 14:36:38 +02:00
Niko1221 4292678b8f trellis: GPU path in tqenc.py and the pilot, diagonal activation second moments (importance-weighted encode), error profile script 2026-10-20 14:36:38 +02:00
Niko1221 68a8cad3ed trellis HIP: register pressure tweaks, CU mode 2026-10-20 14:36:38 +02:00
Niko1221 c23936be78 trellis: HIP encoder (gfx1201), bit-identical to the CPU encoder; host helpers; pilot driver reads remote GGUF shards 2026-10-20 14:36:38 +02:00
Niko1221 7e9531b3da trellis: python wrapper, Hadamard helpers, .tqx container, Gyro re-encode and original-weight comparison scripts, pilot driver 2026-10-20 14:36:38 +02:00
Niko1221 512e298e6e trellis encoder: tail-biting Viterbi for TQ2_T/TQK6/TQK7 plus experimental TQK5X/TQK4X (CPU decoder reads them)
tools/trellis/tqenc.{hpp,cpp}: shift-trellis Viterbi (group-min ACS), tail-biting by a rotated free pass or the exact
search over all start groups, fp16 scale refinement, importance weights.  tqenc_test.cpp checks the streams, the
Viterbi against a naive DP, planted solutions, bit-exact decode against tq_states.hpp and the CPU kernels.
The CPU decoder gets K = 5 and 4 (ids 224, 225, experimental v1); existing types and layouts are unchanged.
2026-10-20 14:36:38 +02:00
408 changed files with 67602 additions and 10814 deletions

No files matched your search

+1 -1
View File
@@ -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.3 · engine 0.1.48"
placeholder: "Strata 0.2.6 · engine 0.1.51"
validations:
required: true
- type: input
+29 -13
View File
@@ -1,25 +1,40 @@
# 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.3, engine 0.1.48. It has three parts:
Version 0.2.6, engine 0.1.51. 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.6-windows-x64-setup.exe`, `Strata-0.2.6-linux-x86_64.AppImage`, `Strata-0.2.6-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.
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh # Linux, Mac
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh # Linux, Mac
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex # Windows PowerShell
```
@@ -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
+145 -3
View File
@@ -1,4 +1,4 @@
# Strata Engine - top-level build (P1.S1).
# Strata Engine - top-level build (P1.S1).
#
# Targets (phase-1 §P1.S1): `strata_artifact` (no CUDA dependency), `tests`. `strata_plan` and the C++
# `strata-pack` are added when their sources exist - a target with no sources would make the build graph
@@ -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.48 LANGUAGES CXX)
project(strata VERSION 0.1.51 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)
@@ -794,6 +794,114 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
target_compile_definitions(strata PRIVATE STRATA_HAVE_QWEN35=1)
endif()
# DeepSeek-V4.1-Flash: the third engine path (src/dsv41, from Project Maya's maya-ds4, MIT, third_party/project-maya).
# Its own executable for now (strata-dsv41); a family switch in `strata` comes after first light. OFF by default: it
# changes no existing target (the vendored kernels live in strata::dsv41k, so no symbol is shared with strata_kernels).
option(STRATA_BUILD_DSV41 "Build the DeepSeek-V4.1 engine path (strata-dsv41)" OFF)
if(STRATA_BUILD_DSV41 AND NOT STRATA_HIP_GFX906)
add_library(strata_dsv41_kernels STATIC src/dsv41/vendor/exl3.cu src/dsv41/vendor/sampler.cu src/dsv41/vendor/moe_win.cu
src/dsv41/vendor/exl3_cpu.cpp)
if(NOT STRATA_ENABLE_HIP) # (the pre-sm_80 NVIDIA decode kernels exl3.cu dispatches to)
target_sources(strata_dsv41_kernels PRIVATE src/dsv41/vendor/exl3_legacy.cu)
endif()
target_include_directories(strata_dsv41_kernels PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ${_strata_gpu_include_directories})
target_link_libraries(strata_dsv41_kernels PUBLIC ${_strata_gpu_runtime_target})
add_executable(strata-dsv41 src/dsv41/ds4_main.cpp src/dsv41/ds4_model.cu src/dsv41/ds4_kernels.cu src/dsv41/ds4_vision.cu
src/dsv41/ds4_gguf.cu src/dsv41/ds4_gguf_cpu.cpp src/dsv41/ds4_iq.cu src/dsv41/ds4_iq_cpu.cpp
src/dsv41/ds4_tq.cu src/dsv41/ds4_tq_cpu.cpp src/dsv41/ds4_gg_cpu.cpp src/dsv41/ds4_packinfo.cpp)
# the commit the binary was built from, readable with --version (set -DSTRATA_BUILD_ID=... for a tree without .git)
if(NOT STRATA_BUILD_ID AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/BUILD_ID) # (a git archive has no .git: the packer writes the commit here)
file(STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/BUILD_ID STRATA_BUILD_ID LIMIT_COUNT 1)
endif()
if(NOT STRATA_BUILD_ID)
execute_process(COMMAND git -C ${CMAKE_CURRENT_SOURCE_DIR} rev-parse --short=9 HEAD OUTPUT_VARIABLE STRATA_BUILD_ID
OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
endif()
if(NOT STRATA_BUILD_ID)
set(STRATA_BUILD_ID unknown)
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})
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
# carve-out is not all the GPU may use, and the engine sizes its expert tiers by what the budget says is free
target_link_libraries(strata-dsv41 PRIVATE strata_core)
endif()
if(STRATA_NATIVE_EXPERTS) # Strata's CPU trellis kernels (tq_cpu.hpp) for the trellis experts on the CPU lane
target_link_libraries(strata-dsv41 PRIVATE strata_kernels_cpu)
target_compile_definitions(strata-dsv41 PRIVATE STRATA_DSV41_TQ_CPU=1)
endif()
if(STRATA_NATIVE_EXPERTS)
# the CPU lane's ggml-type rows: ggml-cpu's dots against Strata's AVX2 i-quant rows (bits and GB/s); no GPU, no model
add_executable(ds4_gg_bench tests/dsv41/gg_dot_bench.cpp src/dsv41/ds4_gg_cpu.cpp)
target_include_directories(ds4_gg_bench PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/src/dsv41
${CMAKE_CURRENT_SOURCE_DIR}/third_party/ggml)
target_link_libraries(ds4_gg_bench PRIVATE strata_kernels_cpu Threads::Threads)
target_compile_definitions(ds4_gg_bench PRIVATE STRATA_DSV41_TQ_CPU=1)
endif()
if(STRATA_NATIVE_EXPERTS)
# the trellis experts (device kernels ds4_tq.cu, CPU lane ds4_tq_cpu.cpp) against a double reference; needs a GPU, no model
# both test .cpp files launch kernels, so they compile as HIP on the AMD backend and as CUDA on the NVIDIA one
if(STRATA_ENABLE_HIP)
set(_ds4_test_lang HIP)
elseif(STRATA_ENABLE_CUDA)
set(_ds4_test_lang CUDA)
else()
set(_ds4_test_lang CXX)
endif()
set_source_files_properties(tests/dsv41/tq_expert_test.cpp PROPERTIES LANGUAGE ${_ds4_test_lang})
add_executable(ds4_tq_test tests/dsv41/tq_expert_test.cpp src/dsv41/ds4_tq.cu src/dsv41/ds4_iq.cu src/dsv41/ds4_tq_cpu.cpp
src/dsv41/ds4_iq_cpu.cpp src/dsv41/ds4_gg_cpu.cpp tools/trellis/tqenc.cpp)
target_include_directories(ds4_tq_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${_strata_gpu_include_directories}
${CMAKE_CURRENT_SOURCE_DIR}/third_party/ggml ${CMAKE_CURRENT_SOURCE_DIR}/src/kernels/cpu)
target_link_libraries(ds4_tq_test PRIVATE strata_kernels_cpu ${_strata_gpu_runtime_target} Threads::Threads)
target_compile_definitions(ds4_tq_test PRIVATE STRATA_DSV41_TQ_CPU=1)
# the same with the plain-FMA tile instead of the gfx12 WMMA one (what CUDA and gfx11 builds run)
add_executable(ds4_tq_test_portable tests/dsv41/tq_expert_test.cpp src/dsv41/ds4_tq.cu src/dsv41/ds4_iq.cu src/dsv41/ds4_tq_cpu.cpp
src/dsv41/ds4_iq_cpu.cpp src/dsv41/ds4_gg_cpu.cpp tools/trellis/tqenc.cpp)
target_include_directories(ds4_tq_test_portable PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${_strata_gpu_include_directories}
${CMAKE_CURRENT_SOURCE_DIR}/third_party/ggml ${CMAKE_CURRENT_SOURCE_DIR}/src/kernels/cpu)
target_link_libraries(ds4_tq_test_portable PRIVATE strata_kernels_cpu ${_strata_gpu_runtime_target} Threads::Threads)
target_compile_definitions(ds4_tq_test_portable PRIVATE STRATA_DSV41_TQ_CPU=1 DS4_TQ_PORTABLE=1)
if(STRATA_ENABLE_HIP)
target_compile_options(ds4_tq_test_portable PRIVATE "$<$<COMPILE_LANGUAGE:HIP,CXX>:SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/include/strata/dsv41/hip_extra.h>")
endif()
if(STRATA_ENABLE_HIP)
target_compile_options(ds4_tq_test PRIVATE "$<$<COMPILE_LANGUAGE:HIP,CXX>:SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/include/strata/dsv41/hip_extra.h>")
endif()
# the i-quant experts (ds4_iq.cu) against ggml's own decode of the same blocks + a double reference, device and CPU
# lane, for every type the device takes: the gate a new type (IQ2_XS ..) has to pass before the engine uses it
set_source_files_properties(tests/dsv41/iq_expert_test.cpp PROPERTIES LANGUAGE ${_ds4_test_lang})
add_executable(ds4_iq_test tests/dsv41/iq_expert_test.cpp src/dsv41/ds4_iq.cu src/dsv41/ds4_iq_cpu.cpp
src/dsv41/ds4_gg_cpu.cpp src/dsv41/ds4_tq.cu src/dsv41/ds4_tq_cpu.cpp)
target_include_directories(ds4_iq_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${_strata_gpu_include_directories}
${CMAKE_CURRENT_SOURCE_DIR}/third_party/ggml ${CMAKE_CURRENT_SOURCE_DIR}/src/kernels/cpu)
target_link_libraries(ds4_iq_test PRIVATE strata_kernels_cpu ${_strata_gpu_runtime_target} Threads::Threads)
target_compile_definitions(ds4_iq_test PRIVATE STRATA_DSV41_TQ_CPU=1)
if(STRATA_ENABLE_HIP)
target_compile_options(ds4_iq_test PRIVATE "$<$<COMPILE_LANGUAGE:HIP,CXX>:SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/include/strata/dsv41/hip_extra.h>")
endif()
# the kept conversations' .meta file and prefix matching (ds4_slotmeta.hpp): no GPU, no model
add_executable(ds4_slot_test tests/dsv41/slot_test.cpp)
target_include_directories(ds4_slot_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/dsv41)
target_compile_features(ds4_slot_test PRIVATE cxx_std_17)
add_executable(ds4_experts_rule_test tests/dsv41/experts_rule_test.cpp)
target_include_directories(ds4_experts_rule_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/dsv41)
target_compile_features(ds4_experts_rule_test PRIVATE cxx_std_17)
add_executable(ds4_pick_rule_test tests/dsv41/pick_rule_test.cpp)
target_include_directories(ds4_pick_rule_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/dsv41)
target_compile_features(ds4_pick_rule_test PRIVATE cxx_std_17)
endif()
if(STRATA_ENABLE_HIP)
foreach(_t strata_dsv41_kernels strata-dsv41)
target_compile_options(${_t} PRIVATE "$<$<COMPILE_LANGUAGE:HIP,CXX>:SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/include/strata/dsv41/hip_extra.h>")
endforeach()
endif()
endif()
if(STRATA_ENABLE_HIP)
enable_testing()
set(_strata_hip_tests)
@@ -1436,6 +1544,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(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)
add_executable(uma_cache_test src/core/uma_cache_test.cpp)
target_include_directories(uma_cache_test PRIVATE include)
add_test(NAME uma_cache_test COMMAND uma_cache_test)
@@ -1565,6 +1676,13 @@ if(STRATA_NATIVE_EXPERTS)
foreach(_f GGML_AVX GGML_AVX2 GGML_FMA GGML_F16C GGML_BMI2)
set(${_f} ON CACHE BOOL "" FORCE)
endforeach()
# The DeepSeek engine (STRATA_BUILD_DSV41) builds ggml-cpu with BMI2 OFF: ggml's IQ1_S / IQ1_M dots use _pdep_u64 with it, and AMD
# Zen 1/2 run PDEP in microcode (EPYC 7452, IQ1_M one-token dot 1.8 GB/s against 32 GB/s without it; IQ1_S 2.3 against 37). The
# results are bit-identical either way (ds4_gg_bench --dump, types 19 and 29, 1-16 tokens). Other engines keep BMI2 until their
# next rebuild is looked at: no Qwen package changes with this commit.
if(STRATA_BUILD_DSV41)
set(GGML_BMI2 OFF CACHE BOOL "" FORCE)
endif()
elseif(STRATA_ISA_FLOOR)
# the experimental older-CPU build (STRATA_ISA_FLOOR above): one ggml-cpu for the floor, ggml's "sandybridge"
# feature set (SSE4.2 + AVX) or SSE4.2 alone - not the build host's, which may be newer than the PC it runs on.
@@ -1623,7 +1741,7 @@ if(STRATA_NATIVE_EXPERTS)
set_source_files_properties(src/kernels/cpu/iq_avx1.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX")
else()
set_source_files_properties(src/kernels/cpu/iq_avx512.cpp src/kernels/cpu/nvfp4_avx512.cpp PROPERTIES COMPILE_OPTIONS
"-mavx512f;-mavx512bw;-mavx512vl;-mavx512dq;-mavx512vnni;-mavx512vbmi;-mf16c;-mfma")
"-mavx512f;-mavx512bw;-mavx512vl;-mavx512dq;-mavx512vnni;-mavx512vbmi;-mavx512vpopcntdq;-mbmi2;-mf16c;-mfma")
set_source_files_properties(src/kernels/cpu/iq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
set_source_files_properties(src/kernels/cpu/kq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
set_source_files_properties(src/kernels/cpu/nvfp4_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
@@ -1671,6 +1789,14 @@ if(STRATA_NATIVE_EXPERTS)
# against ggml-cpu's dot, on random blocks; --bench: ms per expert on one thread, the weights streamed from DRAM
add_executable(iq_avx2_parity src/kernels/cpu/iq_avx2_parity.cpp)
target_link_libraries(iq_avx2_parity PRIVATE strata_kernels_cpu)
# the 512-bit i-quant rows against ggml-cpu's dot: the exact rows bit for bit at 1-4 tokens, the engine's multi-token rows to float
# rounding, with the grid lookups as scalar loads and as gathers (SKIP without AVX-512)
add_executable(iq512_gather_parity src/kernels/cpu/iq512_gather_parity.cpp)
target_link_libraries(iq512_gather_parity PRIVATE strata_kernels_cpu)
if(STRATA_BUILD_TESTS)
add_test(NAME iq512_gather_parity_scalar COMMAND ${CMAKE_COMMAND} -E env STRATA_IQ_GATHER=0 $<TARGET_FILE:iq512_gather_parity>)
add_test(NAME iq512_gather_parity_gather COMMAND ${CMAKE_COMMAND} -E env STRATA_IQ_GATHER=1 $<TARGET_FILE:iq512_gather_parity>)
endif()
# MODEL-UPDATE: the pool's expert rows per format on T threads against the RAM bandwidth (a benchmark, no test)
add_executable(kq_cpu_bench src/kernels/cpu/kq_cpu_bench.cpp)
target_link_libraries(kq_cpu_bench PRIVATE strata_kernels_cpu)
@@ -2265,3 +2391,19 @@ if(STRATA_HIP_GFX906)
add_executable(gfx906_qsa_query_swizzle_probe bench/gfx906_qsa_query_swizzle_probe.cpp)
target_link_libraries(gfx906_qsa_query_swizzle_probe PRIVATE strata_kernels ${_strata_gpu_runtime_target})
endif()
# ---- DeepSeek-V4.1 on a box with no GPU toolkit ------------------------------------------------
# The whole DSV41 block above lives inside if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP), so with neither backend on
# -DSTRATA_BUILD_DSV41=ON was silently unused and the host-side benchmark could not be built at all (found on p1004,
# 2026-10-23: the CPU lane is exactly what a box without a usable GPU needs to measure). This branch builds the one target
# that has no device code, so the CPU-lane A/B runs anywhere.
if(STRATA_BUILD_DSV41 AND NOT STRATA_ENABLE_CUDA AND NOT STRATA_ENABLE_HIP AND NOT STRATA_HIP_GFX906)
message(STATUS "STRATA_BUILD_DSV41: no GPU backend, building the host-side benchmark only (ds4_gg_bench)")
if(STRATA_NATIVE_EXPERTS)
add_executable(ds4_gg_bench tests/dsv41/gg_dot_bench.cpp src/dsv41/ds4_gg_cpu.cpp)
target_include_directories(ds4_gg_bench PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/src/dsv41
${CMAKE_CURRENT_SOURCE_DIR}/third_party/ggml)
target_link_libraries(ds4_gg_bench PRIVATE strata_kernels_cpu Threads::Threads)
target_compile_definitions(ds4_gg_bench PRIVATE STRATA_DSV41_TQ_CPU=1)
endif()
endif()
+121 -337
View File
@@ -1,404 +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"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></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="#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/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>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">
<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://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>
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. Es chattet, schreibt Code, liest
Bilder und arbeitet mit deinen Apps und Coding-Agents zusammen. Deine Chats und Dateien bleiben auf deinem PC.
<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></sub></p>
**Neu hier?** Sieh dir an, [was du brauchst](#was-du-brauchst), und [installiere dann die App](#option-a-die-strata-app).
**Kostenlos, Open Source und ganz in deiner Hand.** Nutze Strata auf dem Desktop oder mit deinen liebsten Coding-Tools. Kein Konto nötig.
- **Läuft auf einem Gaming-PC.** 32 GB Arbeitsspeicher (RAM) und Grafikspeicher zusammen reichen für den Anfang.
- **Passt zu deinen Werkzeugen.** Strata spricht die OpenAI- und die Anthropic-API, deshalb verbinden sich Claude Code, Codex CLI, OpenCode und andere Agents damit.
- **Liest Bilder.** Das ist standardmäßig eingeschaltet.
- **Kostenlos.** MIT-Lizenz, kein Konto, keine Cloud.
- **Privat.** Keine Telemetrie. Was Strata herunterlädt, wann es das Internet nutzt und wie es Downloads prüft, steht in [docs/SAFETY.md](docs/SAFETY.md).
## Modelle
Version 0.2.3 · Engine 0.1.48
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"> &nbsp;<b>Qwen3.8-Flash-Next</b></summary>
**Du brauchst mindestens 32 GB Arbeitsspeicher (RAM) und Grafikspeicher (VRAM) zusammen.** Bei einem Mac oder einem Ryzen AI Max
ist das der gemeinsame Speicher. Mit mehr Speicher läuft ein größeres oder ein schnelleres Modell.
- Ein PC mit 32 GB und einer Grafikkarte mit 12-16 GB führt den Coder aus.
- Mit 64 GB RAM laufen alle Größen.
| Dein Computer | Details |
| Anbieter | Auswahl |
| --- | --- |
| **NVIDIA** GeForce RTX 20, 30, 40, 50 Serie (12 GB oder mehr) | Installiere die App oder das Terminal. |
| **AMD** Radeon RX 6800 und neuer, Radeon AI PRO R9700 (12 GB oder mehr) | [AMD-Karten](docs/AMD_HIP.md) |
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [Intel-Engine](docs/INTEL.md) |
| **Apple-Silicon-Mac** (M1 oder neuer, macOS 15 oder neuer) | [Mac](docs/MAC.md) |
| **AMD Ryzen AI Max (Strix Halo)** und andere Systeme mit gemeinsamem Speicher | [Strix Halo](docs/STRIX_HALO.md) |
| Ältere Karten und Prozessoren | [Ältere GPUs](docs/OLDER_GPUS.md), [Ältere CPUs](docs/INSTALL.md#older-cpus-experimental) |
| **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 nötig:
[Größen, Speicherbedarf und Kompatibilität vergleichen](docs/MODELS.md).
- etwa 80 GB freier Speicherplatz (mit einer SSD startet es beim ersten Mal viel schneller)
- Windows 10 / 11, Linux oder macOS
- ein aktueller Grafiktreiber
</details>
Zwei oder drei Karten desselben Herstellers können sich ein Modell teilen ([Multi-GPU](docs/MULTI_GPU.md)). Strata teilt ein
Modell nie auf NVIDIA- und AMD-Karten auf. Die vollständige Liste: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<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"> &nbsp;<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
Such dir eine Option aus. Alle starten dieselbe Engine und nutzen dieselben Modelle.
<a id="option-a-the-strata-app"></a>
### Option A: die Strata-App
### Desktop-App
Der einfachste Weg. Die App bringt die Engine mit: installieren, ein Modell wählen, chatten. Lade die Datei für dein
System und deine Grafikkarte von der [Releases-Seite](https://github.com/Niko1221/Strata/releases/latest) herunter. Du bist
unsicher, welche du brauchst? Lies den Absatz unter der Tabelle.
**[Strata für Windows, Linux oder Mac herunterladen →](https://github.com/Niko1221/Strata/releases/latest)**
| System | Deine Grafikkarte | Datei zum Herunterladen |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX, Treiber 580 oder neuer | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
| | NVIDIA, ältere Karte oder Treiber älter als 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon RX 9000 Serie oder Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon RX 7000 Serie oder Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX, Treiber 580 oder neuer | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
| | NVIDIA, ältere Karte oder Treiber älter als 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon RX 9000 Serie oder Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon RX 7000 Serie | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
| **Mac** (Apple Silicon, macOS 15 oder neuer) | eingebaut | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
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).
**Wenn du unsicher bist:** Bei NVIDIA nimmst du `cuda13`, wenn dein Treiber Version 580 oder neuer ist, sonst `cuda12`.
`cuda12` ist für ältere Karten und ältere Treiber. Die Dateien `hip` und `amd` sind für AMD-Karten. Für andere AMD-Karten
(RX 6000) gibt es noch keine fertige App: siehe [AMD-Karten](docs/AMD_HIP.md).
<sub>Dateien: `Strata-0.2.6-windows-x64-setup.exe` · `Strata-0.2.6-linux-x86_64.AppImage` · `Strata-0.2.6-macos-arm64.dmg`</sub>
Danach:
<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>
- **Windows:** Starte die `.exe`. Sie wird für deinen Benutzer installiert und braucht keine Administratorrechte. Wenn
Windows "Der Computer wurde durch Windows geschützt" meldet, wähle **Weitere Informationen** und dann **Trotzdem
ausführen** (der Installer ist noch nicht signiert).
- **Linux:** Mach die Datei ausführbar (`chmod +x`) und starte sie. Sie braucht FUSE 2: `sudo apt install libfuse2`
(Ubuntu 22.04) oder `libfuse2t64` (Ubuntu 24.04 und neuer).
- **Mac:** Öffne die `.dmg` und zieh Strata in den Ordner "Programme".
<a id="option-b-the-terminal-one-line"></a>
Auf dem ersten Bildschirm liest die App deinen PC aus und empfiehlt ein passendes Modell. Drück auf Installieren. Das Modell
ist ein großer Download (etwa 40-70 GB) und wird nach einer Unterbrechung dort fortgesetzt, wo er aufgehört hat. Wenn du
das Fenster schließt, beenden sich Strata und die Engine.
### Terminal-App
### Option B: das Terminal (eine Zeile)
Lieber im Terminal? Installiere Strata und starte dann **`strata launch`**.
Das Terminal ist der Befehl `strata`, gedacht für Skripte, Server und KI-Agents.
<details open>
<summary><b>Installationsbefehle</b></summary>
Linux und Mac:
**Linux / Mac**
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | 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
```
Das Skript prüft deinen PC, lädt das Terminal herunter, kontrolliert die SHA-256-Prüfsumme und entpackt es in deinen
Home-Ordner. Es braucht keine Administratorrechte. Öffne unter Windows danach ein neues Terminalfenster. Dann startest du
Strata:
Öffne unter Windows danach ein neues Terminal. Dann:
```sh
strata launch
```
Es stellt dir eine Frage nach der anderen: Modell, Kontext, Bilder, wer sich verbinden darf. Danach lädt es die Engine für
deine Grafikkarte herunter (signiert und vor der Nutzung probeweise gestartet), dazu das Modell, und beginnt zu laufen.
Für Skripte und KI-Agents gibt es Flags:
```sh
strata launch --model q2_0 --yes --no-tui
```
Wenn du das Installationsskript vor dem Ausführen lesen willst, lade es zuerst mit `curl -fsSL <address> -o install.sh`
herunter, lies es und führe dann `sh install.sh` aus. Alle Bildschirme und Flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
### Option C: 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 ein Modell passend zu deinem Speicher. Es ist ein einziges Modell in mehreren
Größen, mehr oder weniger stark komprimiert. 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, dieses Modell auszuführen.
| Größe | Benötigter Speicher (RAM + VRAM) | Download | Was es ist |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27,6 GB | 58,5 GB | Für PCs, auf denen nichts anderes läuft. Die Qualität ist geringer als bei IQ2_XS. |
| **Gyro-M** | 35 GB | 92,0 GB | Ungefähr die Qualität von IQ3_XXS bei etwa 20 % weniger GPU-Speicher. Es schreibt Antworten langsamer als Q2_0: 16 % auf einer RTX 3060 mit 12 GB, 21 % auf einer R9700. |
Gyro läuft auf NVIDIA- und AMD-Karten (bei AMD experimentell) auf Intel-Arc-Karten unter Linux (getestet auf einer Arc A750) und ab 0.2.2 auf Macs mit Apple Silicon (Gyro-S auf einem Mac mini mit 32 GB: 9 Tokens/s, die Experten werden von der SSD gelesen). Details und Messwerte:
[docs/GYRO.md](docs/GYRO.md). Starte es mit `strata launch --model gyro-s`.
### Die anderen Größen
| Dein Speicher (RAM + VRAM) | Nimm | Hinweise |
| --- | --- | --- |
| **32 GB** | **Gyro-S**, oder den **[Coder](docs/MODELS.md#coder)** für Code | Der Coder braucht 32 GB RAM und behält die Hälfte der Experten, deshalb ist er außerhalb von Code schwächer |
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), oder **Gyro-M** (35 GB) | Swift denkt kürzer nach, bevor es antwortet. Gyro-M kommt der Qualität von IQ3_XXS nahe, schreibt Antworten aber 16-21 % langsamer als Q2_0 |
| **48 GB** | **IQ3_XXS** (47,0 GB) | Langsamer, besser |
| **64 GB** | **IQ3_S** (54,8 GB), das Original oder Swift 1.5 | Am langsamsten, am besten. Es erreicht in den veröffentlichten Tests das volle Modell |
| **96 GB oder mehr** | **UD-IQ4_XS** von Unsloth (etwa 72 GB RAM) bis **Q8_0** (etwa 136 GB), 4 bis 8 Bit | Näher am vollen Modell. Bei weniger RAM liest Strata einen Teil von der SSD, das ist viel langsamer |
Auch mit weniger RAM, als eine Zeile nennt, kann es funktionieren: Strata wechselt von selbst in seinen
[Modus für wenig RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Rechne mit weniger Tempo, mit einer kleinen
Grafikkarte mit deutlich weniger.
Weitere Auswahl:
- **[Swift 1.5](docs/MODELS.md#swift-15)** ist eine Feinabstimmung, die kürzer nachdenkt, bevor sie antwortet, bei etwa gleicher Qualität und gleichem RAM-Bedarf.
- **[NVFP4](docs/NVFP4.md)** ist NVIDIAs 4-Bit-Version für NVIDIA-Karten. Sie braucht etwa 80 GB RAM, um alle Experten im RAM zu halten.
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** ist ein kleineres, anderes Modell, das Bilder liest und mehrere Anfragen gleichzeitig beantworten kann.
- **[OrcaRouters Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** richtest du von Hand ein.
Alle Größen, Downloads und was wo läuft: [docs/MODELS.md](docs/MODELS.md).
## Wie schnell ist es?
Gemessen mit einer Grafikkarte mit 12 GB (RTX 5070, Ryzen 5 7600, 64 GB RAM). Ergebnisse anderer Karten findest du hier:
[Tempo jeder Größe](docs/MODELS.md#how-fast-is-each-size), [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 |
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.
Gemessen mit einer AMD-Karte mit 16 GB (RX 9070 XT, Ryzen 9 3900X, 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 |
Die Balken haben in beiden Tabellen denselben Maßstab. Eine Karte mit mehr VRAM ist schneller. Eine RTX 3090 (24 GB) sollte
etwa 100-140 Tokens pro Sekunde schreiben. Die Engine-Version hinter jeder Zahl steht in [DETAILS.md](docs/DETAILS.md#speed-measured).
## Benutzung
### In der App
Die App hat einen **Chat**, einen **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. Mit `/loop 10m <what to do>` wiederholst du eine Nachricht alle 10 Minuten, solange
der Chat geöffnet ist.
Der Chat kann das Web mit DuckDuckGo (oder Bing) durchsuchen, ohne Konto. Unter **Einstellungen, Tools** kannst du einen
Suchschlüssel hinzufügen. Die Suche ist die einzige Funktion, die etwas von deinem PC sendet (die Suchbegriffe).
Um zwei Modelle auszuführen, schalte **Mehrere Modelle ausführen** ein und gib jedem Modell eine eigene Karte desselben
Herstellers.
<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>Ein Chat in der Strata-App: Der Agent hat einen kleinen Voxel-Pagodengarten als eine HTML-Datei geschrieben, und die Seite öffnet sich in der Vorschau neben dem Gespräch</sub></p>
### Im 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>, während das Modell antwortet: Geschwindigkeit, GPU, Speicher und die letzten Anfragen</sub></p>
```sh
strata launch # guided screens: model, context, pictures, access, then serve
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
strata launch --plan --model q2_0 # show what would happen; nothing changes
strata launch --status # what is being served now
strata launch --stop # stop it
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
strata update --check # is there a new engine? (strata update installs it)
strata doctor # check the installed engine files
```
`strata --help` listet alles auf. Starten und Bereitstellen laden nichts herunter. Nur die Installation, `strata launch`
(wenn du ein Modell wählst) und `strata update` tun das. Um Strata von deinem Handy oder einem anderen PC aus zu erreichen,
nutze `strata launch --access lan`. Der Befehl erzeugt einen API-Schlüssel und startet in einem Netzwerk nicht ohne einen.
Ein Browser öffnet sich nur, wenn du `--open` angibst. Anleitung: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Der ganze Client: [docs/TERMINAL.md](docs/TERMINAL.md).
### Mit KI-Agents
Ein Agent installiert Strata mit dem Einzeiler aus Option B 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 in [AGENTS.md](AGENTS.md).
### Andere Apps verbinden
| App-Typ | Basis-URL |
| --- | --- |
| OpenAI-kompatibel (die meisten Apps und Agents) | `http://127.0.0.1:8080/v1` (beliebiger API-Schlüssel, beliebiger Modellname) |
| Anthropic-API (Claude Code und andere) | `http://127.0.0.1:8080`, ohne `/v1`. Diese Apps hängen `/v1/messages` selbst an. 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.
`strata connect opencode --write` schreibt die Datei nach einer Rückfrage und legt eine Sicherung an.
### Bilder
Bilder sind für Modelle, die sie lesen können, standardmäßig eingeschaltet. Hänge ein Bild im Chat an oder füge es ein, oder
sende es aus deiner App. Der Bildleser läuft auf dem Prozessor, wenn deine Grafikkarte weniger als 24 GB hat (einige Sekunden
pro Bild, ohne Speicher der Karte zu belegen), und ab 24 GB auf der Grafikkarte. Die Regel gilt für NVIDIA und AMD gleich.
Du kannst Bilder pro Modell ausschalten oder den Leser mit `--vision off`, `--vision gpu` oder `--vision cpu` wählen.
Die Zahlen hinter der Regel: Auf einer Radeon AI PRO R9700 brauchte der Bildleser der Karte 57 ms für ein Bild mit 260 Tokens,
der des Prozessors 3,1 s. Auf Karten mit 16 GB (Tesla P100 und RTX A4000, Linux) kostete der Bildleser der Karte 3-5 % des
Texttempos, weil er etwa 1,4 GB belegt, die sonst das Modell nutzen würde. Auf dem Mac liest die GPU, egal wie viel Speicher er hat (0,45 s für ein Bild mit 260 Tokens auf einem M4 mit 32 GB, etwa 1,2 GB des gemeinsamen
Speichers); der Prozessor braucht dort etwa 50 s pro Bild und liest deshalb nur mit `--vision cpu`.
### Gut zu wissen
- Das Denkniveau (**aus, niedrig, mittel oder hoch**) stellst du im Chat-Menü oder unter "reasoning effort" in deiner App ein. Aus ist am schnellsten.
- Strata beantwortet standardmäßig eine Anfrage nach der anderen. Für mehrere gleichzeitig nutze `--parallel N` in `strata launch` oder
`"parallel": 2` in der Konfiguration ([BATCHING.md](docs/BATCHING.md)). Auf einer Karte mit 12 GB wird dadurch jede Antwort langsamer.
- Strata liest die erste Nachricht eines Chats komplett, etwa 1 Minute pro 30.000 Tokens. Folgenachrichten starten in Sekunden.
- Wo deine Chats gespeichert werden und die ganze API: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
## 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="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>
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="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></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, oder
führe `START-HERE.bat` (oder `./setup.sh`) erneut aus. Es macht dort weiter, wo es aufgehört hat.
- **Es ist sehr langsam, die Festplattenanzeige blinkt ständig, 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. `strata doctor` zeigt, welche Engine-Dateien installiert sind.
- **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 Probleme und Lösungen: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Du kommst nicht weiter? Öffne ein
[Issue](https://github.com/Niko1221/Strata/issues). Hänge in der App die Datei aus **Einstellungen, Engine, Diagnose, Protokolle anzeigen, Protokolle speichern** an. Füge im Terminal die Ausgabe von `strata doctor` ein. Hänge beim Start aus dem Quellcode die
Datei `strata-<model>.log` aus dem Strata-Ordner an. 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. 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]).
+121 -336
View File
@@ -1,403 +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"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></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="#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/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>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">
<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://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>
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. 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.
<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></sub></p>
**¿Eres nuevo?** Mira [lo que necesitas](#lo-que-necesitas) y luego [instala la aplicación](#opción-a-la-aplicación-strata).
**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.
- **Funciona en un PC gaming.** Con 32 GB entre RAM y memoria gráfica ya puedes empezar.
- **Funciona con tus herramientas.** Habla las API de OpenAI y de Anthropic, así que Claude Code, Codex CLI, OpenCode y otros agentes se conectan a él.
- **Lee imágenes.** Viene activado por defecto.
- **Gratis.** Licencia MIT, sin cuenta, sin nube.
- **Privado.** Sin telemetría. Lo que Strata descarga, cuándo usa internet y cómo comprueba las descargas está en [docs/SAFETY.md](docs/SAFETY.md).
## Modelos
Versión 0.2.3 · motor 0.1.48
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"> &nbsp;<b>Qwen3.8-Flash-Next</b></summary>
**Necesitas al menos 32 GB entre RAM y memoria gráfica (VRAM).** En un Mac o un Ryzen AI Max, es la memoria unificada.
Con más memoria puedes usar un modelo más grande o más rápido.
- Un PC de 32 GB con una tarjeta de 12 a 16 GB ejecuta el Coder.
- Con 64 GB de RAM funcionan todos los tamaños.
| Tu ordenador | Detalles |
| Proveedor | Opciones |
| --- | --- |
| **NVIDIA** GeForce RTX serie 20, 30, 40, 50 (12 GB o más) | Instala la aplicación o la Terminal. |
| **AMD** Radeon RX 6800 y posteriores, Radeon AI PRO R9700 (12 GB o más) | [Tarjetas AMD](docs/AMD_HIP.md) |
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [motor Intel](docs/INTEL.md) |
| **Mac con Apple Silicon** (M1 o posterior, macOS 15 o posterior) | [Mac](docs/MAC.md) |
| **AMD Ryzen AI Max (Strix Halo)** y otros sistemas con memoria unificada | [Strix Halo](docs/STRIX_HALO.md) |
| Tarjetas y procesadores antiguos | [GPU antiguas](docs/OLDER_GPUS.md), [CPU antiguas](docs/INSTALL.md#older-cpus-experimental) |
| **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 hace falta:
[Compara tamaños, necesidades de memoria y compatibilidad](docs/MODELS.md).
- unos 80 GB de disco libre (con un SSD el primer arranque es mucho más rápido)
- Windows 10 / 11, Linux o macOS
- un controlador gráfico actual
</details>
Dos o tres tarjetas de la misma marca pueden compartir un modelo ([multi-GPU](docs/MULTI_GPU.md)). Strata nunca reparte un
modelo entre tarjetas NVIDIA y AMD. Lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<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"> &nbsp;<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
Elige una opción. Todas arrancan el mismo motor y usan los mismos modelos.
<a id="option-a-the-strata-app"></a>
### Opción A: la aplicación Strata
### Aplicación de escritorio
La forma más fácil. La aplicación lleva el motor dentro: la instalas, eliges un modelo y chateas. Descarga el archivo para
tu sistema y tu tarjeta gráfica desde la [página de versiones](https://github.com/Niko1221/Strata/releases/latest). ¿No
sabes cuál elegir? Mira el párrafo que hay debajo de la tabla.
**[Descargar Strata para Windows, Linux o Mac →](https://github.com/Niko1221/Strata/releases/latest)**
| Sistema | Tu tarjeta gráfica | Archivo que descargar |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX, controlador 580 o posterior | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
| | NVIDIA, tarjeta más antigua o controlador anterior a 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon serie RX 9000 o Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon serie RX 7000 o Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX, controlador 580 o posterior | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
| | NVIDIA, tarjeta más antigua o controlador anterior a 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon serie RX 9000 o Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon serie RX 7000 | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
| **Mac** (Apple Silicon, macOS 15 o posterior) | integrada | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
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).
**Si no estás seguro:** con NVIDIA, elige `cuda13` si la versión de tu controlador es la 580 o posterior, y `cuda12` en caso
contrario. `cuda12` es para tarjetas y controladores más antiguos. Los archivos `hip` y `amd` son para tarjetas AMD. Las
demás tarjetas AMD (RX 6000) todavía no tienen una aplicación ya preparada: mira
[Tarjetas AMD](docs/AMD_HIP.md).
<sub>Archivos: `Strata-0.2.6-windows-x64-setup.exe` · `Strata-0.2.6-linux-x86_64.AppImage` · `Strata-0.2.6-macos-arm64.dmg`</sub>
Después:
<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>
- **Windows:** ejecuta el `.exe`. Se instala para tu usuario y no necesita permisos de administrador. Si Windows muestra
"Windows protegió tu PC", elige **Más información** y luego **Ejecutar de todas formas** (el instalador todavía no está
firmado).
- **Linux:** haz el archivo ejecutable (`chmod +x`) y lánzalo. Necesita FUSE 2: `sudo apt install libfuse2` (Ubuntu 22.04)
o `libfuse2t64` (Ubuntu 24.04 y posteriores).
- **Mac:** abre el `.dmg` y arrastra Strata a Aplicaciones.
<a id="option-b-the-terminal-one-line"></a>
En la primera pantalla, la aplicación analiza tu PC y recomienda un modelo adecuado. Pulsa Instalar. El modelo es una
descarga grande (unos 40 a 70 GB) y, si se interrumpe, continúa donde se quedó. Al cerrar la ventana, Strata y el motor se
detienen.
### Aplicación de Terminal
### Opción B: la Terminal (una línea)
¿Prefieres la terminal? Instala Strata y ejecuta **`strata launch`**.
La Terminal es el comando `strata`, pensado para scripts, servidores y agentes de IA.
<details open>
<summary><b>Comandos de instalación</b></summary>
Linux y Mac:
**Linux / Mac**
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | 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
```
El script analiza tu PC, descarga la Terminal, verifica su SHA-256 y la descomprime en tu carpeta personal. No necesita
permisos de administrador. En Windows, abre una ventana de terminal nueva después. Luego arranca Strata:
En Windows, abre una terminal nueva después. Luego:
```sh
strata launch
```
Te hace una pregunta cada vez: modelo, contexto, imágenes, quién puede conectarse. Después descarga el motor para tu tarjeta
gráfica (firmado y arrancado como prueba antes de usarlo) y el modelo, y empieza a servir. Para scripts y agentes de IA, usa
opciones:
```sh
strata launch --model q2_0 --yes --no-tui
```
Si quieres leer el script de instalación antes de ejecutarlo, descárgalo primero con `curl -fsSL <address> -o install.sh`,
léelo y luego ejecuta `sh install.sh`. Todas las pantallas y opciones: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
### Opción C: 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 modelo según tu memoria. Es un solo modelo en varios tamaños, más o menos
comprimido. 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 este modelo.
| Tamaño | Memoria que necesita (RAM + VRAM) | Descarga | Qué es |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27,6 GB | 58,5 GB | Para PC donde no cabe nada más. La calidad es menor que la de IQ2_XS. |
| **Gyro-M** | 35 GB | 92,0 GB | Calidad parecida a IQ3_XXS con un 20 % menos de memoria de GPU. Escribe las respuestas un 16 % más despacio que Q2_0 en una RTX 3060 de 12 GB y un 21 % más despacio en una R9700. |
Gyro funciona en tarjetas NVIDIA y AMD (en AMD es experimental) en tarjetas Intel Arc con Linux (probado en una Arc A750) y, desde 0.2.2, en Mac con Apple Silicon (Gyro-S en un Mac mini de 32 GB: 9 tokens/s, con los expertos leídos del SSD). Detalles y mediciones:
[docs/GYRO.md](docs/GYRO.md). Arráncalo con `strata launch --model gyro-s`.
### Los demás tamaños
| Tu memoria (RAM + VRAM) | Elige | Notas |
| --- | --- | --- |
| **32 GB** | **Gyro-S**, o el **[Coder](docs/MODELS.md#coder)** para código | El Coder necesita 32 GB de RAM y conserva la mitad de los expertos, así que es más flojo fuera del código |
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), o **Gyro-M** (35 GB) | Swift piensa menos tiempo antes de responder. Gyro-M se acerca a la calidad de IQ3_XXS, pero escribe las respuestas un 16-21 % más despacio que Q2_0 |
| **48 GB** | **IQ3_XXS** (47,0 GB) | Más lento, mejor |
| **64 GB** | **IQ3_S** (54,8 GB), el original o Swift 1.5 | El más lento, el mejor. Iguala al modelo completo en las pruebas publicadas |
| **96 GB o más** | **UD-IQ4_XS** de Unsloth (unos 72 GB de RAM) hasta **Q8_0** (unos 136 GB), de 4 a 8 bits | Más cerca del modelo completo. Con menos RAM, Strata lee una parte desde el SSD, que es mucho más lento |
Con menos RAM de la que indica una fila también puede funcionar: Strata pasa solo a su
[modo de poca RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Espera que sea más lento, y mucho más lento con una
tarjeta gráfica pequeña.
Más opciones:
- **[Swift 1.5](docs/MODELS.md#swift-15)** es un ajuste fino que piensa menos tiempo antes de responder, con una calidad y un uso de RAM casi iguales.
- **[NVFP4](docs/NVFP4.md)** es la versión de 4 bits de NVIDIA para tarjetas NVIDIA. Necesita unos 80 GB de RAM para tener todos los expertos en RAM.
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** es un modelo más pequeño y distinto que lee imágenes y puede atender varias peticiones a la vez.
- **[El Uncensored IQ3_XXS de OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** se configura a mano.
Todos los tamaños, descargas y lo que cabe en cada equipo: [docs/MODELS.md](docs/MODELS.md).
## ¿Qué velocidad tiene?
Medido con una tarjeta gráfica de 12 GB (RTX 5070, Ryzen 5 7600, 64 GB de RAM). Resultados con otras tarjetas:
[velocidad de cada tamaño](docs/MODELS.md#how-fast-is-each-size), [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 |
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.
Medido con una tarjeta AMD de 16 GB (RX 9070 XT, Ryzen 9 3900X, 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 |
Las barras usan la misma escala en las dos tablas. Una tarjeta con más VRAM es más rápida. Una RTX 3090 (24 GB) debería
escribir unos 100-140 tokens por segundo. La versión del motor detrás de cada cifra está en [DETAILS.md](docs/DETAILS.md#speed-measured).
## Uso
### En la aplicación
La aplicación tiene 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. Escribe `/loop 10m <what to do>` para repetir un mensaje cada 10 minutos mientras el chat esté abierto.
El chat puede buscar en la web con DuckDuckGo (o Bing), sin cuenta. Puedes añadir una clave de búsqueda en **Ajustes,
Herramientas**. La búsqueda es la única función que envía algo fuera de tu PC (las palabras que buscas).
Para ejecutar dos modelos, activa **Ejecutar varios modelos** y dale a cada modelo su propia tarjeta de la misma marca.
<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>Un chat en la app de Strata: el agente escribió un pequeño jardín de pagoda de vóxeles en un solo archivo HTML, y la página se abre en la vista previa junto a la conversación</sub></p>
### En la 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> mientras el modelo responde: velocidad, GPU, memoria y las últimas solicitudes</sub></p>
```sh
strata launch # guided screens: model, context, pictures, access, then serve
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
strata launch --plan --model q2_0 # show what would happen; nothing changes
strata launch --status # what is being served now
strata launch --stop # stop it
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
strata update --check # is there a new engine? (strata update installs it)
strata doctor # check the installed engine files
```
`strata --help` lo lista todo. Arrancar y servir no descargan nada. Solo lo hacen la instalación, `strata launch` (cuando
eliges un modelo) y `strata update`. Para llegar a Strata desde tu móvil u otro PC, usa `strata launch --access lan`. Crea
una clave de API y no arranca en una red sin ella. No se abre ningún navegador salvo que añadas `--open`. Guía:
[docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). El cliente completo: [docs/TERMINAL.md](docs/TERMINAL.md).
### Con agentes de IA
Un agente instala Strata con el comando de una línea de la opción B 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),
y las reglas del repositorio en [AGENTS.md](AGENTS.md).
### Conecta tus otras aplicaciones
| Tipo de aplicación | URL base |
| --- | --- |
| Compatible con 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 y otros) | `http://127.0.0.1:8080`, sin `/v1`. Estas aplicaciones añaden `/v1/messages` por su cuenta. 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.
`strata connect opencode --write` escribe el archivo después de preguntar y guarda una copia de seguridad.
### Imágenes
Las imágenes vienen activadas por defecto en los modelos que pueden leerlas. Adjunta o pega una imagen en el chat, o envíala
desde tu aplicación. El lector de imágenes funciona en el procesador cuando tu tarjeta gráfica tiene menos de 24 GB (unos
segundos por imagen, sin quitar memoria a la tarjeta) y en la tarjeta gráfica a partir de 24 GB. La regla es la misma para
NVIDIA y AMD. Puedes desactivar las imágenes por modelo, o elegir el lector con `--vision off`, `--vision gpu` o
`--vision cpu`.
Las cifras detrás de la regla: en una Radeon AI PRO R9700, el lector de la tarjeta tardó 57 ms en una imagen de 260 tokens y
el del procesador 3,1 s. En tarjetas de 16 GB (Tesla P100 y RTX A4000, Linux), el lector de la tarjeta costó un 3-5 % de
velocidad en texto, porque ocupa unos 1,4 GB que de otro modo usaría el modelo. En un Mac lee la GPU, tenga la memoria que tenga (0,45 s para una imagen de 260 tokens en un M4 de 32 GB, unos 1,2 GB de la memoria compartida);
el procesador tarda unos 50 s por imagen, así que solo lee con `--vision cpu`.
### Conviene saber
- Elige el nivel de razonamiento (**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.
- Strata responde a una petición a la vez por defecto. Para varias a la vez, usa `--parallel N` en `strata launch` o
`"parallel": 2` en la configuración ([BATCHING.md](docs/BATCHING.md)). En una tarjeta de 12 GB, esto hace más lenta cada respuesta.
- Strata lee entero el primer mensaje de un chat, unos 1 minuto por cada 30.000 tokens. Los mensajes siguientes empiezan en segundos.
- Dónde se guardan tus chats y la API completa: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
## 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="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>
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="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></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`, o vuelve a ejecutar
`START-HERE.bat` (o `./setup.sh`). Continúa donde se quedó.
- **Va muy lento, la luz del disco parpadea sin parar, 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`. `strata doctor` muestra qué archivos del motor están instalados.
- **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 problemas y soluciones: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). ¿Sigues atascado? Abre una
[incidencia](https://github.com/Niko1221/Strata/issues). En la aplicación, adjunta el archivo de **Ajustes, Motor,
Diagnóstico, Ver registros, Guardar registros**. En la Terminal, pega la salida de `strata doctor`. Si lo ejecutas desde el
código fuente, adjunta `strata-<model>.log` de la carpeta de Strata. 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. 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]).
+121 -338
View File
@@ -1,405 +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"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></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="#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/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>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">
<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://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>
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. 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.
<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></sub></p>
**Nouveau ici ?** Regardez [ce qu'il vous faut](#ce-quil-vous-faut), puis [installez l'application](#option-a--lapplication-strata).
**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.
- **Tourne sur un PC de jeu.** 32 Go de RAM et de mémoire graphique réunis suffisent pour commencer.
- **Fonctionne avec vos outils.** Il parle les API d'OpenAI et d'Anthropic, donc Claude Code, Codex CLI, OpenCode et d'autres agents peuvent s'y connecter.
- **Lit les images.** C'est activé par défaut.
- **Gratuit.** Licence MIT, pas de compte, pas de cloud.
- **Privé.** Aucune télémétrie. Ce que Strata télécharge, quand il utilise internet et comment il vérifie les téléchargements est expliqué dans [docs/SAFETY.md](docs/SAFETY.md).
## Modèles
Version 0.2.3 · moteur 0.1.48
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"> &nbsp;<b>Qwen3.8-Flash-Next</b></summary>
**Il vous faut au moins 32 Go de RAM et de mémoire graphique (VRAM) réunis.** Sur un Mac ou un Ryzen AI Max, c'est la
mémoire unifiée. Plus de mémoire permet un modèle plus gros ou plus rapide.
- Un PC de 32 Go avec une carte de 12 à 16 Go fait tourner le Coder.
- Avec 64 Go de RAM, toutes les tailles fonctionnent.
| Votre ordinateur | Détails |
| Fournisseur | Choix |
| --- | --- |
| **NVIDIA** GeForce RTX série 20, 30, 40, 50 (12 Go ou plus) | Installez l'application ou le Terminal. |
| **AMD** Radeon RX 6800 et plus récent, Radeon AI PRO R9700 (12 Go ou plus) | [Cartes AMD](docs/AMD_HIP.md) |
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [moteur Intel](docs/INTEL.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 systèmes à mémoire unifiée | [Strix Halo](docs/STRIX_HALO.md) |
| Cartes et processeurs plus anciens | [GPU anciens](docs/OLDER_GPUS.md), [CPU anciens](docs/INSTALL.md#older-cpus-experimental) |
| **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 faut aussi :
[Comparer les tailles, la mémoire nécessaire et la compatibilité](docs/MODELS.md).
- environ 80 Go d'espace disque libre (un SSD accélère beaucoup le premier démarrage)
- Windows 10 / 11, Linux ou macOS
- un pilote graphique récent
</details>
Deux ou trois cartes de la même marque peuvent se partager un modèle ([multi-GPU](docs/MULTI_GPU.md)). Strata ne répartit
jamais un modèle entre des cartes NVIDIA et AMD. Liste complète : [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<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"> &nbsp;<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
Choisissez une option. Elles lancent toutes le même moteur et utilisent les mêmes modèles.
<a id="option-a-the-strata-app"></a>
### Option A : l'application Strata
### Application de bureau
Le plus simple. L'application contient le moteur : installez-la, choisissez un modèle, discutez. Téléchargez le fichier
correspondant à votre système et à votre carte graphique depuis la [page des versions](https://github.com/Niko1221/Strata/releases/latest).
Vous ne savez pas lequel prendre ? Lisez le paragraphe sous le tableau.
**[Télécharger Strata pour Windows, Linux ou Mac →](https://github.com/Niko1221/Strata/releases/latest)**
| Système | Votre carte graphique | Fichier à télécharger |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX, pilote 580 ou plus récent | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
| | NVIDIA, carte plus ancienne ou pilote antérieur à 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon série RX 7000 ou Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX, pilote 580 ou plus récent | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
| | NVIDIA, carte plus ancienne ou pilote antérieur à 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon série RX 7000 | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
| **Mac** (Apple Silicon, macOS 15 ou plus récent) | intégrée | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
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).
**Si vous hésitez :** pour NVIDIA, prenez `cuda13` si la version de votre pilote est 580 ou plus récente, sinon `cuda12`.
`cuda12` convient aux cartes plus anciennes et aux pilotes plus anciens. Les fichiers `hip` et `amd` sont pour les cartes
AMD. Les autres cartes AMD (RX 6000) n'ont pas encore d'application prête à l'emploi : voir
[Cartes AMD](docs/AMD_HIP.md).
<sub>Fichiers: `Strata-0.2.6-windows-x64-setup.exe` · `Strata-0.2.6-linux-x86_64.AppImage` · `Strata-0.2.6-macos-arm64.dmg`</sub>
Ensuite :
<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>
- **Windows :** lancez le `.exe`. Il s'installe pour votre utilisateur et ne demande pas de droits d'administrateur. Si
Windows affiche « Windows a protégé votre ordinateur », choisissez **Informations complémentaires**, puis **Exécuter
quand même** (l'installateur n'est pas encore signé).
- **Linux :** rendez le fichier exécutable (`chmod +x`) et lancez-le. Il a besoin de FUSE 2 : `sudo apt install libfuse2`
(Ubuntu 22.04) ou `libfuse2t64` (Ubuntu 24.04 et plus récent).
- **Mac :** ouvrez le `.dmg` et faites glisser Strata dans Applications.
<a id="option-b-the-terminal-one-line"></a>
Sur le premier écran, l'application analyse votre PC et recommande un modèle adapté. Appuyez sur Installer. Le modèle est un
gros téléchargement (environ 40 à 70 Go) qui reprend là où il s'est arrêté en cas d'interruption. Quand vous fermez la
fenêtre, Strata et le moteur s'arrêtent.
### Application Terminal
### Option B : le Terminal (une ligne)
Vous préférez le terminal ? Installez Strata, puis lancez **`strata launch`**.
Le Terminal est la commande `strata`, pensée pour les scripts, les serveurs et les agents d'IA.
<details open>
<summary><b>Commandes d'installation</b></summary>
Linux et Mac :
**Linux / Mac**
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | 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
```
Le script analyse votre PC, télécharge le Terminal, vérifie son empreinte SHA-256 et le décompresse dans votre dossier
personnel. Il ne demande pas de droits d'administrateur. Sous Windows, ouvrez ensuite une nouvelle fenêtre de terminal.
Puis lancez Strata :
Sous Windows, ouvrez ensuite un nouveau terminal. Puis :
```sh
strata launch
```
Il pose une question à la fois : modèle, contexte, images, qui peut se connecter. Ensuite il télécharge le moteur pour votre
carte graphique (signé, et lancé à titre d'essai avant usage) ainsi que le modèle, puis commence à servir. Pour les scripts
et les agents d'IA, utilisez des options :
```sh
strata launch --model q2_0 --yes --no-tui
```
Pour lire le script d'installation avant de l'exécuter, téléchargez-le d'abord avec `curl -fsSL <address> -o install.sh`,
lisez-le, puis lancez `sh install.sh`. Tous les écrans et toutes les options : [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
### Option C : 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 un modèle selon votre mémoire. C'est un seul modèle en plusieurs tailles,
plus ou moins compressé. 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) (par agentionai) est la façon la plus légère de faire tourner ce modèle.
| Taille | Mémoire nécessaire (RAM + VRAM) | Téléchargement | Ce que c'est |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27,6 Go | 58,5 Go | Pour les PC où rien d'autre ne tient. La qualité est inférieure à celle d'IQ2_XS. |
| **Gyro-M** | 35 Go | 92,0 Go | À peu près la qualité d'IQ3_XXS avec environ 20 % de mémoire GPU en moins. Il écrit les réponses 16 % plus lentement que Q2_0 sur une RTX 3060 de 12 Go et 21 % plus lentement sur une R9700. |
Gyro fonctionne sur les cartes NVIDIA et AMD (expérimental sur AMD) sur les cartes Intel Arc sous Linux (testé sur une Arc A750) et, depuis la 0.2.2, sur les Mac Apple Silicon (Gyro-S sur un Mac mini de 32 Go : 9 tokens/s, experts lus sur le SSD). Détails et mesures :
[docs/GYRO.md](docs/GYRO.md). Lancez-le avec `strata launch --model gyro-s`.
### Les autres tailles
| Votre mémoire (RAM + VRAM) | Prenez | Remarques |
| --- | --- | --- |
| **32 Go** | **Gyro-S**, ou le **[Coder](docs/MODELS.md#coder)** pour le code | Le Coder demande 32 Go de RAM et garde la moitié des experts, il est donc moins bon hors du code |
| **40 Go** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 Go), ou **Gyro-M** (35 Go) | Swift réfléchit moins longtemps avant de répondre. Gyro-M approche la qualité d'IQ3_XXS, mais il écrit les réponses 16 à 21 % plus lentement que Q2_0 |
| **48 Go** | **IQ3_XXS** (47,0 Go) | Plus lent, meilleur |
| **64 Go** | **IQ3_S** (54,8 Go), l'original ou Swift 1.5 | Le plus lent, le meilleur. Il égale le modèle complet dans les tests publiés |
| **96 Go ou plus** | **UD-IQ4_XS** d'Unsloth (environ 72 Go de RAM) jusqu'à **Q8_0** (environ 136 Go), 4 à 8 bits | Plus proche du modèle complet. Avec moins de RAM, Strata lit une partie depuis le SSD, ce qui est beaucoup plus lent |
Avoir moins de RAM que ce qu'indique une ligne peut encore fonctionner : Strata passe tout seul en
[mode peu de RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Attendez-vous à plus de lenteur, et à bien plus avec
une petite carte graphique.
Autres choix :
- **[Swift 1.5](docs/MODELS.md#swift-15)** est une version affinée qui réfléchit moins longtemps avant de répondre, pour une qualité et un besoin en RAM à peu près identiques.
- **[NVFP4](docs/NVFP4.md)** est la version 4 bits de NVIDIA pour les cartes NVIDIA. Elle demande environ 80 Go de RAM pour garder tous les experts en RAM.
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** est un modèle plus petit et différent, qui lit les images et peut répondre à plusieurs requêtes à la fois.
- **[L'Uncensored IQ3_XXS d'OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** se configure à la main.
Toutes les tailles, les téléchargements et ce qui tient où : [docs/MODELS.md](docs/MODELS.md).
## Quelle est sa vitesse ?
Mesuré avec une carte graphique de 12 Go (RTX 5070, Ryzen 5 7600, 64 Go de RAM). Voir les résultats d'autres cartes :
[vitesse de chaque taille](docs/MODELS.md#how-fast-is-each-size), [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 |
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.
Mesuré avec une carte AMD de 16 Go (RX 9070 XT, Ryzen 9 3900X, 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 |
Les barres utilisent la même échelle dans les deux tableaux. Une carte avec plus de VRAM est plus rapide. Une RTX 3090
(24 Go) devrait écrire environ 100 à 140 tokens par seconde. La version du moteur derrière chaque chiffre est indiquée dans
[DETAILS.md](docs/DETAILS.md#speed-measured).
## Utilisation
### Dans l'application
L'application comprend un **Chat**, un **Monitor** du modèle, de votre GPU, de votre CPU et de votre 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. Tapez `/loop 10m <what to do>` pour répéter un message toutes les
10 minutes tant que le chat est ouvert.
Le chat peut chercher sur le web avec DuckDuckGo (ou Bing), sans compte. Vous pouvez ajouter une clé de recherche sous
**Settings, Tools**. La recherche est la seule fonction qui envoie quelque chose hors de votre PC (les mots recherchés).
Pour faire tourner deux modèles, activez **Run multiple models** (exécuter plusieurs modèles) et donnez à chacun sa propre carte de la même marque.
<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>Une discussion dans l'app Strata : l'agent a écrit un petit jardin de pagode en voxels dans un seul fichier HTML, et la page s'ouvre dans l'aperçu à côté de la conversation</sub></p>
### Dans le 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> pendant que le modèle répond : vitesse, GPU, mémoire et les dernières requêtes</sub></p>
```sh
strata launch # guided screens: model, context, pictures, access, then serve
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
strata launch --plan --model q2_0 # show what would happen; nothing changes
strata launch --status # what is being served now
strata launch --stop # stop it
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
strata update --check # is there a new engine? (strata update installs it)
strata doctor # check the installed engine files
```
`strata --help` liste tout. Démarrer et servir ne téléchargent rien. Seuls l'installation, `strata launch` (quand vous
choisissez un modèle) et `strata update` le font. 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 et refuse de démarrer sur un réseau sans elle. Aucun
navigateur ne s'ouvre, sauf si vous ajoutez `--open`. Guide : [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Le client complet : [docs/TERMINAL.md](docs/TERMINAL.md).
### Avec des agents d'IA
Un agent installe Strata avec la commande en une ligne de l'option B 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),
et les règles du dépôt dans [AGENTS.md](AGENTS.md).
### Connecter vos autres applications
| Type d'application | URL de base |
| --- | --- |
| Compatible 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 et d'autres) | `http://127.0.0.1:8080`, sans `/v1`. Ces applications ajoutent `/v1/messages` elles-mêmes. 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.
`strata connect opencode --write` écrit le fichier après confirmation et garde une sauvegarde.
### Images
Les images sont activées par défaut pour les modèles qui savent les lire. Joignez ou collez une image dans le chat, ou
envoyez-la depuis votre application. Le lecteur d'images tourne sur le processeur quand votre carte graphique a moins de
24 Go (quelques secondes par image, sans prendre de mémoire à la carte) et sur la carte graphique à partir de 24 Go. La
règle est la même pour NVIDIA et AMD. Vous pouvez désactiver les images par modèle, ou choisir le lecteur avec
`--vision off`, `--vision gpu` ou `--vision cpu`.
Les chiffres derrière la règle : sur une Radeon AI PRO R9700, le lecteur de la carte a pris 57 ms pour une image de
260 tokens et celui du processeur 3,1 s. Sur des cartes de 16 Go (Tesla P100 et RTX A4000, Linux), le lecteur de la carte
a coûté 3 à 5 % de vitesse sur le texte, parce qu'il occupe environ 1,4 Go que le modèle utiliserait sinon. Sur un Mac, le lecteur tourne sur le GPU quelle que soit la mémoire (0,45 s pour une image de 260 tokens sur un M4 de 32 Go, environ 1,2 Go de la
mémoire partagée) ; le processeur met environ 50 s par image, il ne lit donc qu'avec `--vision cpu`.
### Bon à savoir
- Choisissez le niveau de réflexion (**désactivé, faible, moyen ou élevé**) dans le menu du chat ou dans le réglage « reasoning effort » de votre application. Désactivé est le plus rapide.
- Strata répond à une requête à la fois par défaut. Pour en traiter plusieurs en même temps, utilisez `--parallel N` dans `strata launch` ou
`"parallel": 2` dans la configuration ([BATCHING.md](docs/BATCHING.md)). Sur une carte de 12 Go, chaque réponse devient alors plus lente.
- Strata lit en entier le premier message d'une discussion, soit environ 1 minute pour 30 000 tokens. Les messages suivants démarrent en quelques secondes.
- Où sont stockées vos discussions et l'API complète : [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
## 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="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>
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="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></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`, ou relancez
`START-HERE.bat` (ou `./setup.sh`). Cela reprend là où ça s'était arrêté.
- **C'est très lent, le voyant du disque clignote sans arrêt, 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`. `strata doctor` montre quels fichiers du moteur sont installés.
- **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 problèmes et solutions : [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Toujours bloqué ? Ouvrez une
[issue](https://github.com/Niko1221/Strata/issues). Dans l'application, joignez le fichier obtenu par **Settings, Engine, Diagnostics, View logs, Save logs**. Dans le Terminal, collez la sortie de `strata doctor`. Si
vous avez lancé depuis les sources, joignez `strata-<model>.log` du dossier Strata. 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. 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]).
+121 -318
View File
@@ -1,385 +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"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></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="#モデル">モデル</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/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>ボクセルの塔の庭園。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">
<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://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>
Strata は **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** を動かします。1250億パラメータのモデルで、
ふつうはサーバーが必要ですが、Strata なら一般的なPCで使えます。チャット、コード作成、画像の読み取りができ、
お使いのアプリやコーディングエージェントとも連携します。チャットとファイルはPCの外に出ません。
<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></sub></p>
**はじめての方へ。** まず[必要な環境](#必要な環境)を確認して、[アプリをインストール](#方法a-strata-アプリ)してください。
**無料でオープンソース。自分の環境で動かせます。** デスクトップで、またはお好みのコーディングツールと一緒に Strata を使えます。アカウントは不要です。
- **ゲーミングPCで動きます。** RAMとグラフィックスメモリの合計が32 GBあれば始められます。
- **お使いのツールと連携します。** OpenAI と Anthropic の API に対応しているので、Claude Code、Codex CLI、OpenCode などのエージェントから接続できます。
- **画像を読めます。** 初期設定でオンです。
- **無料です。** MIT ライセンス。アカウントもクラウドも不要です。
- **プライバシーを守ります。** テレメトリはありません。Strata が何をダウンロードするか、いつインターネットを使うか、ダウンロードをどう検証するかは [docs/SAFETY.md](docs/SAFETY.md) に書いてあります。
## モデル
バージョン 0.2.3 · エンジン 0.1.48
まず **ISTA-DASLab** をおすすめします。次に **Unsloth** などの提供元です。メモリに収まる互換モデルがある場合に選べます。
## 必要な環境
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<b>Qwen3.8-Flash-Next</b></summary>
**RAMとグラフィックスメモリ(VRAM)の合計が、少なくとも32 GB必要です。** MacやRyzen AI Maxでは、ユニファイドメモリがその量にあたります。
メモリが多いほど、大きなモデルを使えたり、速く動いたりします。
- RAM 32 GBのPCに12〜16 GBのグラフィックスカードがあれば、Coder が動きます。
- RAM 64 GBなら、どのサイズも動きます。
| お使いのPC | 詳細 |
| 提供元 | 選択肢 |
| --- | --- |
| **NVIDIA** GeForce RTX 20、30、40、50 シリーズ(12 GB以上) | アプリまたはターミナル版をインストールしてください。 |
| **AMD** Radeon RX 6800 以降、Radeon AI PRO R9700(12 GB以上) | [AMD のカード](docs/AMD_HIP.md) |
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md)、[Intel エンジン](docs/INTEL.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)、[古いCPU](docs/INSTALL.md#older-cpus-experimental) |
| **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) |
そのほかに必要なもの:
[サイズ、必要なメモリ、互換性を比べる](docs/MODELS.md)。
- 約80 GBの空きディスク容量(SSDなら初回の起動がずっと速くなります)
- Windows 10 / 11、Linux、または macOS
- 最新のグラフィックスドライバー
</details>
同じメーカーのカードを2〜3枚使って、1つのモデルを動かせます([マルチGPU](docs/MULTI_GPU.md))。NVIDIA と AMD のカードにまたがってモデルを分けることはありません。
一覧は [docs/INSTALL.md](docs/INSTALL.md#what-you-need) にあります。
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<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"> &nbsp;<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)。
## インストール
3つの方法から1つ選んでください。どれも同じエンジンを起動し、同じモデルを使います。
<a id="option-a-the-strata-app"></a>
### 方法A: Strata アプリ
### デスクトップアプリ
いちばん簡単な方法です。エンジンはアプリに入っているので、インストールしてモデルを選べばチャットを始められます。お使いのOSとグラフィックスカードに合うファイルを
[リリースページ](https://github.com/Niko1221/Strata/releases/latest)からダウンロードしてください。どれか分からない場合は、表の下の説明を見てください。
**[Windows、Linux、Mac 用の Strata をダウンロード →](https://github.com/Niko1221/Strata/releases/latest)**
| OS | グラフィックスカード | ダウンロードするファイル |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX、ドライバー 580 以降 | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
| | NVIDIA、古いカード、またはドライバーが 580 より古い場合 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon RX 9000 シリーズまたは Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon RX 7000 シリーズまたは Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX、ドライバー 580 以降 | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
| | NVIDIA、古いカード、またはドライバーが 580 より古い場合 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon RX 9000 シリーズまたは Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon RX 7000 シリーズ | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
| **Mac**(Apple Silicon、macOS 15 以降) | 内蔵 | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
アプリをインストールし、モデルを選んで、チャットを始めましょう。インストーラーはシステムごとに1つです。初回起動時に、アプリがお使いのグラフィックスカード用のエンジンをダウンロードします。[インストーラーのヘルプ](docs/INSTALL.md#the-app)。
**迷ったときは:** NVIDIA の場合、ドライバーがバージョン 580 以降なら `cuda13`、それより古ければ `cuda12` を選びます。`cuda12` は古いカードと古いドライバー向けです。
`hip` と `amd` のファイルは AMD のカード用です。それ以外の AMD カード(RX 6000)には、まだ用意されたアプリがありません。
[AMD のカード](docs/AMD_HIP.md)を見てください。
<sub>ファイル: `Strata-0.2.6-windows-x64-setup.exe` · `Strata-0.2.6-linux-x86_64.AppImage` · `Strata-0.2.6-macos-arm64.dmg`</sub>
そのあとの手順:
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="チャットとライブのコードプレビューを並べて表示した Strata デスクトップアプリ">
<br><sub>チャット、ファイル、ライブプレビューを1つのウィンドウで。</sub>
</p>
- **Windows:** `.exe` を実行します。ユーザー単位でインストールされ、管理者権限は不要です。「Windows によってPCが保護されました」と表示されたら、
**詳細情報**を選び、**実行**を選んでください(インストーラーにはまだコード署名がありません)。
- **Linux:** ファイルを実行可能にして(`chmod +x`)起動します。FUSE 2 が必要です。`sudo apt install libfuse2`(Ubuntu 22.04)
または `libfuse2t64`(Ubuntu 24.04 以降)を実行してください。
- **Mac:** `.dmg` を開き、Strata を「アプリケーション」にドラッグします。
<a id="option-b-the-terminal-one-line"></a>
最初の画面で、アプリがPCを調べて合うモデルをすすめます。インストールを押してください。モデルは大きく(約40〜70 GB)、中断しても続きからダウンロードできます。
ウィンドウを閉じると、Strata とエンジンが止まります。
### ターミナルアプリ
### 方法B: ターミナル(1行)
ターミナルのほうが好みですか? Strata をインストールして、**`strata launch`** を実行します。
ターミナル版は `strata` コマンドです。スクリプトやサーバー、AI エージェント向けです。
<details open>
<summary><b>インストールコマンド</b></summary>
Linux と Mac:
**Linux / Mac**
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | 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
```
スクリプトはPCを調べ、ターミナル版をダウンロードし、SHA-256 を検証して、ホームフォルダに展開します。管理者権限は不要です。Windows では、終わったら
新しいターミナルを開いてください。そのあと Strata を起動します。
Windows では、インストール後に新しいターミナルを開きます。そのあと:
```sh
strata launch
```
質問は1つずつ表示されます。モデル、コンテキスト、画像、接続を許可する相手の順です。そのあと、お使いのグラフィックスカード用のエンジン
(署名付きで、使う前にテスト起動します)とモデルをダウンロードして、サーバーを開始します。スクリプトや AI エージェントからは、フラグを使います。
```sh
strata launch --model q2_0 --yes --no-tui
```
実行前にインストールスクリプトを読みたい場合は、先に `curl -fsSL <address> -o install.sh` でダウンロードし、内容を確認してから
`sh install.sh` を実行してください。すべての画面とフラグは [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md) にあります。
### 方法C: 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` は、メモリ量に合うモデルをすすめます。モデルは1つで、圧縮の度合いが違う複数のサイズがあります。
小さいサイズは速く、大きいサイズは少し賢くなります。
### メモリが少ないなら Gyro
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)(agentionai 製)は、このモデルをいちばん小さく動かせる方法です。
| サイズ | 必要なメモリ(RAM + VRAM) | ダウンロード | 内容 |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27.6 GB | 58.5 GB | ほかのサイズが入らないPC向け。品質は IQ2_XS より下がります。 |
| **Gyro-M** | 35 GB | 92.0 GB | 品質は IQ3_XXS 程度で、GPUメモリの使用量は約20%少なくなります。回答の書き出しは Q2_0 より、12 GBの RTX 3060 で16%、R9700 で21%遅くなります。 |
Gyro は NVIDIA と AMD のカード(AMD は実験的)、および Linux 上の Intel Arc カード(Arc A750 で確認)で動きます。0.2.2 からは Apple Silicon の Mac でも動きます(32 GB の Mac mini で Gyro-S は 9 トークン/秒、エキスパートは SSD から読み込み)。詳細と測定結果は
[docs/GYRO.md](docs/GYRO.md) にあります。`strata launch --model gyro-s` で起動します。
### ほかのサイズ
| メモリ(RAM + VRAM) | 選ぶモデル | 備考 |
| --- | --- | --- |
| **32 GB** | **Gyro-S**、またはコード向けの **[Coder](docs/MODELS.md#coder)** | Coder はRAM 32 GBが必要で、エキスパートを半分だけ保持するため、コード以外は弱くなります |
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS(39.2 GB)、または **Gyro-M**(35 GB) | Swift は答える前に考える時間が短くなります。Gyro-M は品質が IQ3_XXS に近いものの、回答の書き出しは Q2_0 より16〜21%遅くなります |
| **48 GB** | **IQ3_XXS**(47.0 GB) | 遅くなりますが、品質は上がります |
| **64 GB** | **IQ3_S**(54.8 GB)、オリジナルまたは Swift 1.5 | いちばん遅く、いちばん高品質です。公開されているテストでは完全版と同等です |
| **96 GB 以上** | Unsloth の **UD-IQ4_XS**(RAM 約72 GB)から **Q8_0**(約136 GB)まで、4〜8ビット | 完全版に近づきます。RAM が足りないと、Strata は一部を SSD から読むため、ずっと遅くなります |
表の行より RAM が少なくても動くことがあります。Strata は自動で[低RAMモード](docs/MODELS.md#a-big-graphics-card-and-little-ram)に切り替わります。
速度は落ち、グラフィックスカードが小さいとさらに大きく落ちます。
そのほかの選択肢:
- **[Swift 1.5](docs/MODELS.md#swift-15)** はファインチューン版で、答える前に考える時間が短くなります。品質と RAM の必要量はほぼ同じです。
- **[NVFP4](docs/NVFP4.md)** は NVIDIA カード向けの NVIDIA 製4ビット版です。すべてのエキスパートを RAM に置くには、約80 GBの RAM が必要です。
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** は、より小さい別のモデルです。画像を読め、複数のリクエストに同時に答えられます。
- **[OrcaRouter の Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** は手動で設定します。
すべてのサイズ、ダウンロード、どの構成で何が動くかは [docs/MODELS.md](docs/MODELS.md) にあります。
## どのくらい速い?
12 GBのグラフィックスカード(RTX 5070、Ryzen 5 7600、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 |
1トークンは英単語の約¾にあたるので、毎秒60トークンは読む速さより速くなります。「プロンプトを読む速さ」は、長い文書やコード、チャット履歴を Strata が取り込む速さです。
16 GBの AMD カード(RX 9070 XT、Ryzen 9 3900X、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 |
バーは2つの表で同じ目盛りです。VRAM が多いカードほど速くなり、RTX 3090(24 GB)なら毎秒100〜140トークンほどになるはずです。各数値のエンジンのバージョンは
[DETAILS.md](docs/DETAILS.md#speed-measured) にあります。
## 使い方
### アプリで
アプリには、**チャット**、モデルとGPU・CPU・RAMの状態を見る**モニター**、**モデル**ページ、**設定**があります。チャットはPC上のフォルダで作業できます。
**手動**モードでは、ファイルを変更したりコマンドを実行したりする前に確認します。`/loop 10m <やること>` と入力すると、チャットを開いている間、メッセージを10分ごとに繰り返し送ります。
チャットは DuckDuckGo(または Bing)でウェブ検索ができ、アカウントは不要です。**設定、ツール**で検索キーを追加することもできます。
検索は、PCの外に何かを送る唯一の機能で、送られるのは検索語です。
2つのモデルを動かすには、**複数のモデルを実行**をオンにして、それぞれのモデルに同じメーカーのカードを1枚ずつ割り当てます。
<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>Strata アプリでのチャット。エージェントが小さなボクセルの塔の庭を 1 つの HTML ファイルとして書き、そのページが会話の横のプレビューで開きます</sub></p>
### ターミナルで
<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>。速度、GPU、メモリ、最近のリクエストが表示されます</sub></p>
```sh
strata launch # ガイド付きの画面: モデル、コンテキスト、画像、アクセス、そして開始
strata launch --model q2_0 --yes --no-tui # 質問なし: 開始し、アドレスを表示して戻る
strata launch --plan --model q2_0 # 実行内容を表示するだけで、何も変更しない
strata launch --status # 今、何が動いているか
strata launch --stop # 停止する
strata monitor --url http://127.0.0.1:8080 # 速度、GPU、メモリ、リクエストを見る
strata update --check # 新しいエンジンがあるか確認(strata update でインストール)
strata doctor # インストール済みのエンジンファイルを調べる
```
`strata --help` ですべてのコマンドを確認できます。起動とサーバーの稼働では、何もダウンロードしません。ダウンロードするのは、インストール、
`strata launch`(モデルを選んだとき)、`strata update` だけです。スマートフォンや別のPCから Strata に接続するには、
`strata launch --access lan` を使います。API キーが作られ、キーがない状態ではネットワーク上で起動しません。`--open` を付けない限り、ブラウザは開きません。
ガイド: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md)。クライアント全体: [docs/TERMINAL.md](docs/TERMINAL.md)。
### AI エージェントと使う
エージェントは、方法Bの1行コマンドで Strata をインストールし、
`strata launch --model NAME --yes --no-tui` を実行します。手順、フラグ、終了コードは [docs/AI_SETUP.md](docs/AI_SETUP.md) に、
リポジトリのルールは [AGENTS.md](AGENTS.md) にあります。
### ほかのアプリをつなぐ
| アプリの種類 | ベース URL |
| --- | --- |
| OpenAI 互換(ほとんどのアプリとエージェント) | `http://127.0.0.1:8080/v1`(API キーもモデル名も何でも可) |
| Anthropic API(Claude Code など) | `http://127.0.0.1:8080`、`/v1` は付けません。これらのアプリは `/v1/messages` を自分で付けます。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` は、確認したうえでファイルを書き込み、バックアップを残します。
### 画像
画像を読めるモデルでは、画像は初期設定でオンです。チャットに画像を添付または貼り付けるか、アプリから送ってください。
グラフィックスカードが24 GB未満のときは、画像リーダーがプロセッサで動きます(画像1枚につき数秒、カードのメモリは使いません)。24 GB以上のときはグラフィックスカードで動きます。
このルールは NVIDIA と AMD で同じです。画像はモデルごとにオフにでき、`--vision off`、`--vision gpu`、`--vision cpu` でリーダーを選べます。
このルールの根拠となる数値です。Radeon AI PRO R9700 では、カードのリーダーは260トークンの画像に57 ms、プロセッサのリーダーは3.1秒かかりました。
16 GBのカード(Tesla P100 と RTX A4000、Linux)では、カードのリーダーを使うとテキストの速度が3〜5%下がりました。
本来モデルが使う約1.4 GBをリーダーが占めるためです。Mac ではメモリ量にかかわらず GPU が読み取ります(32 GB の M4 で260トークンの画像に0.45秒、共有メモリを約1.2 GB使います)。プロセッサでは1枚あたり約50秒かかるため、`--vision cpu` を指定したときだけ使います。
### 知っておくと便利なこと
- 思考のレベル(**off、low、medium、high**)は、チャットのメニューか、お使いのアプリの「reasoning effort」で選びます。off がいちばん速くなります。
- Strata は初期設定では、一度に1つのリクエストに答えます。同時に複数処理するには、`strata launch` で `--parallel N` を使うか、設定に
`"parallel": 2` を書きます([BATCHING.md](docs/BATCHING.md))。12 GBのカードでは、1つずつの回答が遅くなります。
- Strata はチャットの最初のメッセージを全文読みます。30,000トークンあたり約1分かかります。続きのメッセージは数秒で始まります。
- チャットの保存場所と API 全体: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored)、[docs/DETAILS.md](docs/DETAILS.md#using-it)。
## ソースからビルド
<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="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>
小さな助手が、次の数単語も予想します。大きなモデルがそれをまとめて確認し、正しいものだけを残します。答えは同じで、1.6〜1.8倍早く返ります。
長い文章は、一度に最大8,192トークンの大きな塊で、毎秒1,000トークンを超える速さで読みます。
## ハードウェア
<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>
| 対応ハードウェア | ガイド |
| --- | --- |
| **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` をもう一度起動するか、`START-HERE.bat`(または
`./setup.sh`)をもう一度実行してください。止まったところから再開します。
- **とても遅い、ディスクのランプが点滅し続ける、または「エンジンが予期せず停止しました」と表示される。** PCの空き RAM が足りません。
ほかのプログラムを閉じるか(ブラウザはメモリを多く使います)、Q2_0 や IQ2_XS のような小さいサイズを選んでください。
- **このPCに合うエンジンがないと表示される。** アプリでは、**設定、エンジン**を開いて**エンジンを更新**を押します。ターミナルでは
`strata update` を実行します。`strata doctor` で、どのエンジンファイルがインストールされているか分かります。
- **ポート 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` の出力を貼り付けます。ソースから動かしている場合は、Strata フォルダの `strata-<model>.log` を添付してください。
セキュリティ上の問題は、非公開で報告してください: [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 が行いました。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]).
+124 -332
View File
@@ -1,399 +1,191 @@
<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"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></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="#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/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">
<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://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>
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. It chats, writes code, reads pictures and works with your apps and coding
agents. Your chats and files stay on your PC.
<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></sub></p>
**New here?** Check [what you need](#what-you-need), then [install the app](#option-a-the-strata-app).
**Free, open source, and yours to run.** Use Strata on your desktop or with your favorite coding tools. No account needed.
- **Runs on a gaming PC.** 32 GB of RAM and graphics memory together is enough to start.
- **Works with your tools.** It speaks the OpenAI and Anthropic APIs, so Claude Code, Codex CLI, OpenCode and other agents connect to it.
- **Reads pictures.** They are on by default.
- **Free.** MIT license, no account, no cloud.
- **Private.** No telemetry. What Strata downloads, when it uses the internet and how it checks downloads is in [docs/SAFETY.md](docs/SAFETY.md).
## Models
Version 0.2.3 · engine 0.1.48
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"> &nbsp;<b>Qwen3.8-Flash-Next</b></summary>
**You need at least 32 GB of RAM and graphics memory (VRAM) together.** On a Mac or a Ryzen AI Max, that is the unified
memory. More memory gives a bigger model or a faster one.
- A 32 GB PC with a 12-16 GB card runs the Coder.
- 64 GB of RAM runs every size.
| Your computer | Details |
| Provider | Choices |
| --- | --- |
| **NVIDIA** GeForce RTX 20, 30, 40, 50 series (12 GB or more) | Install the app or the Terminal. |
| **AMD** Radeon RX 6800 and newer, Radeon AI PRO R9700 (12 GB or more) | [AMD cards](docs/AMD_HIP.md) |
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [Intel engine](docs/INTEL.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 systems | [Strix Halo](docs/STRIX_HALO.md) |
| Older cards and processors | [Older GPUs](docs/OLDER_GPUS.md), [Older CPUs](docs/INSTALL.md#older-cpus-experimental) |
| **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) |
Also needed:
[Compare sizes, memory needs and compatibility](docs/MODELS.md).
- about 80 GB of free disk (an SSD makes the first start much faster)
- Windows 10 / 11, Linux or macOS
- a current graphics driver
</details>
Two or three cards of the same make can share a model ([multi-GPU](docs/MULTI_GPU.md)). Strata never splits a model across
NVIDIA and AMD cards. Full list: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<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"> &nbsp;<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) and AMD Radeon AI PRO R9700 (Linux). 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. 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) |
[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
Pick one option. They all start the same engine and use the same models.
<a id="option-a-the-strata-app"></a>
### Option A: the Strata app
### Desktop app
The easiest way. The app has the engine inside: install it, pick a model, chat. Download the file for your system and
graphics card from the [releases page](https://github.com/Niko1221/Strata/releases/latest). Not sure which one? See the
line under the table.
**[Download Strata for Windows, Linux or Mac →](https://github.com/Niko1221/Strata/releases/latest)**
| System | Your graphics card | File to download |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX, driver 580 or newer | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
| | NVIDIA, older card or driver older than 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon RX 9000 series or Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon RX 7000 series or Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX, driver 580 or newer | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
| | NVIDIA, older card or driver older than 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon RX 9000 series or Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon RX 7000 series | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
| **Mac** (Apple Silicon, macOS 15 or newer) | built in | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
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).
**If you are unsure:** for NVIDIA, take `cuda13` if your driver is version 580 or newer, otherwise `cuda12`. `cuda12` is
for older cards and older drivers. The `hip` and `amd` files are for AMD cards. RX 6000 cards have no installer of
their own: install `hip-rdna3` (Windows) or `amd-rx7900` (Linux), and on a PC with an RX 6000 card it fetches the RX 6000 engine by itself
(also `strata update`). That engine is built but not yet tested on a real RX 6000 card: see [AMD cards](docs/AMD_HIP.md).
<sub>Files: `Strata-0.2.6-windows-x64-setup.exe` · `Strata-0.2.6-linux-x86_64.AppImage` · `Strata-0.2.6-macos-arm64.dmg`</sub>
Then:
<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>
- **Windows:** run the `.exe`. It installs for your user and needs no administrator rights. If Windows says "Windows
protected your PC", choose **More info**, then **Run anyway** (the installer is not code-signed yet).
- **Linux:** make the file runnable (`chmod +x`) and start it. It needs FUSE 2: `sudo apt install libfuse2` (Ubuntu 22.04)
or `libfuse2t64` (Ubuntu 24.04 and newer).
- **Mac:** open the `.dmg` and drag Strata onto Applications.
<a id="option-b-the-terminal-one-line"></a>
On the first screen the app reads your PC and recommends a model that fits. Press install. The model is a big download
(about 40-70 GB) and continues where it stopped if it is interrupted. Closing the window stops Strata and the engine.
### Terminal app
### Option B: the Terminal (one line)
Prefer the terminal? Install Strata, then run **`strata launch`**.
The Terminal is the `strata` command, for scripts, servers and AI agents.
<details open>
<summary><b>Install commands</b></summary>
Linux and Mac:
**Linux / Mac**
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | 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
```
The script checks your PC, downloads the Terminal, verifies its SHA-256 and unpacks it into your home folder. It needs no
administrator rights. On Windows, open a new terminal window afterwards. Then start Strata:
On Windows, open a new terminal afterwards. Then:
```sh
strata launch
```
It asks one question at a time: model, context, pictures, who may connect. Then it downloads the engine for your graphics
card (signed, and test-started before use) and the model, and starts serving. For scripts and AI agents, use flags:
```sh
strata launch --model q2_0 --yes --no-tui
```
To read the install script before it runs, download it first with `curl -fsSL <address> -o install.sh`, read it, then run
`sh install.sh`. All screens and flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
### Option C: 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 and 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 model for your memory. It is one model in several sizes, compressed more or less.
Smaller sizes are faster. Larger sizes are a bit smarter.
### Low on memory? Try Gyro
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (by agentionai) is the smallest way to run this model.
| Size | Memory it needs (RAM + VRAM) | Download | What it is |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27.6 GB | 58.5 GB | For PCs that fit nothing else. Quality is lower than IQ2_XS. |
| **Gyro-M** | 35 GB | 92.0 GB | About IQ3_XXS quality with about 20% less GPU memory. It writes answers 16% (12 GB RTX 3060) to 21% (R9700) slower than Q2_0. |
Gyro runs on NVIDIA and AMD cards (AMD is experimental) on Intel Arc cards on Linux (tested on an Arc A750) and, from 0.2.2, on Apple Silicon Macs (Gyro-S on a 32 GB Mac mini: 9 tok/s, streaming from the SSD; [docs/MAC.md](docs/MAC.md#gyro-s-and-gyro-m-agentionais-trellis-experts)). Details and measurements:
[docs/GYRO.md](docs/GYRO.md). Start it with `strata launch --model gyro-s`.
### The other sizes
The tiers count your RAM plus your VRAM, less 5 GB that Windows and the system keep for themselves. Every other size stays a choice.
| Your memory (RAM + VRAM) | Take | Notes |
| --- | --- | --- |
| **32 GB** | **Gyro-S**, or the **[Coder](docs/MODELS.md#coder)** for code | The Coder needs 32 GB of RAM and keeps half of the experts, so it is weaker outside code |
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39.2 GB), or **Gyro-M** (35 GB) | Swift thinks for a shorter time before it answers. Gyro-M is close to IQ3_XXS in quality, but it writes answers 16-21% slower than Q2_0 |
| **48 GB** | **IQ3_XXS** (47.0 GB) | Slower, better |
| **64 GB** | **IQ3_S** (54.8 GB), the original or Swift 1.5 | The slowest, the best: it matches the full model on the published tests |
| **96 GB or more** | Unsloth's **UD-IQ4_XS** (about 72 GB of RAM) to **Q8_0** (about 136 GB), 4 to 8 bit | Closer to the full model. With less RAM, Strata reads part of them from the SSD, which is much slower |
Less RAM than a row says can still work: Strata switches to its
[low-RAM mode](docs/MODELS.md#a-big-graphics-card-and-little-ram) by itself. Expect it to be slower, and much slower
with a small graphics card.
More choices:
- **[Swift 1.5](docs/MODELS.md#swift-15)** is a fine-tune that thinks for a shorter time before it answers, at about the same quality and RAM.
- **[NVFP4](docs/NVFP4.md)** is NVIDIA's 4-bit version for NVIDIA cards. It needs about 80 GB of RAM to keep every expert in RAM.
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** is a smaller, different model that reads pictures and can answer several requests at once.
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** is set up by hand.
Every size, download and what fits where: [docs/MODELS.md](docs/MODELS.md).
## How fast is it?
Based on a 12 GB graphics card (RTX 5070, Ryzen 5 7600, 64 GB RAM). See results from other cards:
[speed of each model](docs/MODELS.md#how-fast-is-each-size), [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 |
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.
Measured on a 16 GB AMD card (RX 9070 XT, Ryzen 9 3900X, 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 |
The bars use one scale for both tables. A card with more VRAM is faster: an RTX 3090 (24 GB) should write about 100-140
tokens per second. The engine version behind each number is in [DETAILS.md](docs/DETAILS.md#speed-measured).
## Using it
### In the app
The app has a **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. Type
`/loop 10m <what to do>` to repeat a message every 10 minutes while the chat is open.
The chat can search the web with DuckDuckGo (or Bing), with no account. You can add a search key under **Settings, Tools**.
Search is the one feature that sends something off your PC, the search words.
To run two models, turn on **Run multiple models** and give each model its own card of the same make.
<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>A chat in the Strata app: the agent wrote a small voxel pagoda garden as one HTML file, and the page opens in the preview next to the conversation</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 screens: model, context, pictures, access, then serve
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
strata launch --plan --model q2_0 # show what would happen; nothing changes
strata launch --status # what is being served now
strata launch --stop # stop it
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
strata update --check # is there a new engine? (strata update installs it)
strata doctor # check the installed engine files
```
`strata --help` lists everything. Starting and serving never download anything. Only the install, `strata launch` (when
you choose a model) and `strata update` do. To reach Strata from your phone or another PC, use
`strata launch --access lan`. It makes an API key (recommended; add `--no-api-key` to go without, with a warning). No browser opens unless
you add `--open`. Guide: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Whole client: [docs/TERMINAL.md](docs/TERMINAL.md).
### With AI agents
An agent installs Strata with the one-line command in Option B and runs
`strata launch --model NAME --yes --no-tui`. The steps, flags and exit codes are in [docs/AI_SETUP.md](docs/AI_SETUP.md),
and the repository rules are in [AGENTS.md](AGENTS.md).
### Connect your other apps
| App type | Base URL |
| --- | --- |
| OpenAI-compatible (most apps and agents) | `http://127.0.0.1:8080/v1` (any API key, any model name) |
| Anthropic API (Claude Code and others) | `http://127.0.0.1:8080`, without `/v1`. These apps add `/v1/messages` themselves. 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.
`strata connect opencode --write` writes the file after asking and keeps a backup.
### Pictures
Pictures are on by default for models that can read them. Attach or paste a picture in the chat, or send it from your app.
The picture reader runs on the processor when your graphics card has less than 24 GB (a few seconds per picture, nothing
taken from the card's memory) and on the graphics card from 24 GB. The rule is the same for NVIDIA and AMD. Turn pictures
off per model, or choose the reader with `--vision off`, `--vision gpu` or `--vision cpu`.
The numbers behind the rule: on a Radeon AI PRO R9700 the card's reader took 57 ms for a 260-token picture and the
processor's took 3.1 s. On 16 GB cards (Tesla P100 and RTX A4000, Linux) the card's reader cost 3-5% of text speed,
because it holds about 1.4 GB that the model would use otherwise. On a Mac the reader runs on the Mac's GPU whatever its memory (0.45 s for a 260-token picture on an M4 with 32 GB, about 1.2 GB of the shared
memory); the processor reader needs about 50 s per picture there, so it runs only when you ask for it with `--vision cpu`.
### Good to know
- Choose the thinking level (**off, low, medium or high**) in the chat menu or in your app's "reasoning effort". Off is fastest.
- Strata answers one request at a time by default. For several at once, use `--parallel N` in `strata launch` or
`"parallel": 2` in the config ([BATCHING.md](docs/BATCHING.md)). On a 12 GB card this makes each answer slower.
- Strata reads the first message of a chat in full, about 1 minute per 30,000 tokens. Follow-up messages start in seconds.
- Where your chats are stored and the whole API: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
## 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, or run `START-HERE.bat` (or
`./setup.sh`) again. It continues where it stopped.
- **It is very slow, the disk light keeps blinking, or it says "the engine stopped unexpectedly".** Your PC does not have
enough 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`. `strata doctor` shows which engine files are installed.
- **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 problems and fixes: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Still stuck? Open an
[issue](https://github.com/Niko1221/Strata/issues). In the app, attach the file from **Settings, Engine, Diagnostics, View
logs, Save logs**. In the Terminal, paste the output of `strata doctor`. From source, attach `strata-<model>.log` from the
Strata folder. 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. 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>
Interested in sponsoring Strata? Contact me via [GitHub](https://github.com/Niko1221).
[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]).
+121 -327
View File
@@ -1,394 +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"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></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="#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/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>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">
<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://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>
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. 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.
<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></sub></p>
**Primeira vez aqui?** Veja [o que você precisa](#o-que-você-precisa) e depois [instale o app](#opção-a-o-app-strata).
**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.
- **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.** Ele fala as APIs da OpenAI e da Anthropic, então Claude Code, Codex CLI, OpenCode e outros agentes se conectam a ele.
- **Lê imagens.** Já vêm ativadas.
- **Gratuito.** Licença MIT, sem conta, sem nuvem.
- **Privado.** Sem telemetria. O que o Strata baixa, quando usa a internet e como confere os downloads está em [docs/SAFETY.md](docs/SAFETY.md).
## Modelos
Versão 0.2.3 · mecanismo 0.1.48
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"> &nbsp;<b>Qwen3.8-Flash-Next</b></summary>
**Você precisa de pelo menos 32 GB somando a memória RAM e a memória da placa de vídeo (VRAM).** Num Mac ou num Ryzen AI Max, isso é a
memória unificada. Mais memória permite um modelo maior ou mais rápido.
- Um PC de 32 GB com uma placa de 12 a 16 GB roda o Coder.
- 64 GB de RAM rodam todos os tamanhos.
| Seu computador | Detalhes |
| Provedor | Opções |
| --- | --- |
| **NVIDIA** GeForce RTX série 20, 30, 40, 50 (12 GB ou mais) | Instale o app ou o Terminal. |
| **AMD** Radeon RX 6800 e mais novas, Radeon AI PRO R9700 (12 GB ou mais) | [Placas AMD](docs/AMD_HIP.md) |
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [mecanismo Intel](docs/INTEL.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 sistemas de memória unificada | [Strix Halo](docs/STRIX_HALO.md) |
| Placas e processadores mais antigos | [GPUs antigas](docs/OLDER_GPUS.md), [CPUs antigas](docs/INSTALL.md#older-cpus-experimental) |
| **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:
[Compare tamanhos, necessidades de memória e compatibilidade](docs/MODELS.md).
- cerca de 80 GB de espaço livre no disco (um SSD deixa a primeira inicialização bem mais rápida)
- Windows 10 / 11, Linux ou macOS
- um driver de vídeo atualizado
</details>
Duas ou três placas da mesma marca podem dividir um modelo ([multi-GPU](docs/MULTI_GPU.md)). O Strata nunca divide um modelo entre
placas NVIDIA e AMD. Lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<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"> &nbsp;<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
Escolha uma opção. Todas iniciam o mesmo mecanismo e usam os mesmos modelos.
<a id="option-a-the-strata-app"></a>
### Opção A: o app Strata
### Aplicativo de desktop
O jeito mais fácil. O app já traz o mecanismo: instale, escolha um modelo e converse. Baixe o arquivo para o seu sistema e a sua
placa de vídeo na [página de releases](https://github.com/Niko1221/Strata/releases/latest). Não sabe qual? Veja a explicação
logo abaixo da tabela.
**[Baixar o Strata para Windows, Linux ou Mac →](https://github.com/Niko1221/Strata/releases/latest)**
| Sistema | Sua placa de vídeo | Arquivo para baixar |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX, driver 580 ou mais novo | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
| | NVIDIA, placa mais antiga ou driver anterior ao 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon série RX 7000 ou Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX, driver 580 ou mais novo | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
| | NVIDIA, placa mais antiga ou driver anterior ao 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon série RX 7000 | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
| **Mac** (Apple Silicon, macOS 15 ou mais novo) | integrada | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
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).
**Se você estiver em dúvida:** para NVIDIA, use `cuda13` se o seu driver for a versão 580 ou mais nova, e `cuda12` nos outros casos. O `cuda12`
serve para placas e drivers mais antigos. Os arquivos `hip` e `amd` são para placas AMD. Outras placas AMD (RX 6000) ainda não têm app pronto: veja [Placas AMD](docs/AMD_HIP.md).
<sub>Arquivos: `Strata-0.2.6-windows-x64-setup.exe` · `Strata-0.2.6-linux-x86_64.AppImage` · `Strata-0.2.6-macos-arm64.dmg`</sub>
Depois:
<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>
- **Windows:** execute o `.exe`. Ele instala só para o seu usuário e não pede direitos de administrador. Se o Windows disser "O Windows
protegeu o computador", escolha **Mais informações** e depois **Executar assim mesmo** (o instalador ainda não tem assinatura de código).
- **Linux:** torne o arquivo executável (`chmod +x`) e inicie. É preciso ter o FUSE 2: `sudo apt install libfuse2` (Ubuntu 22.04)
ou `libfuse2t64` (Ubuntu 24.04 e mais novos).
- **Mac:** abra o `.dmg` e arraste o Strata para Aplicativos.
<a id="option-b-the-terminal-one-line"></a>
Na primeira tela, o app lê o seu PC e recomenda um modelo que caiba nele. Clique em instalar. O modelo é um download grande
(cerca de 40 a 70 GB) e continua de onde parou se for interrompido. Fechar a janela encerra o Strata e o mecanismo.
### Aplicativo de Terminal
### Opção B: o Terminal (uma linha)
Prefere o terminal? Instale o Strata e execute **`strata launch`**.
O Terminal é o comando `strata`, para scripts, servidores e agentes de IA.
<details open>
<summary><b>Comandos de instalação</b></summary>
Linux e Mac:
**Linux / Mac**
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | 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
```
O script confere o seu PC, baixa o Terminal, verifica o SHA-256 e descompacta na sua pasta pessoal. Não precisa de direitos de
administrador. No Windows, abra uma nova janela do terminal depois. Então inicie o Strata:
No Windows, abra um novo terminal depois. Em seguida:
```sh
strata launch
```
Ele faz uma pergunta por vez: modelo, contexto, imagens, quem pode se conectar. Depois baixa o mecanismo para a sua placa de vídeo
(assinada e testada antes do uso) e o modelo, e começa a servir. Em scripts e agentes de IA, use flags:
```sh
strata launch --model q2_0 --yes --no-tui
```
Para ler o script de instalação antes de executá-lo, baixe-o primeiro com `curl -fsSL <address> -o install.sh`, leia e então rode
`sh install.sh`. Todas as telas e flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
### Opção C: 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 modelo para a sua memória. É um único modelo em vários tamanhos, mais ou menos comprimidos.
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 este modelo.
| Tamanho | Memória necessária (RAM + VRAM) | Download | O que é |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27,6 GB | 58,5 GB | Para PCs onde nenhum outro tamanho cabe. A qualidade é menor que a do IQ2_XS. |
| **Gyro-M** | 35 GB | 92,0 GB | Qualidade próxima à do IQ3_XXS, com cerca de 20% menos memória da GPU. Ele escreve as respostas 16% mais devagar que o Q2_0 numa RTX 3060 de 12 GB e 21% mais devagar numa R9700. |
O Gyro roda em placas NVIDIA e AMD (AMD é experimental) em placas Intel Arc no Linux (testado numa Arc A750) e, desde a 0.2.2, em Macs com Apple Silicon (Gyro-S num Mac mini de 32 GB: 9 tokens/s, com os especialistas lidos do SSD). Detalhes e medições:
[docs/GYRO.md](docs/GYRO.md). Inicie com `strata launch --model gyro-s`.
### Os outros tamanhos
| Sua memória (RAM + VRAM) | Escolha | Observações |
| --- | --- | --- |
| **32 GB** | **Gyro-S**, ou o **[Coder](docs/MODELS.md#coder)** para código | O Coder precisa de 32 GB de RAM e mantém metade dos experts, então é mais fraco fora de código |
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), ou **Gyro-M** (35 GB) | O Swift pensa por menos tempo antes de responder. O Gyro-M chega perto do IQ3_XXS em qualidade, mas escreve as respostas 16 a 21% mais devagar que o Q2_0 |
| **48 GB** | **IQ3_XXS** (47,0 GB) | Mais lento, melhor |
| **64 GB** | **IQ3_S** (54,8 GB), o original ou o Swift 1.5 | O mais lento, o melhor: empata com o modelo completo nos testes publicados |
| **96 GB ou mais** | **UD-IQ4_XS** da Unsloth (cerca de 72 GB de RAM) até **Q8_0** (cerca de 136 GB), de 4 a 8 bits | Mais perto do modelo completo. Com menos RAM, o Strata lê parte deles do SSD, o que é bem mais lento |
Com menos RAM do que a linha indica, ainda pode funcionar: o Strata muda sozinho para o
[modo de pouca RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Espere mais lentidão, e muito mais com uma placa de vídeo pequena.
Mais opções:
- **[Swift 1.5](docs/MODELS.md#swift-15)** é um ajuste fino que pensa por menos tempo antes de responder, com qualidade e RAM parecidas.
- **[NVFP4](docs/NVFP4.md)** é a versão de 4 bits da NVIDIA para placas NVIDIA. Precisa de cerca de 80 GB de RAM para manter todos os experts na RAM.
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** é outro modelo, menor, que lê imagens e pode atender vários pedidos ao mesmo tempo.
- **[Uncensored IQ3_XXS da OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** é configurado manualmente.
Todos os tamanhos, downloads e o que cabe em cada configuração: [docs/MODELS.md](docs/MODELS.md).
## Qual é a velocidade?
Com base numa placa de vídeo de 12 GB (RTX 5070, Ryzen 5 7600, 64 GB de RAM). Veja resultados de outras placas:
[velocidade de cada modelo](docs/MODELS.md#how-fast-is-each-size), [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 |
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.
Medido numa placa AMD de 16 GB (RX 9070 XT, Ryzen 9 3900X, 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 |
As barras usam a mesma escala nas duas tabelas. Uma placa com mais VRAM é mais rápida: uma RTX 3090 (24 GB) deve escrever cerca de 100 a 140
tokens por segundo. A versão do mecanismo por trás de cada número está em [DETAILS.md](docs/DETAILS.md#speed-measured).
## Como usar
### No app
O app tem 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. Digite
`/loop 10m <o que fazer>` para repetir uma mensagem a cada 10 minutos enquanto o chat estiver aberto.
O chat pode pesquisar na web com o DuckDuckGo (ou o Bing), sem conta. Você pode adicionar uma chave de busca em **Configurações, Ferramentas**.
A busca é o único recurso que envia algo para fora do seu PC: os termos da pesquisa.
Para rodar dois modelos, ative **Executar vários modelos** e dê a cada modelo a sua própria placa da mesma marca.
<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>Um chat no app Strata: o agente escreveu um pequeno jardim de pagode em voxels em um único arquivo HTML, e a página abre na pré-visualização ao lado da conversa</sub></p>
### No 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> enquanto o modelo responde: velocidade, GPU, memória e as solicitações recentes</sub></p>
```sh
strata launch # telas guiadas: modelo, contexto, imagens, acesso e depois servir
strata launch --model q2_0 --yes --no-tui # sem perguntas: serve, mostra o endereço e volta
strata launch --plan --model q2_0 # mostra o que aconteceria; nada muda
strata launch --status # o que está sendo servido agora
strata launch --stop # para o serviço
strata monitor --url http://127.0.0.1:8080 # acompanha velocidade, GPU, memória e requisições
strata update --check # há um mecanismo novo? (strata update instala)
strata doctor # confere os arquivos do mecanismo instalada
```
`strata --help` lista tudo. Iniciar e servir nunca baixam nada. Só a instalação, o `strata launch` (quando você
escolhe um modelo) e o `strata update` baixam. Para acessar o Strata do celular ou de outro PC, use
`strata launch --access lan`. Ele cria uma chave de API e não inicia numa rede sem uma. Nenhum navegador abre, a menos que
você adicione `--open`. Guia: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Cliente completo: [docs/TERMINAL.md](docs/TERMINAL.md).
### Com agentes de IA
Um agente instala o Strata com o comando de uma linha da Opção B 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),
e as regras do repositório estão em [AGENTS.md](AGENTS.md).
### Conecte os seus outros aplicativos
| Tipo de aplicativo | URL base |
| --- | --- |
| Compatível com 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 e outros) | `http://127.0.0.1:8080`, sem `/v1`. Esses aplicativos adicionam `/v1/messages` sozinhos. 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.
`strata connect opencode --write` grava o arquivo depois de perguntar e mantém um backup.
### Imagens
As imagens vêm ativadas nos modelos que sabem lê-las. Anexe ou cole uma imagem no chat, ou envie pelo seu aplicativo.
O leitor de imagens roda no processador quando a sua placa de vídeo tem menos de 24 GB (alguns segundos por imagem, sem tirar nada
da memória da placa) e roda na placa de vídeo a partir de 24 GB. A regra é a mesma para NVIDIA e AMD. Desative as imagens
por modelo, ou escolha o leitor com `--vision off`, `--vision gpu` ou `--vision cpu`.
Os números por trás da regra: numa Radeon AI PRO R9700, o leitor da placa levou 57 ms para uma imagem de 260 tokens e o do
processador levou 3,1 s. Em placas de 16 GB (Tesla P100 e RTX A4000, Linux), o leitor da placa custou de 3 a 5% da velocidade do texto,
porque ocupa cerca de 1,4 GB que o modelo usaria de outra forma. No Mac, a GPU faz a leitura, seja qual for a memória (0,45 s para uma imagem de 260 tokens em um M4 de 32 GB, cerca de 1,2 GB da memória compartilhada);
o processador leva cerca de 50 s por imagem, então só lê com `--vision cpu`.
### Bom saber
- Escolha o nível de raciocínio (**off, low, medium ou high**) no menu do chat ou no "reasoning effort" do seu aplicativo. Off é o mais rápido.
- Por padrão, o Strata responde a um pedido por vez. Para vários ao mesmo tempo, use `--parallel N` no `strata launch` ou
`"parallel": 2` na configuração ([BATCHING.md](docs/BATCHING.md)). Numa placa de 12 GB, isso deixa cada resposta mais lenta.
- O Strata lê a primeira mensagem de uma conversa por inteiro, cerca de 1 minuto a cada 30.000 tokens. As mensagens seguintes começam em segundos.
- Onde ficam as suas conversas e a API completa: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
## 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="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>
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="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></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, ou execute `START-HERE.bat` (ou
`./setup.sh`) outra vez. Ele continua de onde parou.
- **Está muito lento, a luz do disco não para de piscar, ou aparece "o mecanismo parou inesperadamente".** 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`. O `strata doctor` mostra quais arquivos do mecanismo estão instalados.
- **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 problemas e soluções: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Ainda sem resolver? Abra uma
[issue](https://github.com/Niko1221/Strata/issues). No app, anexe o arquivo de **Configurações, Mecanismo, Diagnóstico, Ver
registros, Salvar logs**. No Terminal, cole a saída de `strata doctor`. Se estiver rodando pelo código-fonte, anexe o `strata-<model>.log` da pasta do
Strata. 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. 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]).
+121 -314
View File
@@ -1,381 +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"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></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="#模型">模型</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/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>一座体素宝塔花园,由一条提示词一次生成,在装有 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">
<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://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>
Strata 运行 **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**。这是一个 1250 亿参数的模型,
通常需要服务器才能跑,现在用普通电脑就行。它能聊天、写代码、看图片,也能配合你的应用和编程智能体使用。
你的聊天记录和文件都留在你自己的电脑上。
<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></sub></p>
**第一次用?** 先看[硬件要求](#硬件要求),再[安装应用](#方式-astrata-应用)。
**免费、开源,由你自己运行。** 在桌面上使用 Strata,或搭配你喜欢的编程工具。无需账号。
- **游戏电脑就能跑。** 内存和显存加起来有 32 GB 就可以开始。
- **能接入你的工具。** 支持 OpenAI 和 Anthropic 的 API,Claude Code、Codex CLI、OpenCode 等智能体都能连上。
- **能看图片。** 默认开启。
- **免费。** MIT 许可证,不用注册账号,不走云端。
- **隐私。** 没有遥测。Strata 下载什么、什么时候联网、如何校验下载内容,都写在 [docs/SAFETY.md](docs/SAFETY.md)。
## 模型
版本 0.2.3 · 引擎 0.1.48
我们**首先推荐 ISTA-DASLab**,其次是 **Unsloth** 和其他提供方,前提是有内存放得下的兼容模型。
## 硬件要求
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<b>Qwen3.8-Flash-Next</b></summary>
**内存和显存(VRAM)加起来至少要有 32 GB。** Mac 和 Ryzen AI Max 上指的是统一内存。内存越大,能跑的模型越大,
或者速度越快。
- 32 GB 内存加一张 12-16 GB 显存的显卡,可以跑 Coder。
- 64 GB 内存可以跑所有尺寸。
| 你的电脑 | 说明 |
| 提供方 | 可选项 |
| --- | --- |
| **NVIDIA** GeForce RTX 20、30、40、50 系列(12 GB 及以上) | 安装应用或终端版即可。 |
| **AMD** Radeon RX 6800 及更新型号、Radeon AI PRO R9700(12 GB 及以上) | [AMD 显卡](docs/AMD_HIP.md) |
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md)、[Intel 引擎](docs/INTEL.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)、[旧处理器](docs/INSTALL.md#older-cpus-experimental) |
| **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) |
另外还需要:
[比较大小、内存需求和兼容性](docs/MODELS.md)。
- 约 80 GB 的可用磁盘空间(用固态硬盘,首次启动会快很多)
- Windows 10 / 11、Linux 或 macOS
- 较新的显卡驱动
</details>
同一品牌的两三张显卡可以共同运行一个模型([多显卡](docs/MULTI_GPU.md))。Strata 不会把一个模型拆到 NVIDIA 和 AMD
显卡上。完整列表见 [docs/INSTALL.md](docs/INSTALL.md#what-you-need)。
<details>
<summary><img src="docs/media/models/qwen.svg" width="22" alt="" align="middle"> &nbsp;<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"> &nbsp;<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)。
## 安装
三种方式任选一种。它们启动的是同一个引擎,用的是同样的模型。
<a id="option-a-the-strata-app"></a>
### 方式 A:Strata 应用
### 桌面应用
最简单的方式。应用内置引擎:安装、选模型、开始聊天。请根据你的系统和显卡,从
[发布页](https://github.com/Niko1221/Strata/releases/latest)下载对应的文件。不确定选哪个?看表格下面那一段。
**[下载适用于 Windows、Linux 或 Mac 的 Strata →](https://github.com/Niko1221/Strata/releases/latest)**
| 系统 | 你的显卡 | 要下载的文件 |
| --- | --- | --- |
| **Windows 10 / 11** | NVIDIA RTX,驱动 580 或更新 | [`Strata-0.2.3-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda13.exe) |
| | NVIDIA,较旧的显卡,或驱动低于 580 | [`Strata-0.2.3-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-cuda12.exe) |
| | AMD Radeon RX 9000 系列或 Radeon AI PRO R9700 | [`Strata-0.2.3-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip.exe) |
| | AMD Radeon RX 7000 系列或 Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-windows-hip-rdna3.exe) |
| **Linux** | NVIDIA RTX,驱动 580 或更新 | [`Strata-0.2.3-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda13.AppImage) |
| | NVIDIA,较旧的显卡,或驱动低于 580 | [`Strata-0.2.3-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-nvidia-cuda12.AppImage) |
| | AMD Radeon RX 9000 系列或 Radeon AI PRO R9700 | [`Strata-0.2.3-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx9070.AppImage) |
| | AMD Radeon RX 7000 系列 | [`Strata-0.2.3-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-rx7900.AppImage) |
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.3-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-linux-amd-strixhalo.AppImage) |
| **Mac**(Apple 芯片,macOS 15 及以上) | 内置 | [`Strata-0.2.3-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.3-mac.dmg) |
安装应用,选择模型,就可以开始聊天。每个系统只有一个安装程序。首次启动时,应用会下载适合你显卡的引擎。[安装程序帮助](docs/INSTALL.md#the-app)。
**不确定时:** NVIDIA 显卡的驱动版本在 580 及以上就选 `cuda13`,否则选 `cuda12`。`cuda12` 适用于较旧的显卡和驱动。
`hip` 和 `amd` 的文件是给 AMD 显卡用的。其他 AMD 显卡(RX 6000)目前还没有现成的应用,
请看 [AMD 显卡](docs/AMD_HIP.md)。
<sub>文件: `Strata-0.2.6-windows-x64-setup.exe` · `Strata-0.2.6-linux-x86_64.AppImage` · `Strata-0.2.6-macos-arm64.dmg`</sub>
然后:
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="Strata 桌面应用,聊天和实时代码预览并排显示">
<br><sub>聊天、文件和实时预览都在一个窗口里。</sub>
</p>
- **Windows:** 运行 `.exe`。它只为当前用户安装,不需要管理员权限。如果 Windows 提示“Windows 已保护你的电脑”,
点击**更多信息**,再点**仍要运行**(安装程序还没有代码签名)。
- **Linux:** 给文件加上可执行权限(`chmod +x`),然后启动。需要 FUSE 2:`sudo apt install libfuse2`(Ubuntu 22.04),
或 `libfuse2t64`(Ubuntu 24.04 及更新)。
- **Mac:** 打开 `.dmg`,把 Strata 拖到“应用程序”。
<a id="option-b-the-terminal-one-line"></a>
第一个界面里,应用会检测你的电脑,并推荐合适的模型。点击安装即可。模型很大,要下载约 40-70 GB,中断后会从断点继续。
关闭窗口会同时停止 Strata 和引擎。
### 终端应用
### 方式 B:终端(一行命令)
更喜欢终端?安装 Strata,然后运行 **`strata launch`**。
终端版就是 `strata` 命令,适合脚本、服务器和 AI 智能体。
<details open>
<summary><b>安装命令</b></summary>
Linux 和 Mac:
**Linux / Mac**
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | 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
```
脚本会检查你的电脑,下载终端版,校验 SHA-256,然后解压到你的用户目录。不需要管理员权限。在 Windows 上,装完后请
打开一个新的终端窗口。然后启动 Strata:
在 Windows 上,安装后请打开一个新的终端。然后运行:
```sh
strata launch
```
它会一次问一个问题:模型、上下文、图片、允许谁连接。接着为你的显卡下载引擎(已签名,使用前会先试启动),再下载
模型,并开始提供服务。在脚本和 AI 智能体里,请使用参数:
```sh
strata launch --model q2_0 --yes --no-tui
```
如果想在运行前先看看安装脚本,先用 `curl -fsSL <address> -o install.sh` 下载,读完后再用 `sh install.sh` 运行。
所有界面和参数见 [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md)。
### 方式 C:让你的 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)是运行这个模型占用最小的方式。
| 尺寸 | 所需内存(RAM + VRAM) | 下载大小 | 说明 |
| --- | ---: | ---: | --- |
| **Gyro-S** | 27.6 GB | 58.5 GB | 给其他尺寸都放不下的电脑。质量低于 IQ2_XS。 |
| **Gyro-M** | 35 GB | 92.0 GB | 质量约相当于 IQ3_XXS,显存占用少约 20%。输出回答比 Q2_0 慢,12 GB 的 RTX 3060 上慢 16%,R9700 上慢 21%。 |
Gyro 可在 NVIDIA 和 AMD 显卡(AMD 为实验性支持)以及 Linux 上的 Intel Arc 显卡(已在 Arc A750 上测试)上运行,自 0.2.2 起也可在 Apple Silicon 的 Mac 上运行(32 GB 的 Mac mini 上 Gyro-S 为 9 token/秒,专家权重从 SSD 读取)。详情和测试数据:
[docs/GYRO.md](docs/GYRO.md)。用 `strata launch --model gyro-s` 启动。
### 其他尺寸
| 你的内存(RAM + VRAM) | 选择 | 备注 |
| --- | --- | --- |
| **32 GB** | **Gyro-S**,写代码可选 **[Coder](docs/MODELS.md#coder)** | Coder 需要 32 GB 内存,只保留一半的专家,所以代码以外的能力较弱 |
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS(39.2 GB),或 **Gyro-M**(35 GB) | Swift 回答前思考的时间更短。Gyro-M 的质量接近 IQ3_XXS,但输出回答比 Q2_0 慢 16-21% |
| **48 GB** | **IQ3_XXS**(47.0 GB) | 更慢,更好 |
| **64 GB** | **IQ3_S**(54.8 GB),原版或 Swift 1.5 | 最慢,最好:在公开测试中与完整模型持平 |
| **96 GB 及以上** | Unsloth 的 **UD-IQ4_XS**(约 72 GB 内存)到 **Q8_0**(约 136 GB),4 到 8 位 | 更接近完整模型。内存不足时,Strata 会从固态硬盘读取一部分,速度会慢很多 |
内存比这一行写的少,也可能能用:Strata 会自动切换到[低内存模式](docs/MODELS.md#a-big-graphics-card-and-little-ram)。
速度会变慢,显卡较小时会慢得多。
更多选择:
- **[Swift 1.5](docs/MODELS.md#swift-15)** 是微调版本,回答前思考的时间更短,质量和内存占用大致相同。
- **[NVFP4](docs/NVFP4.md)** 是 NVIDIA 为 NVIDIA 显卡提供的 4 位版本。要把所有专家放进内存,需要约 80 GB 内存。
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** 是另一个更小的模型,能看图片,还能同时回答多个请求。
- **[OrcaRouter 的 Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** 需要手动设置。
所有尺寸、下载大小,以及各种配置能跑什么:[docs/MODELS.md](docs/MODELS.md)。
## 速度如何?
以下数据基于 12 GB 显卡(RTX 5070、Ryzen 5 7600、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 |
一个 token 约等于 ¾ 个英文单词,所以每秒 60 个 token 比你阅读的速度还快。“读取提示词”指 Strata 吸收长文档、代码或聊天历史的速度。
在 16 GB 的 AMD 显卡上测得的结果(RX 9070 XT、Ryzen 9 3900X、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 |
两张表的进度条使用同一个比例。显存更大的显卡更快:RTX 3090(24 GB)每秒大约能输出 100-140 个 token。每个数字对应的引擎版本见
[DETAILS.md](docs/DETAILS.md#speed-measured)。
## 使用方法
### 在应用里
应用包含**聊天**、显示模型以及 GPU、CPU 和内存状态的**监控**、**模型**页面和**设置**。聊天可以在你电脑上的某个文件夹里工作。
在**手动**模式下,修改文件或运行命令之前它会先询问你。输入 `/loop 10m <要做的事>`,在聊天保持打开期间每 10 分钟重复发送一次消息。
聊天可以用 DuckDuckGo(或 Bing)搜索网页,不需要账号。你也可以在**设置,工具**里添加搜索密钥。搜索是唯一会把内容发出电脑的功能,
发出的就是搜索词。
要同时运行两个模型,请打开**运行多个模型**,并为每个模型分配同一品牌的单独一张显卡。
<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>Strata 应用中的一次对话:智能体用一个 HTML 文件写出了一个小型体素宝塔花园,页面在对话旁的预览中打开</sub></p>
### 在终端里
<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>:速度、GPU、内存和最近的请求</sub></p>
```sh
strata launch # 引导界面:模型、上下文、图片、访问方式,然后开始服务
strata launch --model q2_0 --yes --no-tui # 不提问:开始服务,打印地址,然后返回
strata launch --plan --model q2_0 # 显示将要执行的操作;不做任何更改
strata launch --status # 当前正在提供什么服务
strata launch --stop # 停止服务
strata monitor --url http://127.0.0.1:8080 # 查看速度、GPU、内存和请求
strata update --check # 有新引擎吗?(strata update 会安装它)
strata doctor # 检查已安装的引擎文件
```
`strata --help` 会列出所有命令。启动和提供服务时不会下载任何东西。只有安装、`strata launch`(在你选择模型时)和
`strata update` 会下载。要从手机或另一台电脑访问 Strata,请使用 `strata launch --access lan`。它会生成一个 API 密钥,
没有密钥就不会在网络上启动。除非你加上 `--open`,否则不会打开浏览器。指南:[docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md)。
完整的客户端说明:[docs/TERMINAL.md](docs/TERMINAL.md)。
### 配合 AI 智能体
智能体用方式 B 中的一行命令安装 Strata,然后运行 `strata launch --model NAME --yes --no-tui`。步骤、参数和退出码见
[docs/AI_SETUP.md](docs/AI_SETUP.md),仓库规则见 [AGENTS.md](AGENTS.md)。
### 连接你的其他应用
| 应用类型 | 基础 URL |
| --- | --- |
| 兼容 OpenAI(大多数应用和智能体) | `http://127.0.0.1:8080/v1`(API 密钥和模型名称随便填) |
| Anthropic API(Claude Code 等) | `http://127.0.0.1:8080`,不带 `/v1`。这些应用会自己加上 `/v1/messages`。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` 会在询问后写入配置文件,并保留备份。
### 图片
能看图片的模型默认开启图片功能。在聊天里附加或粘贴图片,或者从你的应用发送图片。显卡的显存小于 24 GB 时,读图由处理器完成
(每张图片几秒钟,不占用显卡的显存);显存达到 24 GB 时,由显卡完成。NVIDIA 和 AMD 的规则相同。你可以按模型关闭图片功能,
也可以用 `--vision off`、`--vision gpu` 或 `--vision cpu` 选择读图方式。
这条规则背后的数据:在 Radeon AI PRO R9700 上,显卡读一张 260 token 的图片用了 57 ms,处理器用了 3.1 秒。在 16 GB 的显卡
(Tesla P100 和 RTX A4000,Linux)上,用显卡读图会让文字速度下降 3-5%,因为它占用约 1.4 GB 显存,否则这部分会被模型使用。
在 Mac 上,无论内存多大,都由 GPU 读图(32 GB 的 M4 上读一张 260 token 的图片用 0.45 秒,占用约 1.2 GB 共享内存);处理器读一张约需 50 秒,所以只有指定 `--vision cpu` 时才用。
### 使用须知
- 在聊天菜单或你的应用的“reasoning effort”里选择思考级别(**off、low、medium 或 high**)。off 最快。
- Strata 默认一次回答一个请求。要同时处理多个,请在 `strata launch` 中使用 `--parallel N`,或在配置里写
`"parallel": 2`([BATCHING.md](docs/BATCHING.md))。在 12 GB 显卡上,这会让每个回答变慢。
- Strata 会完整读取聊天的第一条消息,每 30,000 个 token 约需 1 分钟。之后的消息几秒钟就开始。
- 聊天记录存放的位置和完整的 API:[docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored)、[docs/DETAILS.md](docs/DETAILS.md#using-it)。
## 从源码构建
<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="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>
另外有一个小助手会猜接下来的几个词。大模型一次检查所有猜测,保留正确的部分。得到的答案不变,但快了 1.6-1.8 倍。
长文本按大块读取,一次最多 8,192 个 token,速度超过每秒 1,000 个 token。
## 硬件
<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>
| 支持的硬件 | 指南 |
| --- | --- |
| **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`,或者再运行一次 `START-HERE.bat`(或 `./setup.sh`)。
它会从中断的地方继续。
- **非常慢、硬盘灯一直闪,或者提示“引擎意外停止”。** 你的电脑可用内存不够。关闭其他程序(浏览器很占内存),
或者选一个更小的尺寸,比如 Q2_0 或 IQ2_XS。
- **提示这台电脑没有可用的引擎。** 在应用里打开**设置,引擎**,点击**更新引擎**。在终端里运行 `strata update`。
`strata doctor` 会显示已安装哪些引擎文件。
- **提示 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` 的输出。从源码运行时,附上 Strata 文件夹里的 `strata-<model>.log`。
如果是安全问题,请私下报告:[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 完成。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]).
+1 -1
View File
@@ -74,7 +74,7 @@ the model. The section above is about the engine's HTTP server. This is what the
The Terminal installs with one line:
```sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
```
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "strata-desktop",
"productName": "Strata",
"version": "0.2.3",
"version": "0.2.6",
"private": true,
"description": "Strata Desktop - local AI, tuned to one model.",
"scripts": {
+1 -1
View File
@@ -3524,7 +3524,7 @@ checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7"
[[package]]
name = "strata-desktop"
version = "0.2.3"
version = "0.2.6"
dependencies = [
"gtk",
"serde",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "strata-desktop"
version = "0.2.3"
version = "0.2.6"
description = "Strata Desktop — local AI, tuned to one model."
edition = "2021"
rust-version = "1.77"
+32
View File
@@ -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"
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "strata",
"version": "0.2.3",
"version": "0.2.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "strata",
"version": "0.2.3",
"version": "0.2.6",
"dependencies": {
"html-to-image": "^1.11.13",
"katex": "^0.19.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "strata",
"private": true,
"version": "0.2.3",
"version": "0.2.6",
"type": "module",
"description": "Strata \u2014 local AI, tuned to one model. Web shell + engine host.",
"scripts": {
+385
View File
@@ -0,0 +1,385 @@
"""Writes src/locales/fix-<lang>.json: the interface strings that had no key in the locale files (#3479, #3825) and the strings of the 0.2.4.3 bug fixes.
Run from apps/host: python scripts/make-fix-locales.py
The keys are fx01.. in this order; fix-en.json is the English source. Placeholders ({0}, {1}) must be kept in every language."""
import json
from pathlib import Path
EN = [
"will stop running and be removed.",
"Downloaded, not loaded. Open Models to load it.",
"{0} lines added, {1} removed",
"Edited {0} files",
"Drop files to add them",
"Pictures and text files",
"Keep goal",
"Remove goal",
"The assistant stops working towards it. Your chat and files stay as they are.",
"This model can read pictures, but the engine installed here does not include the picture reader yet, so Read pictures is off. Update the engine in Settings → About to turn it on.",
"Showing the values of your tuning '{0}'.",
"Your tuning '{0}' sets",
"was {0}",
"· {0} RAM",
"The message was not cancelled",
"Extra settings for the program",
"Draft it with AI",
"Thinking time",
"Writing time",
"tokens/s",
"% of context",
"This page asks for files from the internet, which the preview does not load, so it may look incomplete. View code shows exactly what was written.",
"Keep A",
"Keep B",
"Took",
"Variation",
"Changed",
"Nothing",
"Both versions are ready. Compare A and B, then keep one.",
"The assistant is still answering",
"Send your message when it has finished.",
"The message was not queued",
"Scroll to the latest message",
"Remove task",
"Start a new conversation; keep this one",
"Have the assistant write a plan first; nothing runs until you approve it",
"Search the web, read the pages and answer with sources",
"Copied",
"Response",
"Message",
"PDF added",
"Select the text and copy it with Ctrl+C.",
"{0} is in this chat. Ask about it in your next message.",
]
T = {
"zh": [
"将停止运行并被移除。",
"已下载,尚未加载。打开“模型”页面来加载它。",
"新增 {0} 行,删除 {1} 行",
"已编辑 {0} 个文件",
"拖放文件即可添加",
"图片和文本文件",
"保留目标",
"移除目标",
"助手将不再朝这个目标工作。你的对话和文件保持不变。",
"此模型可以读取图片,但这里安装的引擎还不包含图片读取器,所以“读取图片”已关闭。请在“设置 → 关于”中更新引擎后再开启。",
"显示你的调优“{0}”的取值。",
"你的调优“{0}”设置了",
"原为 {0}",
"· {0} 内存",
"消息没有被取消",
"程序的额外设置",
"用 AI 起草",
"思考时间",
"写作时间",
"词元/秒",
"% 的上下文",
"此页面需要从互联网加载文件,预览不会加载这些文件,所以看起来可能不完整。“查看代码”会显示实际写入的内容。",
"保留 A",
"保留 B",
"用时",
"波动",
"已更改",
"无",
"两个版本都已就绪。比较 A 和 B,然后保留其中一个。",
"助手仍在回答",
"等它回答完再发送你的消息。",
"消息没有进入队列",
"滚动到最新消息",
"移除任务",
"开始新对话;保留当前这个",
"让助手先写一份计划;在你批准之前不会运行任何内容",
"搜索网络、阅读网页,并附上来源作答",
"已复制",
"回复",
"消息",
"已添加 PDF",
"请选中文字,然后按 Ctrl+C 复制。",
"{0} 已在此对话中。请在下一条消息里询问它。",
],
"ja": [
"は停止され、削除されます。",
"ダウンロード済みですが、読み込まれていません。「モデル」を開いて読み込んでください。",
"{0} 行追加、{1} 行削除",
"{0} 個のファイルを編集しました",
"ファイルをドロップして追加",
"画像とテキストファイル",
"目標を残す",
"目標を削除",
"アシスタントはこの目標に向けた作業をやめます。チャットとファイルはそのまま残ります。",
"このモデルは画像を読めますが、ここにインストールされているエンジンにはまだ画像リーダーが含まれていないため、「画像を読む」はオフです。設定 → 情報 でエンジンを更新するとオンにできます。",
"チューニング「{0}」の値を表示しています。",
"チューニング「{0}」が設定する項目",
"変更前: {0}",
"· {0} RAM",
"メッセージはキャンセルされませんでした",
"プログラムの追加設定",
"AI で下書きを作成",
"思考時間",
"執筆時間",
"トークン/秒",
"% のコンテキスト",
"このページはインターネット上のファイルを必要としますが、プレビューでは読み込まれないため、表示が不完全になることがあります。「コードを表示」で書かれた内容をそのまま確認できます。",
"A を残す",
"B を残す",
"所要時間",
"ばらつき",
"変更点",
"なし",
"両方のバージョンの準備ができました。A と B を比べて、どちらかを残してください。",
"アシスタントはまだ回答中です",
"回答が終わってからメッセージを送ってください。",
"メッセージは待機列に入りませんでした",
"最新のメッセージへスクロール",
"タスクを削除",
"新しい会話を始めます。今の会話は残ります",
"アシスタントにまず計画を書かせます。承認するまで何も実行されません",
"ウェブを検索し、ページを読み、出典付きで回答します",
"コピーしました",
"返信",
"メッセージ",
"PDF を追加しました",
"テキストを選択して Ctrl+C でコピーしてください。",
"{0} をこのチャットに追加しました。次のメッセージで質問してください。",
],
"de": [
"wird nicht mehr ausgeführt und entfernt.",
"Heruntergeladen, aber nicht geladen. Öffne „Modelle“, um es zu laden.",
"{0} Zeilen hinzugefügt, {1} entfernt",
"{0} Dateien bearbeitet",
"Dateien hier ablegen, um sie hinzuzufügen",
"Bilder und Textdateien",
"Ziel behalten",
"Ziel entfernen",
"Der Assistent arbeitet nicht mehr darauf hin. Dein Chat und deine Dateien bleiben unverändert.",
"Dieses Modell kann Bilder lesen, aber die hier installierte Engine enthält den Bildleser noch nicht, deshalb ist „Bilder lesen“ aus. Aktualisiere die Engine unter Einstellungen → Info, um es einzuschalten.",
"Zeigt die Werte deiner Abstimmung „{0}“.",
"Deine Abstimmung „{0}“ setzt",
"war {0}",
"· {0} RAM",
"Die Nachricht wurde nicht abgebrochen",
"Zusätzliche Einstellungen für das Programm",
"Mit KI entwerfen",
"Denkzeit",
"Schreibzeit",
"Token/s",
"% des Kontexts",
"Diese Seite lädt Dateien aus dem Internet, was die Vorschau nicht tut. Sie kann deshalb unvollständig aussehen. „Code anzeigen“ zeigt genau, was geschrieben wurde.",
"A behalten",
"B behalten",
"Dauer",
"Schwankung",
"Geändert",
"Nichts",
"Beide Versionen sind fertig. Vergleiche A und B und behalte eine.",
"Der Assistent antwortet noch",
"Sende deine Nachricht, wenn er fertig ist.",
"Die Nachricht wurde nicht eingereiht",
"Zur neuesten Nachricht scrollen",
"Aufgabe entfernen",
"Neue Unterhaltung beginnen; diese bleibt erhalten",
"Der Assistent schreibt zuerst einen Plan; nichts läuft, bis du ihn freigibst",
"Im Web suchen, die Seiten lesen und mit Quellen antworten",
"Kopiert",
"Antwort",
"Nachricht",
"PDF hinzugefügt",
"Markiere den Text und kopiere ihn mit Strg+C.",
"{0} ist in diesem Chat. Frag in deiner nächsten Nachricht danach.",
],
"es": [
"dejará de ejecutarse y se eliminará.",
"Descargado, pero no cargado. Abre Modelos para cargarlo.",
"{0} líneas añadidas, {1} eliminadas",
"{0} archivos editados",
"Suelta los archivos para añadirlos",
"Imágenes y archivos de texto",
"Mantener el objetivo",
"Quitar el objetivo",
"El asistente deja de trabajar en ello. Tu chat y tus archivos se quedan como están.",
"Este modelo puede leer imágenes, pero el motor instalado aquí aún no incluye el lector de imágenes, así que Leer imágenes está desactivado. Actualiza el motor en Ajustes → Acerca de para activarlo.",
"Se muestran los valores de tu ajuste «{0}».",
"Tu ajuste «{0}» establece",
"antes {0}",
"· {0} de RAM",
"El mensaje no se canceló",
"Ajustes adicionales del programa",
"Redactar con IA",
"Tiempo de pensar",
"Tiempo de escribir",
"tokens/s",
"% del contexto",
"Esta página pide archivos de internet, que la vista previa no carga, así que puede verse incompleta. Ver código muestra exactamente lo que se escribió.",
"Mantener A",
"Mantener B",
"Tardó",
"Variación",
"Cambiado",
"Nada",
"Las dos versiones están listas. Compara A y B y quédate con una.",
"El asistente sigue respondiendo",
"Envía tu mensaje cuando termine.",
"El mensaje no se puso en cola",
"Ir al último mensaje",
"Quitar tarea",
"Empieza una conversación nueva; esta se conserva",
"El asistente escribe primero un plan; no se ejecuta nada hasta que lo apruebes",
"Busca en la web, lee las páginas y responde con fuentes",
"Copiado",
"Respuesta",
"Mensaje",
"PDF añadido",
"Selecciona el texto y cópialo con Ctrl+C.",
"{0} está en este chat. Pregunta por él en tu próximo mensaje.",
],
"ru": [
"перестанет работать и будет удалена.",
"Скачана, но не загружена. Откройте «Модели», чтобы загрузить её.",
"Добавлено строк: {0}, удалено: {1}",
"Изменено файлов: {0}",
"Перетащите файлы сюда, чтобы добавить",
"Картинки и текстовые файлы",
"Оставить цель",
"Удалить цель",
"Ассистент перестанет работать над ней. Ваш чат и файлы останутся как есть.",
"Эта модель умеет читать картинки, но установленный здесь движок пока не содержит модуль чтения картинок, поэтому «Читать картинки» выключено. Обновите движок в разделе «Настройки → О программе», чтобы включить.",
"Показаны значения вашей настройки «{0}».",
"Ваша настройка «{0}» задаёт",
"было {0}",
"· {0} ОЗУ",
"Сообщение не было отменено",
"Дополнительные настройки программы",
"Набросать с помощью ИИ",
"Время размышления",
"Время написания",
"токенов/с",
"% контекста",
"Эта страница запрашивает файлы из интернета, а предпросмотр их не загружает, поэтому она может выглядеть неполной. «Показать код» показывает, что именно было написано.",
"Оставить A",
"Оставить B",
"Заняло",
"Разброс",
"Изменено",
"Ничего",
"Обе версии готовы. Сравните A и B и оставьте одну.",
"Ассистент ещё отвечает",
"Отправьте сообщение, когда он закончит.",
"Сообщение не поставлено в очередь",
"К последнему сообщению",
"Удалить задачу",
"Начать новый разговор; этот сохраняется",
"Ассистент сначала напишет план; ничего не запустится, пока вы его не одобрите",
"Искать в интернете, читать страницы и отвечать со ссылками на источники",
"Скопировано",
"Ответ",
"Сообщение",
"PDF добавлен",
"Выделите текст и скопируйте его клавишами Ctrl+C.",
"{0} добавлен в этот чат. Спросите о нём в следующем сообщении.",
],
"pt": [
"vai parar de funcionar e será removido.",
"Baixado, mas não carregado. Abra Modelos para carregá-lo.",
"{0} linhas adicionadas, {1} removidas",
"{0} arquivos editados",
"Solte os arquivos para adicioná-los",
"Imagens e arquivos de texto",
"Manter a meta",
"Remover a meta",
"O assistente deixa de trabalhar nela. Seu chat e seus arquivos continuam como estão.",
"Este modelo consegue ler imagens, mas o motor instalado aqui ainda não inclui o leitor de imagens, então Ler imagens está desligado. Atualize o motor em Configurações → Sobre para ligar.",
"Mostrando os valores do seu ajuste \"{0}\".",
"Seu ajuste \"{0}\" define",
"era {0}",
"· {0} de RAM",
"A mensagem não foi cancelada",
"Configurações extras do programa",
"Rascunhar com IA",
"Tempo de raciocínio",
"Tempo de escrita",
"tokens/s",
"% do contexto",
"Esta página pede arquivos da internet, que a visualização não carrega, então pode parecer incompleta. Ver código mostra exatamente o que foi escrito.",
"Manter A",
"Manter B",
"Levou",
"Variação",
"Alterado",
"Nada",
"As duas versões estão prontas. Compare A e B e mantenha uma.",
"O assistente ainda está respondendo",
"Envie sua mensagem quando ele terminar.",
"A mensagem não entrou na fila",
"Ir para a última mensagem",
"Remover tarefa",
"Começar uma nova conversa; esta continua salva",
"O assistente escreve um plano primeiro; nada roda até você aprovar",
"Pesquisar na web, ler as páginas e responder com fontes",
"Copiado",
"Resposta",
"Mensagem",
"PDF adicionado",
"Selecione o texto e copie com Ctrl+C.",
"{0} está neste chat. Pergunte sobre ele na próxima mensagem.",
],
"ko": [
"실행이 중지되고 제거됩니다.",
"다운로드했지만 아직 불러오지 않았습니다. 모델 화면을 열어 불러오세요.",
"{0}줄 추가, {1}줄 삭제",
"파일 {0}개 편집함",
"파일을 놓아서 추가하세요",
"그림과 텍스트 파일",
"목표 유지",
"목표 제거",
"어시스턴트가 이 목표를 향한 작업을 멈춥니다. 대화와 파일은 그대로 남습니다.",
"이 모델은 그림을 읽을 수 있지만, 여기에 설치된 엔진에는 아직 그림 읽기 기능이 없어서 '그림 읽기'가 꺼져 있습니다. 설정 → 정보에서 엔진을 업데이트하면 켤 수 있습니다.",
"튜닝 '{0}'의 값을 보여 줍니다.",
"튜닝 '{0}'이(가) 설정하는 항목",
"이전 값 {0}",
"· RAM {0}",
"메시지가 취소되지 않았습니다",
"프로그램 추가 설정",
"AI로 초안 작성",
"생각한 시간",
"작성한 시간",
"토큰/초",
"컨텍스트 %",
"이 페이지는 인터넷의 파일을 불러와야 하는데 미리보기는 이를 불러오지 않으므로 불완전하게 보일 수 있습니다. '코드 보기'에서 작성된 내용을 그대로 볼 수 있습니다.",
"A 유지",
"B 유지",
"걸린 시간",
"편차",
"변경됨",
"없음",
"두 버전이 모두 준비되었습니다. A와 B를 비교한 뒤 하나를 유지하세요.",
"어시스턴트가 아직 답하는 중입니다",
"답이 끝나면 메시지를 보내세요.",
"메시지가 대기열에 들어가지 않았습니다",
"최신 메시지로 이동",
"작업 제거",
"새 대화를 시작합니다. 이 대화는 그대로 둡니다",
"어시스턴트가 먼저 계획을 작성합니다. 승인하기 전에는 아무것도 실행되지 않습니다",
"웹을 검색하고 페이지를 읽어 출처와 함께 답합니다",
"복사했습니다",
"답변",
"메시지",
"PDF를 추가했습니다",
"텍스트를 선택한 뒤 Ctrl+C로 복사하세요.",
"{0}이(가) 이 채팅에 있습니다. 다음 메시지에서 물어보세요.",
],
}
here = Path(__file__).resolve().parent.parent / "locales" if False else Path(__file__).resolve().parent.parent / "src" / "locales"
def write(lang, values):
assert len(values) == len(EN), (lang, len(values), len(EN))
for source, text in zip(EN, values):
assert sorted(__import__("re").findall(r"\{\d+\}", source)) == sorted(__import__("re").findall(r"\{\d+\}", text)), (lang, source, text)
data = {f"fx{i + 1:02d}": text for i, text in enumerate(values)}
(here / f"fix-{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")
+7 -1
View File
@@ -5,6 +5,12 @@ import {spawn} from 'node:child_process';
import {resolve} from 'node:path';
const root=resolve(import.meta.dirname,'..');
const files=readdirSync(resolve(root,'tests')).filter(name=>name.endsWith('.test.mjs')).sort().map(name=>resolve(root,'tests',name));
const child=spawn(process.execPath,['--test',...files],{cwd:root,stdio:'inherit'});
/* Every test file runs against scratch copies of the per-user places the host writes to (the owner's real profile is never touched). */
import {mkdtempSync} from 'node:fs';
import {tmpdir} from 'node:os';
import {join} from 'node:path';
const scratch=mkdtempSync(join(tmpdir(),'strata-host-suite-profile-'));
const env={...process.env,STRATA_SETTINGS_FILE:join(scratch,'app-settings.json'),STRATA_COMPONENTS_DIR:join(scratch,'components'),LOCALAPPDATA:join(scratch,'Local'),APPDATA:join(scratch,'Roaming'),XDG_DATA_HOME:join(scratch,'xdg-data'),XDG_STATE_HOME:join(scratch,'xdg-state'),XDG_CONFIG_HOME:join(scratch,'xdg-config')};
const child=spawn(process.execPath,['--test',...files],{cwd:root,stdio:'inherit',env});
child.on('error',error=>{console.error(error.message);process.exitCode=1;});
child.on('exit',(code,signal)=>{process.exitCode=code??(signal?1:0);});
+23 -9
View File
@@ -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.
@@ -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
@@ -90,7 +94,7 @@ What the person chose is saved by `tools-settings.mjs` (inside the host's settin
## Thinking and the output budget
- 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 +112,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 +130,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 +138,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 +146,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.
+52
View File
@@ -234,3 +234,55 @@ export async function installBundledEngine({ bundleDir, components, platform = n
return { action: 'refused', reason: error?.message ?? String(error) };
}
}
/**
* After an update from an installer that carried an engine (a per-GPU build), its `engine-bundle` folder is still in the install folder
* (about 570 MB) although the engine was copied to the engine store long ago. It is removed only when EVERY package in it exists complete in
* the store (every file there with the size and SHA-256 its manifest lists); otherwise it stays and one line says why.
* Returns { removed: bool, reason?: string, packages?: number }; never throws.
*/
export async function removeVerifiedBundle({ bundleDir, components, log = () => {} } = {}) {
try {
if (!bundleDir || !components || !existsSync(bundleDir)) return { removed: false, reason: 'no bundle' };
// Files inside a signed macOS app must stay intact after installation.
if (process.platform === 'darwin' && resolve(bundleDir).split(sep).some(part => part.toLowerCase().endsWith('.app'))) return keepBundle(log, 'macOS app bundles must remain unchanged after signing');
const packages = [];
for (const entry of await readdir(bundleDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const folder = join(bundleDir, entry.name);
const manifest = await readManifest(folder);
packages.push({ folder, manifest, name: entry.name });
}
if (!packages.length) return { removed: false, reason: 'nothing in the bundle' };
const store = join(components, 'engine');
const stored = existsSync(store) ? (await readdir(store, { withFileTypes: true })).filter(e => e.isDirectory() && !e.name.startsWith('.')).map(e => join(store, e.name)) : [];
for (const pkg of packages) {
if (!pkg.manifest) return keepBundle(log, `${pkg.name} has no readable manifest`);
let found = false;
for (const candidate of stored) {
const theirs = await readManifest(candidate);
if (!theirs || theirs.version !== pkg.manifest.version || theirs.platform !== pkg.manifest.platform || theirs.backend !== pkg.manifest.backend
|| (theirs.variant ?? '') !== (pkg.manifest.variant ?? '') || (theirs.build_id ?? '') !== (pkg.manifest.build_id ?? '')) continue;
if (await packageIsWhole(candidate, pkg.manifest, { deep: true })) { found = true; break; }
}
if (!found) return keepBundle(log, `${pkg.name} is not (or not completely) in the engine store`);
}
for (const pkg of packages) await rm(pkg.folder, { recursive: true, force: true });
log(`engine bundle: the copy that came with the installer (${packages.map(p => p.name).join(', ')}) was removed; the same engine is complete in the engine store`);
return { removed: true, packages: packages.length };
} catch (error) {
return keepBundle(log, `could not be checked (${error?.message ?? error})`);
}
}
function keepBundle(log, why) {
log(`engine bundle: kept in the install folder (${why})`);
return { removed: false, reason: why };
}
/**
* What the host does once its engine step has settled: for an engine that is (or was) there, look at the install folder's leftover bundle.
* The lines go to the host's output (console.log), which the desktop shell copies to its log file (`[host] engine bundle: ...`).
*/
export function cleanupAfterInstall(settled, { bundleDir, components, log = line => console.log(line) } = {}) {
return Promise.resolve(settled).then(result => (['installed', 'kept', 'repaired'].includes(result?.action) ? removeVerifiedBundle({ bundleDir, components, log }) : null), () => null);
}
+6
View File
@@ -183,6 +183,12 @@ export function createControl({ control = findControl(), env = process.env, run
if (failed) throw Object.assign(new ControlError(failed.error, { exitCode: failed.exit_code ?? result.code, output: result.json }), { code: result.code === 7 ? 'engine_serving' : 'update_failed' });
throw failure(result, 'The update failed.');
},
/** Install an engine package the person downloaded and chose (the offline path): no network; Control checks the package's own signature. */
async installEngineFile(path, { onEvent = null, signal = null } = {}) {
const result = await call(['components', 'install-file', path, '--output', 'jsonl'], { timeoutMs: 3600_000, onLine: onEvent ?? (() => {}), signal });
if (result.json?.ok) return { ok: true, results: [result.json] };
throw failure(result, 'This file could not be installed.');
},
async rollbackEngine() {
const result = await call(['components', 'rollback', '--output', 'json'], { timeoutMs: 120_000 });
if (result.json?.ok) return result.json;
+5
View File
@@ -1,6 +1,7 @@
/** Physical topology and GPU sensors; unavailable values stay null. */
import { readFile, readdir, realpath } from 'node:fs/promises';
import { probe, round } from './util.mjs';
import { windowsGpuReading } from './windows-gpu.mjs';
/** Unified GPUs share one system memory pool; dedicated GPUs each have their own. */
export function gpuMemoryTotals(devices, readings) {
@@ -83,6 +84,10 @@ export async function readGpuTelemetry(gpus) {
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};
if(gpu.luid&&!nvidia.has(gpu.id)) {
const counters=windowsGpuReading(gpu.luid);
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;
for(const hwmon of await list(`${gpu.sensorPath}/hwmon`)) {
+4 -1
View File
@@ -62,7 +62,10 @@ export function watchedWindowsDialog(shell, args, { readyMs = 30_000, openMs = 2
const later = () => { clearTimeout(timer); ready = true; setTimeout(() => end(resolve, { stdout: '' }), openMs).unref?.(); };
child.stdout?.on('data', chunk => { stdout += chunk; if (!ready && /^READY\s*$/m.test(stdout)) { clearTimeout(timer); later(); } });
child.on('error', error => end(reject, error));
child.on('close', code => { if (settled) return; settled = true; clearTimeout(timer); code === 0 ? resolve({ stdout }) : reject(Object.assign(new Error('exit ' + code), { code })); });
child.on('close', code => { if (settled) return; settled = true; clearTimeout(timer);
// PowerShell ended before its window opened (security software ended it, #3831): the person types the path instead.
if (!ready) return resolve({ noDialog: true });
code === 0 ? resolve({ stdout }) : reject(Object.assign(new Error('exit ' + code), { code })); });
});
}
+7 -5
View File
@@ -12,6 +12,7 @@ import { CONTEXT_TOKENS, VARIANTS, footprint } from './catalog.mjs';
import { MODELS_DIR, diskFreeGb, diskTotalGb } from './store.mjs';
import { probe, round } from './util.mjs';
import { utf8Script } from './powershell-utf8.mjs';
import { DX_ADAPTERS_SCRIPT, parseDxAdapters, windowsOtherGpus } from './windows-gpu.mjs';
let cached = null;
let cachedAt = 0;
@@ -187,11 +188,12 @@ async function detectGpus(platform, totalGb) {
}
if(platform==='win32') {
const raw=await probe('powershell.exe',['-NoProfile','-NonInteractive','-Command',utf8Script('Get-CimInstance Win32_VideoController | Select-Object Name,PNPDeviceID | ConvertTo-Json -Compress')]);
try {const parsed=JSON.parse(raw);for(const row of Array.isArray(parsed)?parsed:[parsed]) {
if(!row?.Name||(/nvidia/i.test(row.Name)&&gpus.some(g=>g.vendor==='nvidia')))continue;
const vendor=/amd|radeon/i.test(row.Name)?'amd':/intel/i.test(row.Name)?'intel':'unknown';
gpus.push({id:String(row.PNPDeviceID),index:gpus.length,vendor,name:row.Name,vramGb:0,unified:false,backend:'vulkan'});
}}catch{}
try {
const parsed=JSON.parse(raw);
const rows=(Array.isArray(parsed)?parsed:[parsed]).filter(row=>row?.Name&&!(/nvidia/i.test(row.Name)&&gpus.some(g=>g.vendor==='nvidia')));
const dx=rows.length?parseDxAdapters(await probe('powershell.exe',['-NoProfile','-NonInteractive','-Command',DX_ADAPTERS_SCRIPT],8000)):[];
for(const card of windowsOtherGpus(rows,dx))gpus.push({...card,index:gpus.length,unified:false,backend:'vulkan'});
}catch{}
}
gpus.forEach((g,i)=>{g.index=i;});
+30 -5
View File
@@ -32,7 +32,8 @@ import { findBrowser, findPlaywrightMcp } from './agent/browser-probe.mjs';
*/
import http from 'node:http';
import { homedir } from 'node:os';
import { cpSync, createReadStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { statfsSync, cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { spawn } from 'node:child_process';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
@@ -61,7 +62,7 @@ import { createForkTimeskip } from './timeskip-fork.mjs';
import { prepareOversized } from './oversize.mjs';
import { createChatFolders, chatRootOf, openFile, resolveChatFile, previewFile, rawFile, createAttachments, withAttachments } from './chat-files.mjs';
import { createLlmProxy, samplingFromExpert } from './agent/llm-shape.mjs';
import { checkUpdates, checkComponentUpdates } from './updates.mjs';
import { checkUpdates, checkComponentUpdates, engineFirstRun } from './updates.mjs';
import { plainNoModel } from './no-model-text.mjs';
import { createAppUi } from './app-ui.mjs';
import { appVersionFrom, appVersionToShow } from './app-version.mjs';
@@ -69,7 +70,7 @@ import { createImageModels } from './image-models.mjs';
import { createRuntimeModels } from './runtime-models.mjs';
import { createNativeControl } from './native-control.mjs';
import { nativeUnloadPlan } from './storage-check.mjs';
import { installBundledEngine } from './bundled-engine.mjs';
import { installBundledEngine, cleanupAfterInstall } from './bundled-engine.mjs';
import { findControl, createControl, componentsDirectory } from './control-runtime.mjs';
import { forbidden as acquisitionForbidden } from './acquisition.mjs';
import { randomBytes, timingSafeEqual } from 'node:crypto';
@@ -130,6 +131,9 @@ const appUpdater = appUi.updater;
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) })
: Promise.resolve({ action: 'none' });
const engineBundleDir = process.env.STRATA_ENGINE_BUNDLE_DIR || resolve(HERE, '..', '..', 'engine-bundle');
/* An update from a per-GPU installer leaves its engine-bundle behind; it goes only when the same engine is complete in the store (sizes and hashes). */
cleanupAfterInstall(bundledEngine, { bundleDir: engineBundleDir, components: componentsDirectory() });
let bundledEngineSettled = !(control && !process.env.STRATA_NO_BUNDLED_ENGINE);
bundledEngine.then(() => { bundledEngineSettled = true; }, () => { bundledEngineSettled = true; });
/* A build made for another make of graphics card is not unpacked; Control's answer then carries the plain sentence (with the right download) instead of its own. */
@@ -1022,11 +1026,11 @@ async function api(req, res, path, method, url) {
if(nativeOperation&&method==='POST')return json(res,200,await native.cancel(nativeOperation[1]));
const nativeInstance=/^\/api\/native\/instances\/([a-z0-9-]+)\/(unload|default)$/.exec(path);
if(nativeInstance&&method==='POST')return json(res,200,nativeInstance[2]==='unload'?await native.unload(nativeInstance[1]):native.setDefault(nativeInstance[1]));
const nativeSettings=/^\/api\/native\/models\/([a-z0-9_-]+)\/settings$/.exec(path);
const nativeSettings=/^\/api\/native\/models\/([a-z0-9_.-]+)\/settings$/.exec(path);
/* Expert mode shows what will actually run: the saved tuning's values come with the settings (null when none applies or unreadable). */
if(nativeSettings&&method==='GET'){const settings=native.settings(nativeSettings[1]);let tuned=null;try{const t=await native.tuneEffective({model:nativeSettings[1],resourceProfile:url.searchParams.get('resourceProfile')||undefined,vision:url.searchParams.get('vision')||undefined});tuned=t.applied?t:null;}catch{tuned=null;}return json(res,200,{...settings,tuned});}
if(nativeSettings&&method==='PATCH')return json(res,200,native.save(nativeSettings[1],await readBody(req)));
const nativeSettingsFile=/^\/api\/native\/models\/([a-z0-9_-]+)\/settings-file$/.exec(path);
const nativeSettingsFile=/^\/api\/native\/models\/([a-z0-9_.-]+)\/settings-file$/.exec(path);
if(nativeSettingsFile&&method==='GET')return json(res,200,native.settingsFile(nativeSettingsFile[1]));
if(nativeSettingsFile&&method==='POST')return json(res,200,await native.openSettingsFile(nativeSettingsFile[1]));
@@ -1327,6 +1331,27 @@ async function api(req, res, path, method, url) {
}
if (path === '/api/setup' && method === 'GET') return json(res, 200, setupState());
if (path === '/api/engine/first-run' && method === 'GET') {
if (!control?.updateCheck) return json(res, 200, { status: 'unavailable', gpus: [] });
const info = await engineFirstRun({ control, gpus: hardware.cachedHardware()?.gpus ?? [] });
try { let at = componentsDirectory(); while (at && !existsSync(at) && dirname(at) !== at) at = dirname(at); const f = statfsSync(at); info.freeBytes = Number(f.bavail) * Number(f.bsize); } catch { /* unknown: no warning */ }
return json(res, 200, info);
}
if (path === '/api/engine/job' && method === 'GET') {
// The newest engine download / file install, read without waiting for the native snapshot to connect (the first-run card needs it before that).
const kinds = ['update-engine', 'install-engine-file'];
return json(res, 200, { job: native.state().jobs.filter(j => kinds.includes(j.kind)).slice(-1)[0] ?? null });
}
if (path === '/api/engine/from-file' && method === 'POST') {
// The person chose an engine package in the page (the offline path). The bytes come here; Control checks and installs them.
const name = String(req.headers['x-file-name'] ?? 'engine.tar.gz').replace(/[^A-Za-z0-9._-]/g, '_').slice(-120);
if (!/\.(tar\.gz|tgz|tar\.xz|zip)$/i.test(name)) return error(res, 400, 'This is not a Strata engine package. Choose the .tar.gz or .zip file from the release page.');
const folder = join(store.DATA_DIR, 'incoming-engine');
mkdirSync(folder, { recursive: true, mode: 0o700 });
const target = join(folder, name);
await pipeline(req, createWriteStream(target, { mode: 0o600 }));
return json(res, 202, native.operation('install-engine-file', { path: target }));
}
if (path === '/api/updates/check' && method === 'GET' && control?.updateCheck) return json(res, 200, await checkComponentUpdates({ control, appUpdater, appVersion: process.env.STRATA_APP_VERSION || null }));
if (path === '/api/updates/check' && method === 'GET') return json(res, 200, await checkUpdates({ appVersion:appVersionToShow(setupState().app.version,native.state().version), engineVersion:native.state().available?(native.state().installed?.engine?.version??null):(await engine.config()).engine.version }));
+3 -3
View File
@@ -5,15 +5,15 @@
*/
import { basename } from 'node:path';
const PART_WORDS = { engine: 'the engine program', 'tokenizer/vocab.json': 'the tokenizer', 'tokenizer/merges.txt': 'the tokenizer', 'tokenizer/token_type.json': 'the tokenizer' };
const PART_WORDS = { engine: 'the engine program', 'component:strata-engine': 'the engine program', 'tokenizer/vocab.json': 'the tokenizer', 'tokenizer/merges.txt': 'the tokenizer', 'tokenizer/token_type.json': 'the tokenizer' };
/** Short readable names, no duplicates, most useful first. */
export function missingNames(controlParts, installerFiles) {
const names = [];
const add = text => { const t = String(text ?? '').trim(); if (t && !names.includes(t)) names.push(t); };
for (const file of Array.isArray(installerFiles) ? installerFiles : []) add(basename(String(file).split(String.fromCharCode(92)).join('/')));
for (const file of Array.isArray(installerFiles) ? installerFiles : []) { const name = basename(String(file).split(String.fromCharCode(92)).join('/')); add(PART_WORDS[name] ?? name); }
for (const item of Array.isArray(controlParts) ? controlParts : []) {
add(PART_WORDS[item?.part] ?? item?.file ?? item?.part);
add(PART_WORDS[item?.part] ?? PART_WORDS[item?.file] ?? item?.file ?? item?.part);
}
return names;
}
+43 -21
View File
@@ -25,7 +25,7 @@ import { friendlyModelName } from './model-names.mjs';
import { parseSaved as parseRoles, mirror as mirrorRoles } from './gpu-roles.mjs';
const ROOT=resolve(dirname(fileURLToPath(import.meta.url)),'..');
const safeId=value=>typeof value==='string'&&/^[a-z0-9_-]{1,80}$/i.test(value);
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 */
const busy=requests=>requests.some(r=>['queued','generating'].includes(r.state));
function readJson(path,fallback={}){try{return JSON.parse(readFileSync(path,'utf8').replace(/^\uFEFF/,''));}catch{return fallback;}}
@@ -38,7 +38,13 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
let child=null,seq=0,buffer='',pending=new Map(),definitions=[],refreshing=null,lastRead=0,connecting=null;
let snapshot={available:false,repo:preferences.repo,summary:'Connect a local Strata installation.',catalog:null,installed:null,hardware:null,error:null};
const instances=new Map(),processes=new Map(),jobs=new Map(),logs=[];
const savedJobs=readJson(join(directory,'native-jobs.json'),[]);for(const j of (Array.isArray(savedJobs)?savedJobs:[]).slice(-12))jobs.set(j.id,{...j,...(j.state==='running'?{state:'error',error:'The host restarted before completion. Check diagnostics, then retry; completed files are kept.',summary:'Interrupted operation'}:{})});
const savedJobs=readJson(join(directory,'native-jobs.json'),[]);/* An app update ends the app on purpose: the installer replaces it and starts it again. The job that asked for it was saved as running together with the app version it started from (appUpdateFrom); a start that finds a different version knows the update finished, instead of calling it an interrupted operation. */
const thisAppVersion=()=>process.env.STRATA_APP_VERSION||null;
/* A record an OLDER host wrote (no appUpdateFrom): the app's own program file was replaced after the job began, so the installer finished. <install>/host/server is ROOT's parent chain: ROOT = <install>/host. */
const appProgramNewerThan=started=>{try{const t=Date.parse(started);if(!Number.isFinite(t))return false;const dir=dirname(ROOT),exe=join(dir,process.platform==='win32'?'strata-desktop.exe':'strata-desktop'),times=[];for(const f of [exe,join(dir,'uninstall.exe')])if(existsSync(f)){const st=statSync(f);times.push(st.mtimeMs,st.ctimeMs);}/* the installer keeps the build time as the program's modified time but writes uninstall.exe and changes the program file's metadata when it installs */return times.some(x=>x>t);}catch{return false;}};
const settledByAppUpdate=j=>j.state==='running'&&j.kind==='update-strata'&&((typeof j.appUpdateFrom==='string'&&!!thisAppVersion()&&thisAppVersion()!==j.appUpdateFrom)||(typeof j.appUpdateFrom!=='string'&&appProgramNewerThan(j.startedAt)));
const neverFinishedAppUpdate=j=>j.state==='running'&&j.kind==='update-strata'&&typeof j.appUpdateFrom==='string'&&!!thisAppVersion()&&thisAppVersion()===j.appUpdateFrom;
for(const j of (Array.isArray(savedJobs)?savedJobs:[]).slice(-12))jobs.set(j.id,{...j,...(settledByAppUpdate(j)?{state:'done',error:null,summary:`Strata is updated to ${thisAppVersion()}.`,finishedAt:new Date().toISOString()}:neverFinishedAppUpdate(j)?{state:'error',error:`The update to ${j.appUpdateTo??'the new version'} did not finish: the app closed for the installer, but the old version ${j.appUpdateFrom} started again. Try Update again, or download the installer from the Strata release page (https://github.com/Niko1221/Strata/releases/latest).`,summary:'The app update did not finish',finishedAt:new Date().toISOString()}:j.state==='running'?{state:'error',error:'The host restarted before completion. Check diagnostics, then retry; completed files are kept.',summary:'Interrupted operation'}:{})});
/* Packaged mode: the app ships Strata Control (control-runtime.mjs). Models are run configs in an install folder, the engine
comes from the component folder, and a start is `strata runtime start` under the owner runtime. Nothing is installed,
updated or fetched from here. Without `control` (a source checkout) every path below is the original one. */
@@ -50,6 +56,9 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
The install only runs when the user clicks Download; starting and serving (control.start) keep STRATA_NO_ACQUIRE. */
const defaultInstall=()=>process.env.STRATA_INSTALL_DIR?resolve(process.env.STRATA_INSTALL_DIR):join(dirname(componentsDirectory()),'install');
let packagedInstall=null;
/* The component updates and the rollback go through Control, not through the native snapshot, so they work in the first seconds after the app starts and on a PC that has no engine yet. */
const COMPONENT_KINDS=['update-engine','update-strata','rollback','install-engine-file'];
const updateLog=line=>{try{console.log(`update · ${line}`);}catch{/* no console */}};
const NO_INSTALL='Installing, updating, tuning with calibration or rolling back model files and engines happens from the Terminal (strata install), never while the app runs.';
let defaultId=null,timer=null,closing=false,polling=false;
const emit=()=>{const value=state();writeFileSync(join(directory,'native-jobs.json'),JSON.stringify(value.jobs),{mode:0o600});changed(value);};
@@ -71,13 +80,13 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
app is reinstalled to another folder, or an update moves Control, those absolute paths point at nothing and every installed
model breaks. At every read of the installed models, a path that ends up inside a folder called `control` that no longer
exists is pointed at the Control this app has now, when the file is there. The config is saved (with a .bak) only then. */
function repairControlPaths(dir){if(!packaged||!dir||!existsSync(dir))return;let names=[];try{names=readdirSync(dir).filter(n=>/^strata-[a-z0-9_-]+\.json$/i.test(n));}catch{return;}
function repairControlPaths(dir){if(!packaged||!dir||!existsSync(dir))return;let names=[];try{names=readdirSync(dir).filter(n=>/^strata-[a-z0-9_.-]+\.json$/i.test(n));}catch{return;}
const here=control.control.dir,inside=/^(.+?[\/]control)(?=[\/]|$)(.*)$/i;
const fix=value=>{if(typeof value!=='string')return value;const m=value.match(inside);if(!m||existsSync(value)||resolve(m[1]).toLowerCase()===resolve(here).toLowerCase())return value;const candidate=join(here,m[2]);return existsSync(candidate)?candidate:value;};
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,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');}
@@ -128,21 +137,21 @@ 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;
}
function configure({repository}){if(packaged){if(instances.size||[...jobs.values()].some(j=>j.state==='running'))throw new Error('Unload models and finish operations before changing the installation.');const next=realpathSync(String(repository));if(!statSync(next).isDirectory())throw new Error('Choose the folder that holds your Strata model configurations.');preferences={...preferences,installDir:next};packagedInstall=null;disconnect();writeFileSync(settingsPath,JSON.stringify(preferences,null,2),{mode:0o600});lastRead=0;return refresh(true);}if(instances.size||[...jobs.values()].some(j=>j.state==='running'))throw new Error('Unload models and finish operations before changing the installation.');const next=realpathSync(String(repository));const old=preferences;preferences={...preferences,repo:next};try{repo();}catch(e){preferences=old;throw e;}disconnect();writeFileSync(settingsPath,JSON.stringify(preferences,null,2),{mode:0o600});lastRead=0;return refresh(true);}
function setDownloadDirectory(input,{insideJob=false}={}){if(!insideJob&&[...jobs.values()].some(j=>j.state==='running'))throw new Error('Finish the current operation before changing the folder.');if(typeof input!=='string'||!input.trim()||/[\r\n\0]/.test(input))throw new Error('Choose a model folder.');let path;try{path=realpathSync(input.trim());}catch{throw new Error('That folder does not exist. Check the path, or that the drive is connected.');}if(!statSync(path).isDirectory())throw new Error('Choose a folder, not a file.');/* A whole drive (D:\) is not filled with models: D:\Strata is made and used. */if(isDriveRoot(path)){const own=dataFolderFromPick(path).path;makeFolder(own);path=realpathSync(own);}try{accessSync(path,constants.R_OK|constants.W_OK);}catch{throw new Error('Strata cannot write in that folder. Choose a folder you can save files in.');}preferences={...preferences,downloadDirectory:path};/* the same choice for the installer and the Terminal: the Control settings file */if(packaged)rememberDataFolder(controlSettingsFile(),path,Date.now()/1000,true);writeFileSync(settingsPath,JSON.stringify(preferences,null,2),{mode:0o600});shareDataFolder();emit();return state();}
function recordFile(model){if(!safeId(model))throw new Error('Choose an installed model.');const entry=snapshot.installed?.models?.find(i=>i.model===model);if(!entry)throw new Error('This model is not installed. Download and prepare it first.');const path=realpathSync(entry.configPath??join(repo(),entry.config));if(dirname(path)!==repo()||!/^strata-[a-z0-9_-]+\.json$/i.test(basename(path)))throw new Error('The model configuration is outside the Strata installation.');return {entry,path};}
function recordFile(model){if(!safeId(model))throw new Error('Choose an installed model.');const entry=snapshot.installed?.models?.find(i=>i.model===model);if(!entry)throw new Error('This model is not installed. Download and prepare it first.');const path=realpathSync(entry.configPath??join(repo(),entry.config));if(dirname(path)!==repo()||!/^strata-[a-z0-9_.-]+\.json$/i.test(basename(path)))throw new Error('The model configuration is outside the Strata installation.');return {entry,path};}
function record(model){const {entry,path}=recordFile(model);return {entry,path,config:readJson(path)};}
/* The text file a power user edits by hand: where it is and what is wrong in it (run-config-check.mjs). */
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};}
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};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 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 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));
/* A chosen drafter that is not on this computer yet is part of the download: its size is listed beside the model's (Control decides; nothing is fetched here). */
@@ -207,6 +216,12 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
if(still.length)throw new Error(`Some files could not be removed: ${still.slice(0,3).map(p=>basename(p)).join('; ')}`);
return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
/* The configuration a load starts the engine with (before its port and key): the saved model config, the user's choices and the resource profile. The tuner measures exactly this, so a saved tuning belongs to the same choices a load makes. */
/* DeepSeek-V4.1-Flash runs on its own engine (strata-dsv41) that takes none of the Flash-Next options the app adds on its own (request slots, the RAM budget, the output clamp, the resource profile): a flag it does not know ends the start with "unknown option". */
const ownEngine=config=>config?.arch==='deepseek41'||/strata-dsv41/i.test(String(config?.exe??''));
/* The DeepSeek engine's version, read from the folder the run config names (engine-dsv41/<version>-<platform>-...). --experts exists from engine 0.1.50: an older engine ends the start with "unknown option", so the choice is offered and passed only for 0.1.50 or newer. */
const DS_EXPERTS_MIN=[0,1,50];
function dsEngineVersion(config){const m=/engine-dsv41[\\/](\d+(?:\.\d+)+)-/.exec(String(config?.exe??''))||/engine-dsv41[\\/](\d+(?:\.\d+)+)-/.exec(String((config?.lib_dirs??[])[0]??''));return m?m[1].split('.').map(Number):null;}
function dsExpertsSupported(config){const v=dsEngineVersion(config);if(!v)return false;for(let i=0;i<DS_EXPERTS_MIN.length;i++){const a=v[i]??0,b=DS_EXPERTS_MIN[i];if(a!==b)return a>b;}return true;}
function buildConfig(args,config){
const vision=runtimeVision(config,args.vision);
const visionPreferences=args.vision!==undefined?{ui_vision_enabled:args.vision!=='no',ui_vision_device:args.vision==='no'?config.ui_vision_device??'yes':args.vision}:{};
@@ -215,7 +230,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
const values0=args.values??{},cleared='vram_reserve_mib' in values0&&(values0.vram_reserve_mib===null||values0.vram_reserve_mib===''),base0=cleared?{...config,vram_reserve_mib:undefined,[USER_KEY]:setUserFlag(config[USER_KEY],'--vram-reserve-mib',null)}:config;
let mine=captureUserArgs(base0);const asked=cleared?null:reserveChoice({...base0,[USER_KEY]:mine},snapshot.hardware,values0).asked;
if(asked!==null)mine=setUserFlag(mine,'--vram-reserve-mib',asked);
const resources=args.resourceProfile?resourceSettings({...base0,[USER_KEY]:mine,vision},snapshot.hardware,args.resourceProfile,modelArena(args.model),values0):null;
const resources=args.resourceProfile&&!ownEngine(config)?resourceSettings({...base0,[USER_KEY]:mine,vision},snapshot.hardware,args.resourceProfile,modelArena(args.model),values0):null;
const cfg={...config,...visionPreferences,...(resources?{...resources,ui_resource_profile:args.resourceProfile}:{}),host:'127.0.0.1',open_browser:false,...validateNativeSettings(args.values??{}),vision};
/* The app's suggested cards only fill a gap: a model whose settings file already names its GPUs ("gpu": [1, 0], with "gpu_order": "as_given") keeps them (#3256). A card the person picked in the screen always wins. */
const namedInFile=(Array.isArray(config.gpu)?config.gpu.length>0:config.gpu!==undefined&&config.gpu!==null&&config.gpu!=='')||!!config.gpu_roles;
@@ -230,11 +245,14 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
if(!cfg.vision&&Array.isArray(cfg.args))cfg.args=cfg.args.filter(a=>a!=='--vision');
for(const arg of cfg.args??[])if(arg.endsWith('.gguf')&&!existsSync(resolve(repo(),arg)))throw new Error('An engine argument names a missing GGUF. Restore the saved arguments or repair the files.');
/* The app's own choice of request slots (4, fewer where the card has no room) is part of the configuration, so a tune measures the engine a load will start: slots take VRAM from the expert cache. */
const parallelChoice=packaged?chooseParallel({config:cfg,values:args.values??{},hardware:snapshot.hardware?.gpus?.length?snapshot.hardware:{gpus:hardwareProbe()??[]}}):null;
const parallelChoice=packaged&&!ownEngine(config)?chooseParallel({config:cfg,values:args.values??{},hardware:snapshot.hardware?.gpus?.length?snapshot.hardware:{gpus:hardwareProbe()??[]}}):null;
let parallelNote=null;
/* The agent asks for long answers (a thinking model can use most of the window): let the engine clamp the output to what is left of the context instead of refusing the request. */
if(packaged&&cfg.fit_max_tokens===undefined)cfg.fit_max_tokens=true;
if(packaged&&!ownEngine(config)&&cfg.fit_max_tokens===undefined)cfg.fit_max_tokens=true;
if(parallelChoice){const applied=applyParallel(cfg,parallelChoice);for(const k of Object.keys(cfg))if(!(k in applied.cfg))delete cfg[k];Object.assign(cfg,applied.cfg);parallelNote=applied.note;}
/* DeepSeek's Speed choice: the engine decides by itself (--experts auto: 5 when the cards total 16 GB or less, else 6), so nothing is passed unless the person chose 5 or 6. */
if(ownEngine(config)){const list=[];const old=Array.isArray(cfg.args)?cfg.args:[];for(let i=0;i<old.length;i++){if(old[i]==='--experts'){i++;continue;}list.push(old[i]);}if(dsExpertsSupported(config)&&(cfg.ds_experts==='5'||cfg.ds_experts==='6'))list.push('--experts',cfg.ds_experts);else if(!dsExpertsSupported(config)&&old.includes('--experts'))list.splice(0,list.length,...old);cfg.args=list;delete cfg.ds_experts;}
else delete cfg.ds_experts;
return {cfg,resources,visionPreferences,parallelChoice,parallelNote};
}
/* The DFlash2 drafter, before a load that uses it: Control says whether the choice needs it (and the file is not here), downloads it pinned and checked, and packs it, with plain progress in the job.
@@ -286,7 +304,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
/* The engine starts under Control's owner runtime, not as our child: a saved profile per slot, then `runtime start`. */
const used=new Set([...instances.values()].map(i=>i.profile));let slot=1;while(used.has('app-'+slot))slot++;instance.profile='app-'+slot;
instances.set(id,instance);if(!defaultId||!args.multiple||wasDefault)defaultId=id;emit();
const startWith=value=>{writeFileSync(configPath,JSON.stringify(value,null,2),{mode:0o600});return control.stop(instance.profile).catch(()=>{}).then(()=>control.saveProfile(instance.profile,configPath)).then(()=>control.start(instance.profile,{waitSeconds:300}));};
const startWith=value=>{writeFileSync(configPath,JSON.stringify(value,null,2),{mode:0o600});return control.stop(instance.profile).catch(()=>{}).then(()=>control.saveProfile(instance.profile,configPath)).then(()=>control.start(instance.profile,{waitSeconds:ownEngine(config)?Math.min(600,Math.max(300,Number(config.engine_ready_s)||300)):300}));};
started=startWith(cfg);
started.then(()=>append(id,'Strata Control reports the engine is ready.'),error=>{append(id,error.message);});
try{await started;}catch(firstError){let error=firstError;
@@ -299,7 +317,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
/* ONE automatic second try in all, never saved, never repeated (args.retrySafer false turns it off): with a safer memory setting when the
failure looks like a memory stop, and without several-requests-at-once when that was the app's own default (not a number the user chose).
Both changes ride on the same single try, with the plain sentence of the memory change when there is one. */
const allowed=args.retrySafer!==false&&job.state==='running';
const allowed=args.retrySafer!==false&&job.state==='running'&&!ownEngine(config);
const safer=allowed?saferStart(cfg,snapshot.hardware,modelArena(args.model),said):null;
const dropParallel=allowed&&cfg.parallel>1&&!parallelChoice?.explicit;
if(!safer&&!dropParallel){instance.status='error';instance.error=said;emit();throw new Error(said);}
@@ -331,7 +349,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
function ensurePoll(){if(!timer){timer=setInterval(()=>void poll(),2000);timer.unref?.();}void poll();}
async function benchmark(job,args){const instance=resolveInstance(args.instanceId);job.instanceId=instance.id;if(busy(requests().filter(r=>r.instanceId===instance.id)))throw new Error('Wait for this model to finish before benchmarking.');const started=Date.now();job.abort=new AbortController();const response=await fetcher(`http://127.0.0.1:${instance.port}/v1/chat/completions`,{method:'POST',headers:{'content-type':'application/json',authorization:`Bearer ${instance.key}`},signal:AbortSignal.any([job.abort.signal,AbortSignal.timeout(180000)]),body:JSON.stringify({model:instance.apiModelId,messages:[{role:'user',content:'Explain how a rainbow forms in clear, simple language.'}],temperature:0,reasoning_effort:'none',max_tokens:200,stream:false})});const body=await response.json();if(!response.ok)throw new Error(body.error?.message??'Benchmark failed.');if(job.state!=='running')return;const result={summary:'Measured benchmark complete.',at:new Date().toISOString(),model:instance.apiModelId,durationMs:Date.now()-started,tokensIn:body.usage?.prompt_tokens??null,tokensOut:body.usage?.completion_tokens??null,outputTps:body.timings?.predicted_per_second??null,promptTps:body.timings?.prompt_per_second??null,source:'engine',settings:'Greedy · thinking off · up to 200 output tokens'};writeFileSync(join(directory,'native-benchmark.json'),JSON.stringify(result,null,2),{mode:0o600});return result;}
function resolveInstance(id){const aliases=[...instances.values()].filter(i=>i.aliases?.includes(id));if(aliases.length>1&&!instances.has(id)&&![...instances.values()].some(i=>i.apiModelId===id))throw new Error('This alias matches more than one instance. Choose its exact model ID.');const i=instances.get(id)??[...instances.values()].find(i=>i.apiModelId===id)??aliases[0]??(!id&&instances.size===1?[...instances.values()][0]:undefined);if(!i)throw new Error('Choose a loaded model ID.');if(i.status!=='ready')throw new Error('This model is not ready.');return i;}
async function program(job,args){if(packaged)return componentProgram(job);if(externalRunning().length)throw new Error('Stop the native server running outside this UI before changing engine files.');if(instances.size)throw new Error('Unload native models before changing engine files.');const folder=repo();let {command,args:prefix}=python(),argv;
async function program(job,args){if(packaged)return componentProgram(job,args);if(externalRunning().length)throw new Error('Stop the native server running outside this UI before changing engine files.');if(instances.size)throw new Error('Unload native models before changing engine files.');const folder=repo();let {command,args:prefix}=python(),argv;
if(job.kind==='calibrate'||job.kind==='tune'){if(!state().capabilities.calibration)throw new Error('This installation calibrates NVIDIA GPUs only. Use the short benchmark on other devices.');const model=args.model??snapshot.installed?.models?.[0]?.model;const chosen=record(model);if(!chosen.entry.ready)throw new Error('Prepare this model before calibrating.');command=venvPython(folder);prefix=[];if(!existsSync(command))throw new Error('Prepare the Python environment first.');argv=['-c','import sys; from pathlib import Path; import setup; sys.exit(0 if setup.calibrate_config(Path(sys.argv[1])) else 1)',chosen.path];}
else if(job.kind==='rollback'){if(!previousEngine())throw new Error('No saved previous engine is available.');argv=[join(folder,'setup.py'),'--rollback-engine'];}
else if(job.kind==='update-engine'){if(!snapshot.installed?.models?.length)throw new Error('Prepare a model first. Its installer downloads the matching engine.');argv=[join(folder,'setup.py'),'--update'];}
@@ -342,29 +360,33 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
/* Packaged app: engine and Control updates and the engine rollback go through Control's explicit update step (`strata update`,
`strata components rollback`), never through git, setup.py or a download in this process. The app itself is updated by the
shell's signed Tauri updater. Models are not part of any of it. */
async function componentProgram(job){
if(!['update-engine','update-strata','rollback'].includes(job.kind)||typeof control.update!=='function')throw new Error(NO_INSTALL);
async function componentProgram(job,args={}){
if(!COMPONENT_KINDS.includes(job.kind)||typeof control.update!=='function')throw new Error(NO_INSTALL);
if(instances.size)throw new Error('Unload native models before changing engine files.');
job.abort=new AbortController();
const subject=job.kind==='update-strata'?'Strata':'engine';
try{return await componentSteps(job,subject);}catch(error){throw error?.details!==undefined&&error.message?error:plainError(error,subject);}
try{return await componentSteps(job,subject,args);}catch(error){updateLog(`${job.kind} failed: ${String(error?.message??error).slice(0,400)}`);throw error?.details!==undefined&&error.message?error:plainError(error,subject);}
}
async function componentSteps(job,subject){
async function componentSteps(job,subject,args={}){
const onEvent=event=>{
if(event.event==='progress'){job.summary=`Downloading: ${event.percent}%`;job.result={install:{step:job.summary,download:{file:event.label,percent:event.percent}}};}
else if(event.event==='phase'&&PHASE_TEXT[event.phase])job.summary=PHASE_TEXT[event.phase];
else if(event.event==='waiting')job.summary='Waiting for the running model to stop…';
emit();
};
updateLog(`${job.kind} started (app ${thisAppVersion()??'unknown'})`);
if(job.kind==='install-engine-file'){if(typeof args.path!=='string'||!args.path)throw new Error('Choose the engine file first.');let done;try{done=await control.installEngineFile(args.path,{onEvent,signal:job.abort.signal});}finally{try{const own=resolve(directory,'incoming-engine')+sep;if(resolve(args.path).startsWith(own))rmSync(args.path,{force:true});}catch{/* the copy in the host's own folder; a leftover is harmless */}}return {summary:updateSummary(done)==='Everything is already up to date.'?(done.results?.[0]?.note??'Nothing was changed.'):updateSummary(done),result:done};}
if(job.kind==='rollback'){const done=await control.rollbackEngine();return {summary:'The previous engine is back. Your models and settings are unchanged.',result:done};}
if(job.kind==='update-engine'){const done=await control.update({engine:true,control:false,onEvent,signal:job.abort.signal});return {summary:updateSummary(done),result:done};}
const done=await control.update({engine:false,control:true,onEvent,signal:job.abort.signal});
updateLog(`Control step finished: ${updateSummary(done)}`);
const notes=[updateSummary(done)];
if(appUpdater){
job.summary='Looking for a new version of the app…';emit();
let verdict;
try{verdict=await appUpdater.check();}catch(error){if(error?.code!=='app-update-unavailable')throw error;verdict={available:false};notes.push(appUpdater.unavailable||error.message);}
if(verdict.available){await appUpdater.install({signal:job.abort.signal,onProgress:p=>{if(p.total){job.summary=`Downloading the app update: ${Math.round(100*p.downloaded/p.total)}%`;job.result={install:{step:job.summary,download:{file:'Strata app',percent:Math.round(100*p.downloaded/p.total)}}};emit();}}});notes.push('The app update is installed. Strata will restart.');}
updateLog(`app check: ${verdict.available?`version ${verdict.version} is available`:'no newer app version'}`);
if(verdict.available){job.appUpdateFrom=thisAppVersion();job.appUpdateTo=verdict.version??null;emit();updateLog(`app update to ${verdict.version??'?'} starting; the installer will end this app`);await appUpdater.install({signal:job.abort.signal,onProgress:p=>{if(p.total){job.summary=`Downloading the app update: ${Math.round(100*p.downloaded/p.total)}%`;job.result={install:{step:job.summary,download:{file:'Strata app',percent:Math.round(100*p.downloaded/p.total)}}};emit();}}});notes.push('The app update is installed. Strata will restart.');}
}
return {summary:notes.filter(Boolean).join(' '),result:done};
}
@@ -410,7 +432,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
const room=args.resourceProfile==='work';
return autotuner.start(job,job.kind==='recheck'?'recheck':'tune',{configPath:path,presetId:args.presetId,mode:args.mode,use:args.use,goal:room?'room':'speed',leaveRam:room?20:0,allowSmallDifferences:args.allowSmallDifferences===true,name:args.name},{onProgress:p=>{job.summary=p.text??job.summary;emit();}});
}
function operation(kind,args={}){if(!['install','load','benchmark','tune','autotune','recheck','calibrate','update-engine','update-strata','rollback','remove'].includes(kind))throw new Error('Choose a supported native operation.');if(!snapshot.available)throw new Error(snapshot.error??'Connect the native Strata installation first.');if(kind==='install'||kind==='load'){const refusal=engineRefusal(snapshot.catalog,args.model,snapshot.engineInfo);if(refusal)throw new Error(refusal);}return begin(kind,job=>kind==='install'?install(job,args):kind==='load'?loadEngine(job,args):kind==='benchmark'?benchmark(job,args):kind==='tune'?tune(job,args):kind==='autotune'||kind==='recheck'?autotune(job,args):kind==='remove'?removeModel(job,args):program(job,args),{guided:args.guided===true,model:args.model,...(args.family?{family:args.family}:{})});}
function operation(kind,args={}){if(!['install','load','benchmark','tune','autotune','recheck','calibrate','update-engine','update-strata','rollback','install-engine-file','remove'].includes(kind))throw new Error('Choose a supported native operation.');if(!snapshot.available&&!(packaged&&COMPONENT_KINDS.includes(kind)))throw new Error(snapshot.error??'Connect the native Strata installation first.');if(kind==='install'||kind==='load'){const refusal=engineRefusal(snapshot.catalog,args.model,snapshot.engineInfo);if(refusal)throw new Error(refusal);}return begin(kind,job=>kind==='install'?install(job,args):kind==='load'?loadEngine(job,args):kind==='benchmark'?benchmark(job,args):kind==='tune'?tune(job,args):kind==='autotune'||kind==='recheck'?autotune(job,args):kind==='remove'?removeModel(job,args):program(job,args),{guided:args.guided===true,model:args.model,...(args.family?{family:args.family}:{})});}
/* ---- The models folder card (0.2.2): what a picked folder means, and the three choices. The copying is Control's (strata models move). ---- */
const currentModelsFolder=()=>preferences.downloadDirectory??(modelsDirectory?modelsDirectory():snapshot.installed?.data_folder)??null;
const moveFile=()=>join(directory,'models-move.cancel');
@@ -433,7 +455,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
const under=value=>typeof value==='string'&&[ 'models','packs','mtp' ].some(item=>{const base=join(old,item).toLowerCase();const v=value.toLowerCase();return v===base||v.startsWith(base+sep)||v.startsWith(base+'/');});
const scan=value=>Array.isArray(value)?value.some(scan):value&&typeof value==='object'?Object.values(value).some(scan):under(value);
for(const name of readdirSync(folder)){
if(!/^strata-[a-z0-9_-]+\.json$/i.test(name)||name.endsWith('.shared-settings.json'))continue;
if(!/^strata-[a-z0-9_.-]+\.json$/i.test(name)||name.endsWith('.shared-settings.json'))continue;
if(!scan(readJson(join(folder,name))))continue;
mkdirSync(kept,{recursive:true});renameSync(join(folder,name),join(kept,name));aside.push(name);
}
+2 -1
View File
@@ -11,6 +11,7 @@ export const NATIVE_FIELDS=[
{id:'mtp',label:'Draft layer (MTP)',group:'Serving',type:'boolean',hint:'The draft layer guesses several words ahead, which makes answers faster, and costs about 1 GB of graphics memory and some RAM. Turn it off if the model stops while it starts, or the card is small: the expert cache gets that memory back and answers are slower. Default is on.'},
{id:'drafting',label:'Drafting',group:'Serving',type:'enum',choices:['auto','mtp','dflash2','off'],hint:'Which drafter guesses words ahead to make answers faster. Auto (recommended) lets Strata pick from what it measured on cards like yours; today that is the MTP. DFlash2 is for Qwen3.8-Flash-Next only and downloads a 1.2 GB drafter once. If DFlash2 cannot run, the MTP is used and Details says why. Off turns the draft layer off.'},
{id:'batchMtp',label:'MTP per request',group:'Serving',type:'boolean',hint:'Each concurrent request uses the draft layer. Left alone, Strata turns it on for one graphics card with 24 GB or more and several requests at once, and off otherwise; choose on or off here to decide yourself.'},
{id:'ds_experts',label:'Speed (DeepSeek)',group:'Serving',type:'enum',choices:['5','6'],forModel:'deepseek',hint:'Faster uses 5 of 6 experts per word: a small quality change, usually not noticeable. Full quality uses all 6. Left alone, the engine picks Faster on a PC whose graphics cards total 16 GB or less.'},
{id:'parallel',label:'Concurrent requests',group:'Serving',type:'number',min:1,max:8,hint:'More slots need more memory and can slow each response.'},
{id:'engine_silence_s',label:'Engine stall timeout (seconds)',group:'Recovery',type:'number',min:10,max:3600},
{id:'api_monitor',label:'Keep API request metadata',group:'Recovery',type:'boolean'},
@@ -26,7 +27,7 @@ export const NATIVE_FIELDS=[
{id:'mcp_max_result_chars',label:'Maximum tool result characters',group:'Tools',type:'number',min:100,max:1000000},
];
/** 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'];
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. */
export function withoutDraftLayer(args){
const out=[];const list=(args??[]).map(String);
+38 -7
View File
@@ -1,5 +1,7 @@
/** Host counters. CPU sockets are measured only when their logical CPU IDs are known. */
import os from 'node:os';
import { spawn } from 'node:child_process';
import { createPowerShellLoop, endWithHost } from './windows-poll.mjs';
import { readFile, readdir } from 'node:fs/promises';
import { availableMemoryGb } from './hardware.mjs';
import { probe } from './util.mjs';
@@ -18,7 +20,7 @@ export function cpuUtilization(previous, current, ids) {
}
export function diskRate(previous, current, time) {
const elapsed=previous?(time-previous.time)/1000:0;
const elapsed=previous?((current?.at??time)-(previous.counters?.at??previous.time))/1000:0;
if(elapsed<=0||!current||!previous?.counters)return {readMbs:null,writeMbs:null};
const read=current.read-previous.counters.read,write=current.write-previous.counters.write;
return {readMbs:read>=0?read/1024**2/elapsed:null,writeMbs:write>=0?write/1024**2/elapsed:null};
@@ -41,6 +43,33 @@ export function parseMacDiskCounters(raw) {
return reads.length&&writes.length?{read:reads.reduce((n,m)=>n+Number(m[1]),0),write:writes.reduce((n,m)=>n+Number(m[1]),0)}:null;
}
export function parseWindowsDiskLine(line) {
const m=/^\s*(\d+)\s+(\d+)\s*$/.exec(String(line));
return m?{read:Number(m[1]),write:Number(m[2])}:null;
}
/** The disk counters, read by one long-lived PowerShell (windows-poll.mjs; #3939). */
export function createWindowsDiskReader({ spawnFn=spawn, intervalSec=4, maxStarts=3, now=Date.now }={}) {
let latest=null;
const script=`while($true){$d=Get-CimInstance Win32_PerfRawData_PerfDisk_PhysicalDisk -Filter "Name='_Total'";if($d){[Console]::Out.WriteLine(([string]$d.DiskReadBytesPersec)+' '+([string]$d.DiskWriteBytesPersec));[Console]::Out.Flush()};Start-Sleep -Seconds ${intervalSec}}`;
const loop=createPowerShellLoop({script,spawnFn,maxStarts,onLine:line=>{
const parsed=parseWindowsDiskLine(line);
if(parsed)latest={...parsed,at:now()};
return !!parsed;
}});
return {
read() { loop.start(); return latest&&now()-latest.at<5*intervalSec*1000?latest:null; },
stop() { loop.stop(); },
get starts() { return loop.starts; },
};
}
let windowsReader=null;
function windowsDisks() {
windowsReader??=endWithHost(createWindowsDiskReader());
return windowsReader.read();
}
async function disks() {
try {
if(process.platform==='linux') {
@@ -49,24 +78,26 @@ async function disks() {
}
if(process.platform==='darwin')return parseMacDiskCounters(await probe('ioreg',['-r','-c','IOBlockStorageDriver','-l'],1500));
if(process.platform==='win32') {
const raw=await probe('powershell.exe',['-NoProfile','-NonInteractive','-Command',"Get-CimInstance Win32_PerfRawData_PerfDisk_PhysicalDisk -Filter \"Name='_Total'\" | Select-Object DiskReadBytesPersec,DiskWriteBytesPersec | ConvertTo-Json -Compress"],2000);
const value=JSON.parse(raw);
if(value.DiskReadBytesPersec!=null&&value.DiskWriteBytesPersec!=null)return {read:Number(value.DiskReadBytesPersec),write:Number(value.DiskWriteBytesPersec)};
return windowsDisks();
}
} catch { /* An unsupported counter stays unavailable. */ }
return null;
}
export function createSystemSampler() {
let previousCpu=null,previousDisk=null;
let previousCpu=null,previousDisk=null,lastRate={readMbs:null,writeMbs:null};
return async processors => {
const current=os.cpus(),time=Date.now();
const cpuLoad=cpuUtilization(previousCpu,current,current.map((_,i)=>i));
const cpus=processors.map(p=>({id:p.id,utilizationPct:cpuUtilization(previousCpu,current,p.logicalIds??(processors.length===1?current.map((_,i)=>i):[]))}));
previousCpu=current;
const [available,counters]=await Promise.all([availableMemoryGb(process.platform),disks()]);
const rate=diskRate(previousDisk,counters,time);
previousDisk={time,counters};
let rate=lastRate;
if(!(counters&&counters.at!==undefined&&previousDisk?.counters?.at===counters.at)) {
rate=diskRate(previousDisk,counters,time);
previousDisk={time,counters};
lastRate=rate;
}
const total=os.totalmem()/1024**3,free=available??os.freemem()/1024**3;
return {cpuLoad,cpus,ramUsedGb:Math.max(0,Math.min(total,total-free)),ramTotalGb:total,diskReadGbs:rate.readMbs===null?null:rate.readMbs/1024,diskWriteMbs:rate.writeMbs};
};
+33 -3
View File
@@ -59,7 +59,7 @@ export async function checkComponentUpdates({ control, appUpdater = null, appVer
const message = /update server|aren't available|No internet/.test(plain.headline) ? plain.message : `Couldn't check for updates. Try again in a moment.`;
return { checkedAt, strata: { installedVersion: appVersion ?? control.version ?? null, status: 'error', message, details: plain.details, canInstall: false }, engine: { installedVersion: null, status: 'error', message, details: plain.details, canInstall: false } };
}
const mib = bytes => `${Math.round((bytes ?? 0) / (1 << 20))} MB`;
const mib = bytes => `${Math.round((bytes ?? 0) / 1_000_000)} MB`; // decimal megabytes, like the Terminal and the release page
const engine = report.engine ?? {};
const installedEngine = engine.installed?.version ?? null;
const offered = engine.offered?.version ?? null;
@@ -74,6 +74,7 @@ export async function checkComponentUpdates({ control, appUpdater = null, appVer
const app = report.app ?? {};
let appNew = app.status === 'available' ? app.offered : null;
let appNotes = app.notes ?? '';
let appCheckFailed = null; // the shell's own (Tauri) check threw: before this, the Settings check hid it and showed only Control's text
let appCannot = null; // browser mode: the app window's updater is not running, so the app itself cannot be updated from here
if (appUpdater) {
try {
@@ -81,12 +82,41 @@ export async function checkComponentUpdates({ control, appUpdater = null, appVer
appNew = verdict.available ? verdict.version : null;
appNotes = verdict.notes ?? appNotes;
} catch (error) {
if (error?.code === 'app-update-unavailable' && appUpdater.unavailable) appCannot = appUpdater.unavailable; // otherwise the feed's own view stands
if (error?.code === 'app-update-unavailable' && appUpdater.unavailable) appCannot = appUpdater.unavailable;
else appCheckFailed = String(error?.message ?? error ?? 'The app update check failed.'); // the app's own check did not work: say so, raw text behind Details
}
}
const controlNew = control_.status === 'available' ? control_.offered : null;
const strata = { installedVersion: appVersion ?? control_.installed ?? control.version ?? null, canInstall: !!((appNew && !appCannot) || controlNew) };
if (appNew || controlNew) Object.assign(strata, { status: 'available', latestVersion: appNew ?? controlNew, message: `A new version of Strata is available.${appNotes ? ` ${appNotes}` : ''}${appNew && appCannot ? ` ${appCannot}` : ''}` });
const appCheckLine = "Strata couldn't check for a new version of the app itself. Try again later.";
if (appNew || controlNew) Object.assign(strata, { status: 'available', latestVersion: appNew ?? controlNew, message: `A new version of Strata is available.${appNotes ? ` ${appNotes}` : ''}${appNew && appCannot ? ` ${appCannot}` : ''}${appCheckFailed ? ` ${appCheckLine}` : ''}` });
else if (appCheckFailed) Object.assign(strata, { status: 'error', message: appCheckLine });
else Object.assign(strata, { status: 'current', message: 'Strata is up to date.' });
if (appCheckFailed) strata.details = appCheckFailed;
return { checkedAt, feed: report.feed, strata, engine: engineView, serving: report.serving ?? [] };
}
/**
* What the first-run card shows: this PC's graphics card(s), the engine package the signed feed offers for it and how big it is.
* One feed read, nothing downloaded. `status`: offer (a package fits), none (nothing fits; `reasons` say why), offline (the feed could not be
* read), installed (an engine is already here), wrong-make (an engine for another make of card is installed).
*/
export async function engineFirstRun({ control, gpus = [] }) {
const cards = gpus.map(g => ({ name: g.name ?? 'Graphics card', vramGb: g.vramGb ?? null }));
let report;
try {
report = await control.updateCheck({});
} catch (error) {
const plain = plainUpdateError(error, 'Strata');
return { status: looksOffline(error) || /update server|No internet/.test(plain.headline ?? '') ? 'offline' : 'error', gpus: cards, message: plain.message, details: plain.details };
}
const engine = report.engine ?? {};
const hardware = report.hardware ?? {};
const base = { gpus: cards, driver: hardware.nvidia_driver ?? null, backend: hardware.backend ?? null };
if (engine.installed) return { ...base, status: 'installed', installedVersion: engine.installed.version ?? null };
if (engine.status === 'wrong-make') return { ...base, status: 'wrong-make', message: engine.note ?? null, reasons: engine.reasons ?? [] };
if (engine.offered && (engine.status === 'install' || engine.status === 'available')) {
return { ...base, status: 'offer', offer: { version: engine.offered.version, bytes: engine.download_bytes || engine.offered.size || 0, variant: engine.offered.variant ?? '', backend: engine.offered.backend ?? null } };
}
return { ...base, status: 'none', message: engine.note ?? null, reasons: engine.reasons ?? [] };
}
+90
View File
@@ -0,0 +1,90 @@
/**
* Windows graphics cards that nvidia-smi does not cover (AMD, Intel): their size and their load.
* - size: the DirectX adapter list in the registry (HKLM\SOFTWARE\Microsoft\DirectX) has a 64-bit DedicatedVideoMemory and the adapter
* LUID. Win32_VideoController.AdapterRAM is 32 bits and reads 0 or 4 GB for a 16 GB card (#3727, #3836).
* - load and memory in use: the Windows GPU performance counters (the ones Task Manager shows), read by one long-lived PowerShell and
* matched to the card by LUID. Temperature, power and the PCIe link need the vendor's own library, so they stay "not available".
*/
import { createPowerShellLoop, endWithHost } from './windows-poll.mjs';
import { utf8Script } from './powershell-utf8.mjs';
export const DX_ADAPTERS_SCRIPT = utf8Script(
"Get-ChildItem HKLM:\\SOFTWARE\\Microsoft\\DirectX | ForEach-Object { $p = Get-ItemProperty $_.PSPath; " +
"if ($p.Description -and $null -ne $p.AdapterLuid) { [pscustomobject]@{ Description = [string]$p.Description; VendorId = [string]$p.VendorId; " +
"Luid = [string]$p.AdapterLuid; Dedicated = [string]$p.DedicatedVideoMemory; Software = [string]$p.SoftwareAdapter } } } | ConvertTo-Json -Compress");
/** luid_0x<high>_0x<low> in the spelling of the performance counters, from the registry's 64-bit number. */
export function luidName(value) {
try {
const n = BigInt.asUintN(64, BigInt(String(value)));
const hex = v => '0x' + v.toString(16).padStart(8, '0');
return `luid_${hex(n >> 32n)}_${hex(n & 0xffffffffn)}`;
} catch { return null; }
}
/** The adapters the DirectX registry lists: [{ description, vendorId, luid, bytes, software }]. Never throws. */
export function parseDxAdapters(raw) {
try {
const parsed = JSON.parse(raw), rows = Array.isArray(parsed) ? parsed : [parsed];
return rows.filter(r => r?.Description).map(r => ({
description: String(r.Description), vendorId: Number(r.VendorId) || 0, luid: luidName(r.Luid),
bytes: Number(r.Dedicated) || 0, software: String(r.Software) === '1' || String(r.Software).toLowerCase() === 'true',
}));
} catch { return []; }
}
const norm = text => String(text ?? '').toLowerCase().replace(/[^a-z0-9]/g, '');
const VIRTUAL = /virtual|remote|indirect|mirror|basic render|meta |parsec|displaylink|idd/i;
/**
* Win32_VideoController rows + the DirectX adapter list -> the cards this app can show. A card keeps the size from the registry;
* a display driver for a virtual monitor (no size, no known maker) is not a graphics card and is dropped.
*/
export function windowsOtherGpus(videoRows, dx) {
const used = new Set();
const out = [];
for (const row of videoRows) {
if (!row?.Name) continue;
const match = dx.find((a, i) => !used.has(i) && !a.software && norm(a.description) === norm(row.Name));
if (match) used.add(dx.indexOf(match));
const vendor = /amd|radeon/i.test(row.Name) ? 'amd' : /intel/i.test(row.Name) ? 'intel' : 'unknown';
const vramGb = match?.bytes ? Math.round(match.bytes / 1024 ** 3 * 10) / 10 : 0;
if (vendor === 'unknown' && (!vramGb || VIRTUAL.test(row.Name))) continue;
out.push({ id: String(row.PNPDeviceID), vendor, name: row.Name, vramGb, luid: match?.luid ?? null });
}
return out;
}
/** One counter line: "G <luid> <load percent> <dedicated bytes in use>". */
export function parseGpuCounterLine(line) {
const m = /^G\s+(luid_0x[0-9a-f]+_0x[0-9a-f]+)\s+(\d+(?:\.\d+)?)\s+(\d+)\s*$/i.exec(String(line));
return m ? { luid: m[1].toLowerCase(), utilizationPct: Math.max(0, Math.min(100, Number(m[2]))), memoryUsedGb: Number(m[3]) / 1024 ** 3 } : null;
}
export function createWindowsGpuReader({ spawnFn, intervalSec = 4, maxStarts = 3, now = Date.now } = {}) {
const readings = new Map();
const script = utf8Script(
`while($true){` +
`$e=@(Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUEngine);$m=@(Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUAdapterMemory);` +
`$u=@{};foreach($r in $e){if($r.Name -match '(luid_0x[0-9A-Fa-f]+_0x[0-9A-Fa-f]+)_phys_\\d+_eng_\\d+_engtype_(\\w+)'){$k=$matches[1].ToLower()+'|'+$matches[2];$u[$k]=[double]$u[$k]+[double]$r.UtilizationPercentage}};` +
`$b=@{};foreach($k in $u.Keys){$l=$k.Split('|')[0];if(-not $b.ContainsKey($l) -or $u[$k] -gt $b[$l]){$b[$l]=$u[$k]}};` +
`foreach($r in $m){if($r.Name -match '(luid_0x[0-9A-Fa-f]+_0x[0-9A-Fa-f]+)'){$l=$matches[1].ToLower();[Console]::Out.WriteLine('G '+$l+' '+[math]::Round([double]$b[$l],0)+' '+$r.DedicatedUsage)}};` +
`[Console]::Out.Flush();Start-Sleep -Seconds ${intervalSec}}`);
const loop = createPowerShellLoop({ script, spawnFn, maxStarts, onLine: line => {
const parsed = parseGpuCounterLine(line);
if (parsed) readings.set(parsed.luid, { ...parsed, at: now() });
return !!parsed;
} });
return {
read(luid) { loop.start(); const r = readings.get(String(luid ?? '').toLowerCase()); return r && now() - r.at < 6 * intervalSec * 1000 ? r : null; },
stop() { loop.stop(); },
};
}
let shared = null;
/** The card's load and memory in use from the Windows counters; null when there is no reading. */
export function windowsGpuReading(luid) {
if (process.platform !== 'win32' || !luid) return null;
shared ??= endWithHost(createWindowsGpuReader());
return shared.read(luid);
}
+45
View File
@@ -0,0 +1,45 @@
/**
* One long-lived PowerShell that prints a line per reading, in a loop. The monitor used to start a cold PowerShell for every sample;
* its WMI start cost more than the probe timeout, so every query failed and WmiPrvSE burned a core (#3939).
* The loop gives up after `maxStarts` early exits in a row, so a counter that cannot be read stays quiet.
*/
import { spawn } from 'node:child_process';
export function createPowerShellLoop({ script, onLine, spawnFn = spawn, maxStarts = 3 }) {
let child = null, starts = 0, buffer = '';
function start() {
if (child || starts >= maxStarts) return;
starts++;
let proc;
try { proc = spawnFn('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] }); }
catch { return; }
child = proc;
buffer = '';
proc.on?.('error', () => { if (child === proc) child = null; });
proc.on?.('exit', () => { if (child === proc) child = null; });
proc.stdout?.on('data', chunk => {
buffer += chunk;
let i;
while ((i = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, i);
buffer = buffer.slice(i + 1);
if (onLine(line)) starts = 0;
}
});
proc.unref?.();
proc.stdout?.unref?.();
}
return {
start,
stop() { try { child?.kill(); } catch { /* gone */ } child = null; },
get starts() { return starts; },
};
}
let stoppers = null;
/** Every loop is ended with the host. */
export function endWithHost(loop) {
if (!stoppers) { stoppers = new Set(); process.once('exit', () => stoppers.forEach(l => l.stop())); }
stoppers.add(loop);
return loop;
}
+4 -2
View File
@@ -4,7 +4,7 @@ import { useId, useRef, useState } from 'react';
import { FloatingMenu } from './FloatingMenu';
import { Bar, Btn, Dialog, ErrorNotice, Icon } from './ui';
import { go, useLifecycle, loadVariant, selectRemoteModel, setSettingsSection, useStore } from '../lib/store';
import { pickerGroups, pickerVersionState } from '../lib/model-picker';
import { pickerGroups, pickerNativeModels, pickerVersionState } from '../lib/model-picker';
import { versionFit } from '../lib/model-versions';
import type { ModelVariant } from '../lib/types';
import { sizeGb } from '../lib/format';
@@ -37,6 +37,7 @@ function LegacyChatModelPicker() {
const running = library?.variants.find(v => v.id === library.loaded);
const version = library?.variants.find(v => v.id === selected);
const groups = library ? pickerGroups(library) : [];
const installedNative = pickerNativeModels(native?.installed?.models, native?.instances);
const status = version ? pickerVersionState(version, library?.loaded ?? null) : null;
const tooBig = !!version && library?.mode === 'real' && versionFit(version, hardware, contextTokens(engine?.beginner.contextLength ?? '32K')).zone === 'too-big';
async function apply() {
@@ -78,7 +79,8 @@ function LegacyChatModelPicker() {
<button ref={trigger} className="chat-model-badge chat-model-picker" aria-label={tr("Choose model: {0}", [apiModel?.name ?? (running?.local ? running.name : running?.family) ?? 'No model loaded'])} title={tr(apiModel ? `${connection?.name} · ${apiModel.id}` : 'View and switch models')} disabled={!library && !providers} aria-haspopup="menu" aria-expanded={open} onClick={() => setOpen(v => !v)}><Icon name={apiModel ? 'link' : modelFamilyIcon(running?.family ?? groups[0]?.family)} size={16}/><span>{tr(apiModel?.name ?? (running?.local ? running.name : running?.family) ?? groups[0]?.family ?? 'No model loaded')}</span><Icon name="chevron" size={12}/></button>
{tr(open ? <FloatingMenu anchor={trigger.current} title={tr("Models")} width={380} onClose={() => setOpen(false)}>
{tr(native?.instances.filter(i => i.status !== 'error').map(i => <button key={i.id} className="menu__item" role="menuitem" disabled={streaming || i.status !== 'ready'} onClick={() => void nativeAction(`instances/${i.id}/default`).then(() => { selectRemoteModel(null); setOpen(false); }).catch(e => setError(e.message))}><Icon name="models"/><span>{tr(i.label)}<small>{tr(i.gpus.map(g => `GPU ${g + 1}`).join(" + ") || "Automatic GPU")}</small></span></button>))}<p className="model-picker-heading">{tr("Local models")}</p>
{tr(!groups.length ? <p className="model-picker-empty">{tr("No local models downloaded yet.")}</p> : null)}
{tr(installedNative.map(m => <button key={m.config || m.model} className="menu__item model-picker-option model-picker-api" role="menuitem" disabled={streaming} onClick={() => { setOpen(false); go('models'); }}><span><strong>{tr(m.model_name || m.model)}</strong><small>{tr("Downloaded, not loaded. Open Models to load it.")}</small></span></button>))}
{tr(!groups.length && !installedNative.length ? <p className="model-picker-empty">{tr("No local models downloaded yet.")}</p> : null)}
{tr(groups.map(({ family, versions }, index) => {
const main = versions[0], state = pickerVersionState(main, library!.loaded);
const others = versions.slice(1), isExpanded = expanded.includes(family), id = `${groupsId}-${index}`;
@@ -5,10 +5,11 @@ import { FloatingMenu } from './FloatingMenu';
import { DictationControl } from './DictationControl';
import { Btn, Dialog, Icon, ErrorNotice } from './ui';
import { MOD } from '../lib/commands';
import { setChatPreferences, useStore } from '../lib/store';
import { pushToast, setChatPreferences, useStore } from '../lib/store';
import { artifactFromUrl, type PreviewArtifact } from '../lib/artifacts';
import type { ChatMode, Session } from '../lib/types';
import { attachChatPhoto } from '../lib/chat-photos';
import { uploadChatFile, FILE_ACCEPT } from '../lib/chat-attach-file';
import { useNative } from '../lib/native';
import { pictureStatus } from '../lib/pictures';
const modes: {
@@ -91,6 +92,12 @@ export function ComposerControls({ session, onPreview, onAttach, onError, onSlas
await attachChatPhoto(file, session?.id);
continue;
}
if (!directory && session?.id && /\.pdf$/i.test(file.name) && file.size <= 12 * 1024 * 1024) {
// A PDF goes to the chat's folder and is named in the next message; "Open a file" still previews it.
const saved = await uploadChatFile(file, session.id);
pushToast('success', 'PDF added', `${saved.name} is in this chat. Ask about it in your next message.`);
continue;
}
if (!directory && (photoTypes.test(file.name) || /\.pdf$/i.test(file.name))) {
await openFile(file);
if (photoTypes.test(file.name))
@@ -157,7 +164,7 @@ export function ComposerControls({ session, onPreview, onAttach, onError, onSlas
return <>
{tr(!remoteModel && nativeModel?.metrics?.mcp?.servers?.length ? <button className="composer-choice" aria-label={tr("Use connected MCP tools")} aria-pressed={session?.preferences?.useTools === true} onClick={() => session?.preferences?.useTools ? setChatPreferences(session.id, { useTools: false }) : setToolsOpen(true)}><Icon name="skills" size={16}/>{tr("Tools")}{tr(session?.preferences?.useTools ? ' on' : '')}</button> : null)}
<Dialog open={toolsOpen} compact title={tr("Enable connected tools?")} onClose={() => setToolsOpen(false)} actions={<><Btn onClick={() => setToolsOpen(false)}>{tr("Keep tools off")}</Btn><Btn variant="primary" onClick={() => { setChatPreferences(session?.id, { useTools: true, mode: 'bypass' }); setToolsOpen(false); }}>{tr("Enable tools")}</Btn></>}><p>{tr("Connected servers can act on the resources you configured. Calls run automatically when the model chooses them, and their results appear in chat.")}</p><p className="small muted">{tr("This switches this conversation to Bypass permissions. Servers: ")}{tr((nativeModel?.metrics?.mcp?.servers ?? []).map((s: any) => s.name).join(', '))}.</p></Dialog>
<input ref={files} type="file" multiple hidden accept="image/*,.pdf,.txt,.md,.markdown,.csv,.ts,.tsx,.js,.jsx,.py,.json,.css,.rs,.go,.sh,.html,.htm,.yaml,.yml,.toml,.xml,.log" onChange={e => { void addFiles(Array.from(e.target.files ?? [])); e.target.value = ''; }}/>
<input ref={files} type="file" multiple hidden accept={FILE_ACCEPT} onChange={e => { void addFiles(Array.from(e.target.files ?? [])); e.target.value = ''; }}/>
<input ref={folder} type="file" multiple hidden onChange={e => { void addFiles(Array.from(e.target.files ?? []), true); e.target.value = ''; }}/>
<input ref={previewFile} type="file" hidden onChange={e => { void openFile(e.target.files?.[0]); e.target.value = ''; }}/>
<button ref={trigger} className="composer-add" aria-label={tr("Add or open")} title={tr("Add or open")} disabled={reading || streaming} aria-haspopup="menu" aria-expanded={menu} onClick={() => { setPreferences(null); setMenu(v => !v); }}><Icon name="plus" size={22}/></button>
+28
View File
@@ -0,0 +1,28 @@
import { useEffect, useState } from 'react';
import { parseDecimal, partialDecimal } from '../lib/decimal';
/** A number field that lets the person type a decimal point: it keeps the text while it is typed and tells the page the number once it is one (#3736). */
export function DecimalInput({ value, onChange, ariaLabel, className = 'st-input', placeholder, disabled }: {
value: number | null | undefined;
onChange: (value: number | null) => void;
ariaLabel: string;
className?: string;
placeholder?: string;
disabled?: boolean;
}) {
const [text, setText] = useState(value === null || value === undefined ? '' : String(value));
// A change that came from elsewhere (Restore saved, another tab) replaces the text, but never while the typed text already means this number.
useEffect(() => {
const shown = text === '' ? null : parseDecimal(text);
if ((value ?? null) !== shown) setText(value === null || value === undefined ? '' : String(value));
}, [value]);
return <input className={className} aria-label={ariaLabel} inputMode="decimal" disabled={disabled} placeholder={placeholder} value={text}
onChange={e => {
const next = e.target.value;
if (!partialDecimal(next)) return;
setText(next);
if (next.trim() === '') onChange(null);
else { const n = parseDecimal(next); if (n !== null) onChange(n); }
}}
onBlur={() => { const n = text.trim() === '' ? null : parseDecimal(text); setText(n === null ? '' : String(n)); }}/>;
}
+1 -10
View File
@@ -6,16 +6,7 @@ import { Icon } from './ui';
/** Files dropped on the chat go where the attach button would put them: the composer reads the 'strata:drop-files' event. */
export const DROP_EVENT = 'strata:drop-files';
/** The pictures on the clipboard, named so each one is told apart in the composer (a pasted screenshot arrives as "image.png"). */
export function pastedFiles(data: DataTransfer | null | undefined): File[] {
const out: File[] = [];
for (const file of Array.from(data?.files ?? [])) {
if (!file.type.startsWith('image/')) continue;
const extension = file.type.split('/')[1]?.replace('jpeg', 'jpg').replace(/[+].*/, '') || 'png';
out.push(new File([file], `Pasted picture ${new Date().toLocaleTimeString().replace(/[^0-9]+/g, '-').replace(/-$/, '')}${out.length ? `-${out.length + 1}` : ''}.${extension}`, { type: file.type }));
}
return out;
}
export { pastedFiles } from '../lib/pasted-files';
const hasFiles = (e: React.DragEvent) => Array.from(e.dataTransfer?.types ?? []).includes('Files');
/** A file dropped outside the chat must not take the window to that file: the page ignores it (called once at start). */
+173
View File
@@ -0,0 +1,173 @@
import { tr } from '../lib/i18n';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Brand } from './Brand';
import { Bar, Btn, Icon } from './ui';
import { api } from '../lib/client';
import { useDownloadEta } from '../lib/download-eta';
import type { NativeJob } from '../lib/native';
import { megabytes } from '../lib/format';
/** What the host says about this PC's engine (GET /api/engine/first-run). */
export interface FirstRunInfo {
status: 'offer' | 'none' | 'offline' | 'error' | 'installed' | 'wrong-make' | 'unavailable';
gpus: { name: string; vramGb: number | null }[];
driver?: string | null;
offer?: { version: string; bytes: number; variant: string; backend: string | null };
message?: string | null;
reasons?: string[];
details?: string;
freeBytes?: number;
}
export const mb = megabytes;
/**
* Whether this PC still needs its engine. Reads only Control's local answer (no network): the engine is missing and the engine that came
* with the app is not being unpacked. `null` while that is not known yet.
*/
export function useEngineNeeded(): boolean | null {
const [needed, setNeeded] = useState<boolean | null>(null);
useEffect(() => {
let live = true;
const ask = () => api.get<{ engine?: { usable?: boolean } | null; engineSetup?: boolean }>('/api/control/status').then(s => {
if (!live) return;
if (s.engineSetup) return setNeeded(null);
setNeeded(!!s.engine && s.engine.usable === false);
}, () => { if (live) setNeeded(false); });
void ask();
const timer = setInterval(() => void ask(), 3000);
return () => { live = false; clearInterval(timer); };
}, []);
return needed;
}
function gpuLine(info: FirstRunInfo) {
// a processor's own tiny graphics (no memory of its own) is not "a card"
const first = info.gpus.find(g => (g.vramGb ?? 0) >= 1) ?? info.gpus[0];
if (!first) return '';
const vram = first.vramGb ? `, ${Math.round(first.vramGb)} GB` : '';
const driver = info.driver ? `, driver ${info.driver}` : '';
return `${first.name}${vram}${driver}`;
}
/**
* The first-run card: the engine this PC needs, one button to download it, progress, and the offline way (choose a downloaded file).
* Nothing is downloaded before a button is pressed.
*/
export function EngineFirstRun({ onDone }: { onDone: () => void }) {
const [info, setInfo] = useState<FirstRunInfo | null>(null);
const [job, setJob] = useState<NativeJob | null>(null);
const [busy, setBusy] = useState(false), [problem, setProblem] = useState('');
const [finished, setFinished] = useState(false);
const file = useRef<HTMLInputElement>(null);
const started = useRef(false);
const look = useCallback(() => {
setInfo(null);
setProblem('');
api.get<FirstRunInfo>('/api/engine/first-run').then(setInfo, e => setInfo({ status: 'error', gpus: [], message: e instanceof Error ? e.message : String(e) }));
}, []);
useEffect(() => { look(); }, [look]);
// follow the download / install job
useEffect(() => {
const timer = setInterval(() => {
if (!started.current) return;
api.get<{ job: NativeJob | null }>('/api/engine/job').then(r => {
setJob(r.job);
if (r.job?.state === 'done') setFinished(true);
if (r.job && r.job.state !== 'running') started.current = false;
}, () => { });
}, 700);
return () => clearInterval(timer);
}, []);
async function start() {
setBusy(true);
setProblem('');
try {
started.current = true;
setJob(await api.post<NativeJob>('/api/native/operations', { kind: 'update-engine', args: {} }));
}
catch (e) {
started.current = false;
setProblem(e instanceof Error ? e.message : String(e));
}
finally {
setBusy(false);
}
}
async function chosen(files: FileList | null) {
const picked = files?.[0];
if (!picked) return;
setBusy(true);
setProblem('');
try {
const response = await fetch('/api/engine/from-file', { method: 'POST', headers: { 'x-file-name': picked.name }, body: picked });
const body = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(body?.error ?? `The file could not be used (${response.status}).`);
started.current = true;
setJob(body as NativeJob);
}
catch (e) {
setProblem(e instanceof Error ? e.message : String(e));
}
finally {
setBusy(false);
if (file.current) file.current.value = '';
}
}
const running = job?.state === 'running';
const failed = !!job && (job.state === 'error' || job.state === 'cancelled');
const percent = Number(job?.result?.install?.download?.percent ?? 0);
const bytes = info?.offer?.bytes ?? 0;
const doneMb = Math.round(mb(bytes) * percent / 100);
const eta = useDownloadEta(running && bytes ? { done_gb: bytes * percent / 100 / 1e9, remaining_gb: bytes * (100 - percent) / 100 / 1e9 } : undefined, job?.id);
const cards = (info?.gpus ?? []).filter(g => (g.vramGb ?? 0) >= 1).length;
const hidden = <input ref={file} type="file" accept=".gz,.tgz,.xz,.zip" hidden onChange={e => void chosen(e.target.files)}/>;
const pick = (label: string) => <button type="button" className="onboard-continue-anyway" disabled={busy || running} onClick={() => file.current?.click()}>{tr(label)}</button>;
let body;
if (finished) {
body = <><div className="onboard-welcome-copy"><h1 tabIndex={-1} data-initial-focus>{tr("The engine is ready.")}</h1><p>{tr("Next: pick a model.")}</p></div>
<footer className="onboard-footer onboard-footer--welcome"><Btn variant="primary" onClick={onDone}>{tr("Continue")}<Icon name="chevron" size={16}/></Btn></footer></>;
}
else if (running) {
body = <><div className="onboard-welcome-copy"><h1 tabIndex={-1} data-initial-focus>{tr("Downloading the engine")}</h1>
<p>{bytes && job?.result?.install?.download ? `${doneMb} of ${mb(bytes)} MB${eta ? ' · ' + eta : ''}` : tr(job?.summary ?? 'Starting…')}</p></div>
<div className="onboard-error" style={{ maxWidth: 520, margin: '0 auto', width: '100%' }}><Bar value={percent} label="Downloading the engine"/><p className="small muted" style={{ textAlign: 'center' }}>{tr("You can close Strata; it continues where it stopped.")}</p></div></>;
}
else if (!info) {
body = <div className="onboard-welcome-copy"><h1 tabIndex={-1} data-initial-focus>{tr("Welcome to Strata")}</h1><p role="status">{tr("Looking at your graphics card…")}</p></div>;
}
else {
const card = gpuLine(info);
const found = card ? <p>{tr("Strata found your graphics card:")}<br/><strong>{card}</strong>{cards > 1 ? <><br/><span className="small muted">{tr("{0} cards found. Strata uses the first one for the engine; change it later in Settings.", [cards])}</span></> : null}</p>
: <p>{tr("Strata did not recognise a supported graphics card.")}</p>;
let main, actions;
if (info.status === 'offer' && info.offer) {
main = <>{found}<p>{tr("To run models on it, Strata needs one download:")}<br/><strong>{tr("The engine for this card")}: {mb(info.offer.bytes)} MB</strong></p>
{info.freeBytes !== undefined && info.freeBytes < info.offer.bytes * 2.5 ? <p className="small inline-error">{tr("The engine needs about {0} GB of free space on this drive and only {1} GB is free. Free some space, then continue.", [Math.ceil(info.offer.bytes * 2.5 / 1e9), (info.freeBytes / 1e9).toFixed(1)])}</p> : null}
<p className="small muted">{tr("It comes from Strata’s own update server and is checked before it is used. Your models and chats are never touched by it.")}</p></>;
actions = <><Btn variant="primary" disabled={busy} onClick={() => void start()}>{tr(failed ? 'Continue' : 'Download the engine')}</Btn>{pick("I already have the file…")}</>;
}
else if (info.status === 'offline') {
main = <>{found}<p>{tr("Strata could not reach github.com. Check your connection and try again, or download the engine file on another PC and choose it here.")}</p></>;
actions = <><Btn variant="primary" disabled={busy} onClick={look}>{tr("Try again")}</Btn>{pick("Choose a downloaded file…")}</>;
}
else if (info.status === 'wrong-make') {
main = <>{found}<p>{info.message ?? tr("The engine installed here is for another make of graphics card.")}</p></>;
actions = <><Btn variant="primary" disabled={busy} onClick={look}>{tr("Try again")}</Btn>{pick("Choose a downloaded file…")}</>;
}
else {
main = <>{found}<p>{tr("Strata can’t pick an engine for this PC from its update list.")}</p>{info.message ? <p className="small muted">{info.message}</p> : null}{(info.reasons ?? []).length ? <ul className="small muted" style={{ textAlign: 'left', display: 'inline-block' }}>{(info.reasons ?? []).map(r => <li key={r}>{r}</li>)}</ul> : null}</>;
actions = <><Btn variant="primary" disabled={busy} onClick={look}>{tr("Try again")}</Btn>{pick("Choose a downloaded file…")}<button type="button" className="onboard-continue-anyway" onClick={onDone}>{tr("Continue without an engine")}</button></>;
}
body = <><div className="onboard-welcome-copy"><h1 tabIndex={-1} data-initial-focus>{tr("Welcome to Strata")}</h1>{main}</div>
{failed ? <div className="onboard-error" style={{ maxWidth: 560, margin: '0 auto' }}><p role="alert" className="small inline-error">{job?.error ?? job?.summary}</p><p className="small muted">{tr("The download stopped. Continue picks it up where it stopped.")}</p></div> : null}
{problem ? <div className="onboard-error" style={{ maxWidth: 560, margin: '0 auto' }}><p role="alert" className="small inline-error">{problem}</p></div> : null}
<footer className="onboard-footer onboard-footer--welcome">{actions}</footer></>;
}
return <div className="setup guided-onboarding" data-step="engine">{hidden}<header className="onboard-header"><Brand variant="lockup" height={22}/></header>{body}</div>;
}
+14 -3
View File
@@ -22,6 +22,15 @@ type Choices = NonNullable<NonNullable<SetupState['choices']>['onboarding']>;
function radioKeys(event: KeyboardEvent<HTMLDivElement>) { if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key))
return; event.preventDefault(); const items = Array.from(event.currentTarget.querySelectorAll<HTMLButtonElement>('[role=radio]')); const current = items.indexOf(event.target as HTMLButtonElement); const next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 : (current + (['ArrowLeft', 'ArrowUp'].includes(event.key) ? -1 : 1) + items.length) % items.length; items[next]?.focus(); items[next]?.click(); }
const steps = ['Welcome', 'Your model', 'Prepare', 'Make it yours'];
/** What a person is told before choosing DeepSeek: the size, where it runs, how fast, what it needs. Every line is a warning or a fact; nothing here stops the download. */
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>
{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>;
}
export function GuidedSetup() {
const native = useNative(), setup = useStore(s => s.setup), hardware = useStore(s => s.hardware);
const families = nativeCatalogModels(native?.catalog?.families ?? []);
@@ -132,7 +141,8 @@ export function GuidedSetup() {
setTuneJobId(tuneJob.id);
void persist(4, { tuneJobId: tuneJob.id }).catch(e => setError(e.message));
} }, [step, installJob?.id, tuneJob?.id]);
const values = { guided: true, family: version?.sourceFamily, model: version?.model, context, vision: version?.images ? 'yes' : 'no', kv: 'int8', low_ram: 'auto', loadAfter: false, multiple: false, ...(directory ? { data_dir: directory } : {}) };
const fixedContext = version?.multi_file ? 8192 : null; // the one context DeepSeek's engine was checked at
const values = { guided: true, family: version?.sourceFamily, model: version?.model, context: fixedContext ?? context, vision: version?.images ? 'yes' : 'no', kv: 'int8', low_ram: 'auto', loadAfter: false, multiple: false, ...(directory ? { data_dir: directory } : {}) };
async function review() {
await perform(async () => {
setInstallJobId('');
@@ -154,7 +164,7 @@ export function GuidedSetup() {
}
async function tune() {
await perform(async () => {
const result = await nativeAction<NativeJob>('operations', { kind: 'tune', args: { guided: true, model: version?.id, profile, calibrate, vision: version?.images && vision ? 'yes' : 'no', multiple: false } });
const result = await nativeAction<NativeJob>('operations', { kind: 'tune', args: { guided: true, model: version?.id, profile, calibrate: calibrate && !version?.multi_file, vision: version?.images && vision ? 'yes' : 'no', multiple: false } });
setTuneJobId(result.id);
await persist(4, { tuneJobId: result.id });
});
@@ -201,9 +211,10 @@ export function GuidedSetup() {
<div className="onboard-stage-heading"><h1 ref={titleRef} tabIndex={-1} data-initial-focus>{tr(title)}</h1>{tr(step === 2 ? <p>{tr("You can always change your models later.")}</p> : step === 4 && tuneJob?.state === 'running' ? <p>{tr("We\u2019re measuring, applying your preference, and getting ready to chat.")}</p> : null)}</div>
{tr(step === 2 ? <div className="onboard-model-stage">
{tr(selectedFamily ? <>
<div className="onboard-model-top"><div className="onboard-model-picker"><span className="onboard-control-label">{tr("Model")}</span><div><span className="onboard-model-symbol"><Icon name="models" size={23}/></span><Select ariaLabel={tr("Setup model")} value={selectedFamily.family} options={families.map(f => ({ value: f.family, label: f.title }))} onChange={value => chooseNative(value, '')}/></div></div><div className="onboard-context"><span className="onboard-control-label">{tr("Conversation context")}</span><Select ariaLabel={tr("Setup conversation context")} value={String(context)} options={(native?.catalog?.contexts ?? [32768]).map(n => ({ value: String(n), label: `${n / 1024}K` }))} onChange={value => setContext(Number(value))}/></div></div>
<div className="onboard-model-top"><div className="onboard-model-picker"><span className="onboard-control-label">{tr("Model")}</span><div><span className="onboard-model-symbol"><Icon name="models" size={23}/></span><Select ariaLabel={tr("Setup model")} value={selectedFamily.family} options={families.map(f => ({ value: f.family, label: f.title }))} onChange={value => chooseNative(value, '')}/></div></div><div className="onboard-context"><span className="onboard-control-label">{tr("Conversation context")}</span><Select ariaLabel={tr("Setup conversation context")} value={String(version?.multi_file ? 8192 : context)} options={(version?.multi_file ? [8192] : native?.catalog?.contexts ?? [32768]).map(n => ({ value: String(n), label: `${n / 1024}K` }))} onChange={value => setContext(Number(value))}/></div></div>
<OnboardingMemory hardware={hardware} native={native?.hardware}/>{tr(!version || version.needsNewerEngine ? <p className="small muted">{tr("This model needs a newer engine. Update your engine or import a local GGUF for this model.")}</p> : null)}
<div className="onboard-version-heading"><strong>{tr("Choose a version")}</strong><span>{tr(version ? `${catalogVersionLabel(version)} selected` : '')}</span></div>
{tr(version?.multi_file ? <DeepSeekNote version={version} plan={plan}/> : null)}
<div className="onboard-versions" role="radiogroup" aria-label={tr("Model version")} onKeyDown={radioKeys}>{tr(cards.map(v => versionCard(v)))}</div>
{tr(others.length ? <div className="onboard-more-versions"><VersionPicker versions={others} selectedId={version?.id ?? ''} hardware={hardware ? { ram_gb: native?.hardware?.ram_gb ?? hardware.memory.totalGb, vram_gb: Math.max(0, ...(native?.hardware?.gpus ?? []).filter(g => g.usable !== false).map(g => g.vram_gb)) } : null} downloaded={readyIds} showRecommended={false} moreLabel="More versions" onSelect={id => { setSize(id); setPlan(null); }} onOwnGguf={() => void ownModel()}/></div> : null)}
<div className="onboard-folder"><Icon name="folder" size={20}/><div><strong>{tr(directory ? 'Your model folder' : 'Already downloaded the model?')}</strong><small>{tr(directory || "Choose the folder and we'll start your models with Strata.")}</small></div><Btn size="sm" onClick={() => setFolderOpen(v => !v)}>{tr(folderOpen ? 'Close' : directory ? 'Change' : 'Choose folder')}</Btn></div>
+28 -2
View File
@@ -1,4 +1,5 @@
import { tr } from '../lib/i18n';
import { copyText } from '../lib/clipboard';
/** Safe, dependency-free rendering of the Markdown structures used in answers. */
import { Fragment, type ReactNode } from 'react';
import { Btn, IconBtn } from './ui';
@@ -47,6 +48,20 @@ function inline(text: string, onOpenLink?: (url: string) => void, imageSession?:
: <Fragment key={p}>{inlineMarks(part.text, onOpenLink, imageSession)}</Fragment>)}</Fragment>;
});
}
/** Does this line continue the paragraph above it, or does it start a block of its own? */
export function continuesParagraph(lines: string[], at: number): boolean {
const line = lines[at];
if (!line.trim() || /^\s*```/.test(line) || /^(#{1,6})\s+(.+)$/.test(line) || /^\s*[-*+]\s+\S/.test(line) || /^\s*\d+[.)]\s+\S/.test(line) || /^>\s?/.test(line))
return false;
if (line.includes('|') && at + 1 < lines.length && /^\s*\|?\s*:?-{3,}/.test(lines[at + 1]))
return false;
return !displayBlock(lines, at);
}
/** Copy a code block. The toast store needs a browser, so it is loaded here, when someone copies: the renderer itself must render without one. */
async function copyCode(text: string) {
const { pushToast } = await import('../lib/store');
await copyText(text, 'Code', { toast: pushToast });
}
export function Markdown({ text, onOpenLink, onPreviewHtml, imageSession }: {
text: string;
onOpenLink?: (url: string) => void;
@@ -69,7 +84,7 @@ export function Markdown({ text, onOpenLink, onPreviewHtml, imageSession }: {
while (i < lines.length && !/^\s*```/.test(lines[i]))
body.push(lines[i++]);
i++;
nodes.push(<div className="st-code" key={key}><div className="st-code__head"><span>{language || tr('code')}</span>{tr(/^html$/i.test(language) && onPreviewHtml ? <Btn size="sm" onClick={() => onPreviewHtml(body.join('\n'))}>{tr("Open preview")}</Btn> : null)}<IconBtn name="copy" title={tr("Copy code")} onClick={() => void navigator.clipboard.writeText(body.join('\n'))}/></div><SyntaxCode text={body.join('\n')} language={language}/></div>);
nodes.push(<div className="st-code" key={key}><div className="st-code__head"><span>{language || tr('code')}</span>{tr(/^html$/i.test(language) && onPreviewHtml ? <Btn size="sm" onClick={() => onPreviewHtml(body.join('\n'))}>{tr("Open preview")}</Btn> : null)}<IconBtn name="copy" title={tr("Copy code")} onClick={() => void copyCode(body.join('\n'))}/></div><SyntaxCode text={body.join('\n')} language={language}/></div>);
continue;
}
const block = displayBlock(lines, i);
@@ -113,8 +128,19 @@ export function Markdown({ text, onOpenLink, onPreviewHtml, imageSession }: {
i++;
continue;
}
nodes.push(<p key={i}>{tr(inline(line, onOpenLink, imageSession))}</p>);
// One paragraph: a single newline inside it is a soft break (a space); two spaces or a backslash at the end of a line is a hard break (#3818).
const key = i, parts: ReactNode[] = [];
let current = line;
i++;
while (true) {
const hard = /( {2,}|\\)$/.test(current);
parts.push(<Fragment key={parts.length}>{tr(inline(current.replace(/( {2,}|\\)$/, ''), onOpenLink, imageSession))}</Fragment>);
if (i >= lines.length || !continuesParagraph(lines, i))
break;
parts.push(hard ? <br key={`b${parts.length}`}/> : ' ');
current = lines[i++].trimStart();
}
nodes.push(<p key={key}>{tr(parts)}</p>);
}
return <>{tr(nodes)}</>;
}
+7 -4
View File
@@ -104,7 +104,7 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
// Drafting (which drafter guesses ahead): DFlash2 is for Qwen3.8-Flash-Next only (the qwen and gyro families), so no other model is offered it.
const draftingChoices = selectedSize ? draftingOptions(selectedSize.sourceFamily ?? selectedFamily?.family, native?.capabilities?.dflash2) : null;
const draftingWords: Record<string, string> = { auto: 'Auto (recommended)', mtp: 'MTP', dflash2: 'DFlash2', off: 'Off' };
const values = useMemo(() => ({ family: selectedSize?.sourceFamily, model: selectedSize?.model, context, vision: selectedSize?.images ? vision : 'no', kv, low_ram: lowRam, resourceProfile, ...(cardsGpus.length > 1 ? { gpus: cardsGpus.join(',') } : cardsGpus.length === 1 ? { gpu: cardsGpus[0] } : {}), loadAfter: true, multiple: expert && multiple }), [selectedSize?.sourceFamily, selectedSize?.model, context, vision, kv, lowRam, resourceProfile, cardsGpus.join(','), expert, multiple]);
const values = useMemo(() => ({ family: selectedSize?.sourceFamily, model: selectedSize?.model, context: selectedSize?.multi_file ? 8192 : context, vision: selectedSize?.images ? vision : 'no', kv, low_ram: lowRam, resourceProfile, ...(cardsGpus.length > 1 ? { gpus: cardsGpus.join(',') } : cardsGpus.length === 1 ? { gpu: cardsGpus[0] } : {}), loadAfter: true, multiple: expert && multiple }), [selectedSize?.sourceFamily, selectedSize?.model, context, vision, kv, lowRam, resourceProfile, cardsGpus.join(','), expert, multiple]);
const downloadValues = { ...values, ...(draftingChoices ? { drafting } : {}), vision: selectedSize?.images && vision !== 'no' ? 'yes' : 'no', loadVision: selectedSize?.images ? vision : 'no' };
useEffect(() => { if (!native?.catalog || family)
return; const current = native.instances.find(i => i.id === native.defaultId); const rec = native.catalog.recommendation ?? { family: null, model: null }; const loadedFamily = families.find(f => f.sizes.some(s => s.id === current?.model)); const have = new Set((native.installed?.models ?? []).filter(m => m.ready).map(m => m.model)); const installedFamily = families.find(f => f.sizes.some(s => have.has(s.id))); const f = loadedFamily ?? installedFamily ?? families.find(f => f.family === rec.family || f.sizes.some(s => s.sourceFamily === rec.family)) ?? families[0]; if (f) {
@@ -138,8 +138,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);
@@ -204,13 +207,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) : 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} 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>
+20 -10
View File
@@ -1,3 +1,4 @@
import { DecimalInput } from './DecimalInput';
import { tr } from '../lib/i18n';
import {nativeSettingsResponse,nativeSettingsValues} from '../lib/native-settings-response';
import { describeChange, type TunedChange } from '../lib/tune-values';
@@ -5,12 +6,15 @@ import { useEffect, useState } from 'react';
import { api } from '../lib/client';
import { refreshNative, useNative, type NativeField } from '../lib/native';
import { Btn, ErrorNotice, Select } from './ui';
import { speedOptions } from '../lib/deepseek-speed';
import type { RunCard } from '../lib/native-fit';
import { NativeMcpEditor } from './NativeMcpEditor';
export function NativeSettings({ model }: {
model: string;
}) {
const native = useNative();
const [revision,setRevision]=useState(0);
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('');
useEffect(() => {
@@ -25,9 +29,11 @@ export function NativeSettings({ model }: {
values: Record<string, any>;
mcp: Record<string, any>;
tuned?: { preset: { name: string } | null; changed: TunedChange[] } | null;
capabilities?: { dsExperts?: boolean };
}>(`/api/native/models/${model}/settings`).then(input => {
if (!active)
return;
setDsExperts(!!input.capabilities?.dsExperts);
setTuned(input.tuned ?? null);
const value = nativeSettingsResponse(input);
setValues(value.values);
@@ -90,16 +96,9 @@ export function NativeSettings({ model }: {
<summary>{tr('Native engine settings')}</summary>
{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 => `${tr(f.label)} ${f.id} ${tr(f.group)}`.toLowerCase().includes(query.toLowerCase()));
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()));
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><input className="st-input" aria-label={tr("Native {0}", [label])} inputMode="decimal" value={values.sampling?.[id] ?? ''} onChange={e => {
const sampling = { ...values.sampling };
if (e.target.value === '')
delete sampling[id];
else
sampling[id] = Number(e.target.value);
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))}<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>;
}
@@ -112,9 +111,20 @@ function NativeFieldInput({ field, value, onChange }: {
return <NativeAliases field={field} value={value} onChange={onChange}/>;
if (field.type === 'boolean')
return <Select disabled={field.supported === false} ariaLabel={tr(field.label)} value={value === null || value === undefined ? '' : String(value)} options={[{ value: '', label: 'Default' }, { value: 'true', label: 'On' }, { value: 'false', label: 'Off' }]} onChange={v => onChange(v === '' ? null : v === 'true')}/>;
if (field.id === 'ds_experts')
return <DeepSeekSpeed field={field} value={value} onChange={onChange}/>;
if (field.type === 'enum')
return <Select disabled={field.supported === false} ariaLabel={tr(field.label)} value={value ?? ''} options={[{ value: '', label: 'Default' }, ...(field.choices ?? []).map(v => ({ value: v, label: v }))]} onChange={v => onChange(v || null)}/>;
return <input disabled={field.supported === false} className="st-input" aria-label={tr(field.label)} inputMode={field.type === 'number' ? 'numeric' : undefined} placeholder={tr("Default")} value={Array.isArray(value) ? value.join(', ') : value ?? ''} onChange={e => onChange(e.target.value === '' ? null : field.type === 'array' ? e.target.value.split(',').map(s => s.trim()).filter(Boolean) : field.type === 'number' ? Number(e.target.value) : e.target.value)}/>;
if (field.type === 'number')
return <DecimalInput disabled={field.supported === false} ariaLabel={tr(field.label)} placeholder={tr("Default")} value={value === null || value === undefined || value === '' || !Number.isFinite(Number(value)) ? null : Number(value)} onChange={onChange}/>;
return <input disabled={field.supported === false} className="st-input" aria-label={tr(field.label)} placeholder={tr("Default")} value={Array.isArray(value) ? value.join(', ') : value ?? ''} onChange={e => onChange(e.target.value === '' ? null : field.type === 'array' ? e.target.value.split(',').map(s => s.trim()).filter(Boolean) : field.type === 'number' ? Number(e.target.value) : e.target.value)}/>;
}
/** DeepSeek's Speed choice. The engine decides the default itself (--experts auto); the labels name the same default (lib/deepseek-speed.ts). */
function DeepSeekSpeed({ field, value, onChange }: { field: NativeField; value: any; onChange: (v: any) => void }) {
const native = useNative();
const options = speedOptions(native?.hardware?.gpus as RunCard[] | undefined).map(o => ({ ...o, label: tr(o.label) }));
return <Select disabled={field.supported === false} ariaLabel={tr(field.label)} value={value === null || value === undefined ? '' : String(value)}
options={options} onChange={v => onChange(v === '' ? null : v)}/>;
}
function NativeAliases({ field, value, onChange }: {
field: NativeField;
+13 -2
View File
@@ -8,9 +8,20 @@ export function OnboardingMemory({ hardware, native }: {
hardware: Hardware | null;
native: NativeState['hardware'] | undefined;
}) {
const gpus = hardware ? hardware.gpus.filter(gpu => gpu.backend !== 'cpu').map(gpu => ({ id: gpu.id ?? String(gpu.index), name: gpu.name, memory: gpu.vramGb, shared: gpu.unified })) : native?.gpus.map(gpu => ({ id: String(gpu.index), name: gpu.name, memory: gpu.vram_gb, shared: false })) ?? [];
// The Windows host probe (Win32_VideoController) cannot read the VRAM of AMD cards and reports 0 for them; the engine's own list has
// the real size. For a dedicated card the host reports as 0, take the engine's figure for the card of the same name, or for the
// card in the same position when both lists have the same length. A card the engine does not list stays at 0.
const nativeGpus = native?.gpus ?? [];
const hostGpus = hardware ? hardware.gpus.filter(gpu => gpu.backend !== 'cpu') : [];
const engineVram = (name: string, position: number) => {
const norm = (value: string) => value.toLowerCase().replace(/[^a-z0-9]/g, '');
const byName = nativeGpus.find(g => norm(g.name).includes(norm(name)) || norm(name).includes(norm(g.name)));
return (byName ?? (nativeGpus.length === hostGpus.length ? nativeGpus[position] : undefined))?.vram_gb || 0;
};
const gpus = hardware ? hostGpus.map((gpu, position) => ({ id: gpu.id ?? String(gpu.index), name: gpu.name, memory: gpu.unified || gpu.vramGb > 0 ? gpu.vramGb : engineVram(gpu.name, position), shared: gpu.unified })) : nativeGpus.map(gpu => ({ id: String(gpu.index), name: gpu.name, memory: gpu.vram_gb, shared: false }));
const dedicatedCount = gpus.filter(gpu => !gpu.shared).length;
const memory = hardware ? memoryInventory(hardware) : native ? { ram: native.ram_gb, vram: native.gpus.reduce((sum, gpu) => sum + Math.max(0, gpu.vram_gb || 0), 0), total: 0, shared: false } : null;
const base = hardware ? memoryInventory(hardware) : null;
const memory = base ? (base.shared ? base : { ...base, vram: gpus.reduce((sum, gpu) => sum + (gpu.shared ? 0 : Math.max(0, gpu.memory || 0)), 0) }) : native ? { ram: native.ram_gb, vram: native.gpus.reduce((sum, gpu) => sum + Math.max(0, gpu.vram_gb || 0), 0), total: 0, shared: false } : null;
if (!memory)
return <div className="onboard-hardware" role="group" aria-label={tr("Computer memory")}><div className="onboard-memory-intro"><Icon name="memory" size={18}/><span>{tr("Reading memory\u2026")}</span></div></div>;
const total = memory.ram + memory.vram;
+1 -1
View File
@@ -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);
+6 -5
View File
@@ -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) {
@@ -71,14 +72,14 @@ export function SettingsUpdates() {
const labels: Record<SoftwareUpdate['status'], string> = { available: 'Update available', current: 'Up to date', unavailable: 'Check unavailable', error: 'Check failed', 'no-releases': 'No release yet' };
return <div className="settings-version" data-update={update?.status}>
<Icon name={name === 'Strata' ? 'layers' : 'engine'} size={20}/>
<span><strong>{tr(name)}</strong><small>{tr(version ?? 'Version unavailable')}</small>{tr(!checking && update ? <small className="settings-version__message">{tr(update.message)}</small> : null)}</span>
<span><strong>{tr(name)}</strong><small>{tr(version ?? 'Version unavailable')}</small>{tr(!checking && update ? <small className="settings-version__message">{tr(update.message)}</small> : null)}{tr(!checking && name === 'Strata' && update?.details ? <details className="native-settings-diagnostic"><summary className="small">{tr("Details")}</summary><p className="small muted">{update.details}</p></details> : null)}</span>
{tr(!checking && update?.status === 'available' ? (native?.available ? <Btn size="sm" variant="primary" disabled={!!active || (name === 'Engine' && !native.installed?.models.length && !native.capabilities.componentUpdates)} onClick={() => setConfirm(name === 'Strata' ? 'update-strata' : 'update-engine')}>{tr("Update available")}</Btn> : update.releaseUrl ? <a className="st-btn st-btn--secondary st-btn--sm" href={update.releaseUrl} target="_blank" rel="noreferrer" title={tr("Open the release page to update this installation")}>{tr("Update available")}<Icon name="download" size={15}/></a> : <span className="settings-version__label">{tr(labels[update.status])}</span>) : <span className="settings-version__label">{tr(checking ? 'Checking…' : update ? labels[update.status] : version === 'Not installed' ? 'Not installed' : 'Installed')}</span>)}
</div>;
}
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.3 · Engine 0.1.48": the app's own version first, the engine's beside it (About). */
/** "Strata 0.2.6 · Engine 0.1.51": 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>;
}
+11
View File
@@ -0,0 +1,11 @@
/** A file that is not a picture or plain text (a PDF) is saved by the host in the chat's folder and named in the next message, so the assistant can open it (#3838). */
export async function uploadChatFile(file: File, sessionId: string, fetchFn: typeof fetch = fetch): Promise<{ name: string }> {
const host = (globalThis as { __STRATA_HOST__?: string }).__STRATA_HOST__ ?? '';
const res = await fetchFn(`${host}/api/chat/upload?sessionId=${encodeURIComponent(sessionId)}&name=${encodeURIComponent(file.name)}`, { method: 'POST', headers: { 'content-type': file.type || 'application/octet-stream' }, body: file });
const data = await res.json().catch(() => ({})) as { files?: { name: string; attached?: boolean }[]; error?: string };
if (!res.ok) throw new Error(data.error || 'The file could not be added. Try again.');
return { name: data.files?.find(f => f.attached)?.name ?? file.name };
}
/** Types the Linux file dialog needs next to the extensions (WebKitGTK reads only the type part of `accept`). */
export const FILE_ACCEPT = 'image/*,application/pdf,text/*,application/json,application/xml,.pdf,.txt,.md,.markdown,.csv,.ts,.tsx,.js,.jsx,.py,.json,.css,.rs,.go,.sh,.html,.htm,.yaml,.yml,.toml,.xml,.log';
+35
View File
@@ -0,0 +1,35 @@
/**
* Copy text for the person. The browser clipboard call can be refused (the desktop web view asks for permission, or has no clipboard
* object); the older copy command through a hidden field still works there. The answer is always a toast: "Copied", or one plain
* sentence and what to do next (#3731).
*/
export interface ClipboardDeps {
clipboard?: { writeText(text: string): Promise<void> } | null;
legacyCopy?: (text: string) => boolean;
toast: (tone: 'success' | 'warn', title: string, body?: string) => void;
}
function legacyCopy(text: string): boolean {
if (typeof document === 'undefined') return false;
const field = document.createElement('textarea');
field.value = text;
field.setAttribute('readonly', '');
field.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none';
document.body.appendChild(field);
const active = document.activeElement as HTMLElement | null;
try {
field.select();
return document.execCommand('copy');
} catch { return false; }
finally { field.remove(); active?.focus?.(); }
}
export async function copyText(text: string, label: string, deps: ClipboardDeps): Promise<boolean> {
const clipboard = deps.clipboard === undefined ? (typeof navigator === 'undefined' ? null : navigator.clipboard) : deps.clipboard;
let done = false;
try { if (clipboard) { await clipboard.writeText(text); done = true; } } catch { /* the fallback is next */ }
if (!done) { try { done = (deps.legacyCopy ?? legacyCopy)(text); } catch { done = false; } }
if (done) deps.toast('success', 'Copied', label);
else deps.toast('warn', 'Couldn’t copy', 'Select the text and copy it with Ctrl+C.');
return done;
}
+9
View File
@@ -0,0 +1,9 @@
/** A number typed by a person: "0.95", ".95", "0,95" (a comma is a decimal point in many languages). null while it is not a whole number yet ("", ".", "0.", "-"). */
export function parseDecimal(text: string): number | null {
const t = String(text ?? '').trim().replace(',', '.');
if (!/^-?(\d+\.?\d*|\.\d+)$/.test(t) || t.endsWith('.')) return null;
const n = Number(t);
return Number.isFinite(n) ? n : null;
}
/** Whether what is typed could still become a number: the field keeps it while the person is in the middle of "0." */
export const partialDecimal = (text: string) => /^-?\d*[.,]?\d*$/.test(String(text ?? '').trim());
+30
View File
@@ -0,0 +1,30 @@
import type { RunCard } from './native-fit';
/**
* DeepSeek's Speed default, said the way the engine decides it (`--experts auto`, src/dsv41/ds4_experts_rule.hpp): Faster (5 experts a word) when
* a card in use is an integrated / unified-memory GPU (Strix Halo, gfx1151: the model shares the PC's memory), or when the cards in use total
* 16 GiB or less; otherwise Full quality (6). The app never decides: the engine does; this only names the same default beside the choice.
* The total is compared in whole MiB (a 16 GB card reports 16376 MiB = 15.99 GiB and counts as 16 GiB or less).
*/
export const FASTER_MIB = 16 * 1024;
export const isIntegrated = (card: RunCard) => !!(card.uma || card.integrated || (card.unified_gb ?? 0) > 0);
export function fasterByDefault(cards: readonly RunCard[] | null | undefined): boolean {
const used = (cards ?? []).filter(card => card.usable !== false);
if (!used.length) return false;
if (used.some(isIntegrated)) return true;
const mib = used.reduce((sum, card) => sum + Math.round((card.vram_gb ?? 0) * 1024), 0);
return mib > 0 && mib <= FASTER_MIB;
}
/** The three entries of the Speed select: "Faster" or "Full quality" says "(default on this PC)" when it is what the engine picks by itself. */
export function speedOptions(cards: readonly RunCard[] | null | undefined): { value: string; label: string }[] {
const faster = fasterByDefault(cards);
const mark = ' (default on this PC)';
return [
{ value: '', label: faster ? 'Default (Faster on this PC)' : 'Default (Full quality on this PC)' },
{ value: '5', label: 'Faster' + (faster ? mark : '') },
{ value: '6', label: 'Full quality' + (faster ? '' : mark) },
];
}
+8
View File
@@ -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;
}
+3
View File
@@ -128,3 +128,6 @@ export function formatModelName(model: {
export function userError(message: string): string {
return message.replace(/^\s*Fixture:\s*/i, '').replace(/^./, c => c.toUpperCase());
}
/** Decimal megabytes (1 MB = 1,000,000 bytes, at least 1): the same figure as the Terminal and the release page. The only size helper in MB; no binary units. */
export const megabytes = (bytes: number) => Math.max(1, Math.round(bytes / 1_000_000));
+12 -3
View File
@@ -15,6 +15,14 @@ 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 fix_en from '../locales/fix-en.json';
import fix_zh from '../locales/fix-zh.json';
import fix_ja from '../locales/fix-ja.json';
import fix_de from '../locales/fix-de.json';
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 base_zh from '../locales/zh.json';
import base_ja from '../locales/ja.json';
import base_de from '../locales/de.json';
@@ -27,8 +35,9 @@ export const LANGUAGES=[{value:'en',label:'English'},{value:'zh',label:'中文
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. */
const english:Record<string,string>={...baseEnglish,...tune_en,...tools_en};
const [zh,ja,de,es,ru,pt,ko]=[{...base_zh,...tune_zh,...tools_zh},{...base_ja,...tune_ja,...tools_ja},{...base_de,...tune_de,...tools_de},{...base_es,...tune_es,...tools_es},{...base_ru,...tune_ru,...tools_ru},{...base_pt,...tune_pt,...tools_pt},{...base_ko,...tune_ko,...tools_ko}] as Record<string,string>[];
/* 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 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);
@@ -45,7 +54,7 @@ const patterns=Object.entries(english).filter(([,value])=>/\{\d+\}/.test(value))
expression+=escape(value.slice(at));
return{id,slots,pattern:new RegExp('^'+expression+'$'),specificity:value.replace(/\{\d+\}/g,'').length};
}).sort((a,b)=>b.specificity-a.specificity);
const localizedTemplateSlots:Record<string,number[]>={s2473:[1,2],s0825:[0,1],s2455:[0],s2456:[0],s7400:[1]};
const localizedTemplateSlots:Record<string,number[]>={s1242:[0],s2473:[1,2],s0825:[0,1],s2455:[0],s2456:[0],s7400:[1]};
/** Translate interface copy only. Non-string React children pass through unchanged. */
export function tr<T>(value:T,values?:readonly unknown[]):T{
+6
View File
@@ -22,3 +22,9 @@ export function pickerGroups(library: ModelLibrary) {
}
return Array.from(groups, ([family, versions]) => ({ family, versions }));
}
/** Optimized models that are on this PC but not loaded yet: the chat picker must list them, or it says "none downloaded" while Storage shows them (#3814). */
export function pickerNativeModels<M extends { model: string; model_name?: string; ready?: boolean }>(installed: M[] | undefined | null, instances: { label: string }[] | undefined | null): M[] {
const loaded = new Set((instances ?? []).map(i => i.label));
return (installed ?? []).filter(m => m.ready !== false && !loaded.has(m.model_name ?? m.model));
}
+25
View File
@@ -13,7 +13,32 @@ export function gpuMetric(devices:GpuInfo[],readings:GpuReading[]|undefined,key:
return mode==='max'?Math.max(...values):values.reduce((n,v)=>n+v,0)/(mode==='mean'?values.length:1);
}
/** The card's name without its maker: "NVIDIA GeForce RTX 5070" -> "RTX 5070". */
export const shortGpuName=(name:string)=>String(name??'').replace(/^(NVIDIA|AMD|Intel)(\(R\)|\(TM\))?\s+/i,'').replace(/^(GeForce|Radeon|Arc)(\(TM\))?\s+/i,'').trim()||String(name??'');
/**
* The reading over the cards that DO report this value. A card that cannot report it (an integrated one without a temperature sensor) is left
* out instead of blanking the figure; `partial` says some were left out, and `devices` names the cards the figure is from.
*/
export function gpuReported(devices:GpuInfo[],readings:GpuReading[]|undefined,key:keyof GpuReading,mode:'sum'|'mean'|'max'='sum'):{value:number|null;devices:GpuInfo[];partial:boolean} {
const reporting=devices.filter(device=>hasReading(readings?.find(reading=>reading.id===gpuId(device))?.[key]));
return {value:reporting.length?gpuMetric(reporting,readings,key,mode):null,devices:reporting,partial:reporting.length>0&&reporting.length<devices.length};
}
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;
}
+2 -1
View File
@@ -3,7 +3,7 @@ import {tr} from './i18n';
import {nvfp4Version} from './native-names';
export interface CatalogVersion extends NativeSize{sourceFamily:string;edition:string;images:boolean}
export interface CatalogModel extends Omit<NativeFamily,'sizes'>{sizes:CatalogVersion[]}
export function nativeVersionLabel(model:string){const nv=nvfp4Version(model);if(nv)return tr(nv.name);return tr(({Q2_0:'Fastest',IQ2_XS:'Compact',IQ3_XXS:'Balanced',IQ3_S:'High quality',IQ1_M:'Coding','Gyro-S':'Smallest','Gyro-M':'Small memory'} as Record<string,string>)[model]??model);}
export function nativeVersionLabel(model:string){const nv=nvfp4Version(model);if(nv)return tr(nv.name);return tr(({Q2_0:'Fastest',IQ2_XS:'Compact',IQ3_XXS:'Balanced',IQ3_S:'High quality',IQ1_M:'Coding','Gyro-S':'Smallest','Gyro-M':'Small memory','IST-DAS-1.75bpw':'Smallest','IST-DAS-2.00bpw':'Balanced','IST-DAS-2.25bpw':'Best quality'} as Record<string,string>)[model]??model);}
export const catalogVersionLabel=(version:CatalogVersion)=>{const label=nativeVersionLabel(version.model);return [tr(version.edition),label===version.model&&version.edition?'':label].filter(Boolean).join(' · ');};
/** The one line that says how a version differs from its neighbours (the three NVFP4 versions); empty for the rest. */
export const versionNote=(model:string)=>{const nv=nvfp4Version(model);return nv?tr(nv.note):'';};
@@ -12,6 +12,7 @@ export function nativeCatalogModels(families:NativeFamily[]):CatalogModel[]{
if(!families.length)return[];
const groups:CatalogModel[]=[{family:'qwen3.8',title:'Qwen3.8-Flash-Next',about:'',experimental:false,images:false,sizes:[]},{family:'qwen3.6',title:'Qwen3.6-35B-A3B',about:'',experimental:false,images:false,sizes:[]}];
for(const family of families){
if(family.family==='deepseek41'){const sizes=family.sizes.filter(size=>!/^UD-Q8/i.test(size.model));if(sizes.length)groups.push({family:'deepseek41',title:family.title||'DeepSeek-V4.1-Flash',about:family.about??'',experimental:false,images:false,sizes:sizes.map(size=>({...size,sourceFamily:family.family,edition:'',images:false}))});continue;}
const target=/qwen3[._-]?6|35b[-_]?a3b/i.test(family.title+' '+family.family)?groups[1]:['qwen','swift','coder','unsloth','gyro'].includes(family.family)||/qwen3[._-]?8|flash.next/i.test(family.title)?groups[0]:undefined;
if(!target)continue;
const edition=target===groups[0]?({swift:'Swift 1.5',coder:'Coder',unsloth:'Unsloth',gyro:'Gyro'} as Record<string,string>)[family.family]??'':'';
+14 -3
View File
@@ -30,16 +30,27 @@ export function memoryNeeded(v: FitInput): number {
return round1((v.experts_gb ?? v.ram_needed_gb ?? 0) + DENSE_EXTRA_GB);
}
export function fitOn(v: FitInput, hw: FitHardware | null | undefined, mtp = true): Fit {
/** The conversation cache of Flash-Next: 13 attention layers, bytes per token per layer by cache format, 5.4% over the bare cache
* (strata_control/vram_fit.py KV_BYTES, KV_MEASURED_FACTOR; setup.py UD_FIT_KV_BYTES). */
const KV_BYTES: Record<string, number> = { fp16: 2112, int8: 1056, q4_0: 576, k8v4: 816 };
const KV_LAYERS = 13, KV_FACTOR = 1.054, BASE_CONTEXT = 32768;
export function cacheGb(context: number, kv = 'int8'): number {
return (121 + context * KV_LAYERS * (KV_BYTES[String(kv).toLowerCase()] ?? KV_BYTES.int8) * KV_FACTOR / 2 ** 20) / 1024; // 121 MiB fixed per session
}
export interface CacheChoice { context: number; kv?: string }
export function fitOn(v: FitInput, hw: FitHardware | null | undefined, mtp = true, cache?: CacheChoice | null): Fit {
const memoryGb = memoryNeeded(v);
// DENSE_GB already holds a 32K cache in the default format; the person's context and format replace it (#3832).
const denseGb = cache && cache.context > 0 ? Math.max(DENSE_GB - cacheGb(BASE_CONTEXT), 0) + cacheGb(cache.context, cache.kv) : DENSE_GB;
const ram = Number(hw?.ram_gb), vram = Number(hw?.vram_gb) || 0;
if (!Number.isFinite(ram) || ram <= 0) return { memoryGb, vramUseGb: 0, ramUseGb: 0, spillGb: 0, known: false };
const experts = Math.max(0, memoryNeeded(v) - DENSE_EXTRA_GB);
const onGpu = Math.max(0, Math.min(experts, vram - DENSE_GB - (mtp && vram > 0 ? MTP_VRAM_GB : 0)));
const onGpu = Math.max(0, Math.min(experts, vram - denseGb - (mtp && vram > 0 ? MTP_VRAM_GB : 0)));
const ramRoom = Math.max(0, ram - SYSTEM_ROOM_GB);
const spillGb = Math.max(0, experts - onGpu - ramRoom);
const inRam = Math.max(0, experts - onGpu - spillGb);
const vramUseGb = vram > 0 ? Math.min(vram, DENSE_GB + (mtp ? MTP_VRAM_GB : 0) + onGpu) : 0;
const vramUseGb = vram > 0 ? Math.min(vram, denseGb + (mtp ? MTP_VRAM_GB : 0) + onGpu) : 0;
return { memoryGb, vramUseGb: round1(vramUseGb), ramUseGb: round1(inRam + DENSE_EXTRA_GB * (vram > 0 ? 0 : 1)), spillGb: round1(spillGb), known: true };
}
+7 -3
View File
@@ -3,9 +3,9 @@ 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}
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 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}
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}
export interface NativeInstance {id:string;apiModelId:string;model:string;label:string;aliases?:string[];vision?:boolean;resourceProfile?:'work'|'performance'|null;context?:number;port:number;gpus:number[];status:'loading'|'ready'|'unloading'|'error';phase:string;loadedAt:string;error:string|null;metrics:Record<string,any>|null}
export interface NativeState {
@@ -15,6 +15,10 @@ export interface NativeState {
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}}
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). */
let settled=false;
export const nativeSettled=()=>settled;
export function subscribeNative(fn:()=>void){listeners.add(fn);return()=>{listeners.delete(fn);};}
function put(value:NativeState){if(state&&JSON.stringify(state)===JSON.stringify(value))return;state=value;listeners.forEach(fn=>fn());}
/** The host pushes its whole native state (jobs included) at every progress step: the page shows it at once, without waiting for the next poll. */
export function applyNativeEvent(value:unknown){if(value&&typeof value==='object'&&'available' in value&&'jobs' in value)put(value as NativeState);}
@@ -24,7 +28,7 @@ function read(force:boolean):Promise<void>{
// A read that never answers must not freeze the page: after the limit the next poll starts a fresh one.
let stop:ReturnType<typeof setTimeout>|undefined;
const limit=new Promise<never>((_,reject)=>{stop=setTimeout(()=>reject(new Error('The host did not answer in time.')),READ_LIMIT_MS);});
return Promise.race([request,limit]).then(put).catch(error=>{if(state){state={...state,error:error.message,instances:state.instances.map(i=>({...i,metrics:null,phase:'unavailable'}))};listeners.forEach(fn=>fn());}}).finally(()=>clearTimeout(stop));
return Promise.race([request,limit]).then(put).catch(error=>{if(state){state={...state,error:error.message,instances:state.instances.map(i=>({...i,metrics:null,phase:'unavailable'}))};listeners.forEach(fn=>fn());}}).finally(()=>{clearTimeout(stop);if(!settled){settled=true;listeners.forEach(fn=>fn());}});
}
/* Two kinds of read that never wait for each other: the light poll (the host answers at once with the jobs it holds) and the forced read after an action (the host
re-reads the installation, which can take many seconds while a download is writing). A slow forced read used to hold every later poll, so a running download looked stuck. */
+19
View File
@@ -0,0 +1,19 @@
/** The pictures on the clipboard, named so each one is told apart in the composer (a pasted screenshot arrives as "image.png"). */
export function pastedFiles(data: DataTransfer | null | undefined): File[] {
const out: File[] = [];
const found: File[] = Array.from(data?.files ?? []);
// A screenshot from the clipboard (Win+Shift+S) often comes as an item and not in `files` (#3748).
if (!found.some(f => f.type.startsWith('image/'))) {
for (const item of Array.from(data?.items ?? [])) {
if (item.kind !== 'file' || !item.type.startsWith('image/')) continue;
const file = item.getAsFile();
if (file) found.push(file);
}
}
for (const file of found) {
if (!file.type.startsWith('image/')) continue;
const extension = file.type.split('/')[1]?.replace('jpeg', 'jpg').replace(/[+].*/, '') || 'png';
out.push(new File([file], `Pasted picture ${new Date().toLocaleTimeString().replace(/[^0-9]+/g, '-').replace(/-$/, '')}${out.length ? `-${out.length + 1}` : ''}.${extension}`, { type: file.type }));
}
return out;
}
+8
View File
@@ -0,0 +1,8 @@
import type { Session, Workspace } from './types';
/** The workspace a chat was made in: the sidebar groups chats by this same rule (the saved path, or an earlier folder of the workspace). */
export function workspaceIdForSession(workspaces: Workspace[], session: Pick<Session, 'workspace'> | null | undefined): string | null {
const path = session?.workspace;
if (!path) return null;
return workspaces.find(w => w.path === path || !!w.aliases?.includes(path))?.id ?? null;
}
+8 -3
View File
@@ -1,3 +1,4 @@
import { workspaceIdForSession } from './session-workspace';
import { plainFailure, plainFailureText } from './plain-errors';
import { showcaseMessages, SHOWCASE_AGENTS } from './chat-showcase';
import { updateAgent } from './agents';
@@ -721,7 +722,7 @@ export async function bootstrap() {
skills,
workspacePath: workspaceState.workspaces[0]?.path ?? '',
workspaces: workspaceState.workspaces,
activeWorkspace: workspaceState.workspaces.some(w => w.id === state.activeWorkspace) ? state.activeWorkspace : workspaceState.workspaces[0]?.id ?? null,
activeWorkspace: workspaceState.workspaces.some(w => w.id === state.activeWorkspace) ? state.activeWorkspace : workspaceIdForSession(workspaceState.workspaces, state.sessions.find(s => s.id === state.activeSession)) ?? workspaceState.workspaces[0]?.id ?? null,
connection: 'live',
});
applyProviders(providers);
@@ -1104,7 +1105,11 @@ export function continueTaskInNewChat(task: Task) {
}
export function selectSession(id: string) {
set({ activeSession: id });
// A chat comes back in the workspace it was made in, so its next turn runs in the same folder (#3754).
const workspace = workspaceIdForSession(state.workspaces, state.sessions.find(s => s.id === id));
const change = workspace && workspace !== state.activeWorkspace;
set(change ? { activeSession: id, activeWorkspace: workspace } : { activeSession: id });
if (change) void loadWorkspaceFiles();
markRead(id);
}
@@ -1728,7 +1733,7 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
// The Mode menu's choice for this chat, read when the turn starts; the host applies it to the agent.
mode:state.sessions.find(s=>s.id===sessionId)?.preferences?.mode??'manual',
// So a skill narrowed to another workspace does not fire here.
workspaceId: state.activeWorkspace,
workspaceId: workspaceIdForSession(state.workspaces, state.sessions.find(s => s.id === sessionId)) ?? state.activeWorkspace,
},
(frame) => {
// A line from the app itself carries `key` (the English sentence): shown in the person's language.
+2
View File
@@ -8,6 +8,8 @@ export interface SoftwareUpdate {
releaseUrl?: string;
canInstall: boolean;
message: string;
/** The technical reason behind `message`, shown only behind the Details disclosure. */
details?: string;
}
export interface UpdateCheckResult { checkedAt: string; strata: SoftwareUpdate; engine: SoftwareUpdate }
+2 -1
View File
@@ -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. */
+71
View File
@@ -0,0 +1,71 @@
{
"fx01": "wird nicht mehr ausgeführt und entfernt.",
"fx02": "Heruntergeladen, aber nicht geladen. Öffne „Modelle“, um es zu laden.",
"fx03": "{0} Zeilen hinzugefügt, {1} entfernt",
"fx04": "{0} Dateien bearbeitet",
"fx05": "Dateien hier ablegen, um sie hinzuzufügen",
"fx06": "Bilder und Textdateien",
"fx07": "Ziel behalten",
"fx08": "Ziel entfernen",
"fx09": "Der Assistent arbeitet nicht mehr darauf hin. Dein Chat und deine Dateien bleiben unverändert.",
"fx10": "Dieses Modell kann Bilder lesen, aber die hier installierte Engine enthält den Bildleser noch nicht, deshalb ist „Bilder lesen“ aus. Aktualisiere die Engine unter Einstellungen → Info, um es einzuschalten.",
"fx11": "Zeigt die Werte deiner Abstimmung „{0}“.",
"fx12": "Deine Abstimmung „{0}“ setzt",
"fx13": "war {0}",
"fx14": "· {0} RAM",
"fx15": "Die Nachricht wurde nicht abgebrochen",
"fx16": "Zusätzliche Einstellungen für das Programm",
"fx17": "Mit KI entwerfen",
"fx18": "Denkzeit",
"fx19": "Schreibzeit",
"fx20": "Token/s",
"fx21": "% des Kontexts",
"fx22": "Diese Seite lädt Dateien aus dem Internet, was die Vorschau nicht tut. Sie kann deshalb unvollständig aussehen. „Code anzeigen“ zeigt genau, was geschrieben wurde.",
"fx23": "A behalten",
"fx24": "B behalten",
"fx25": "Dauer",
"fx26": "Schwankung",
"fx27": "Geändert",
"fx28": "Nichts",
"fx29": "Beide Versionen sind fertig. Vergleiche A und B und behalte eine.",
"fx30": "Der Assistent antwortet noch",
"fx31": "Sende deine Nachricht, wenn er fertig ist.",
"fx32": "Die Nachricht wurde nicht eingereiht",
"fx33": "Zur neuesten Nachricht scrollen",
"fx34": "Aufgabe entfernen",
"fx35": "Neue Unterhaltung beginnen; diese bleibt erhalten",
"fx36": "Der Assistent schreibt zuerst einen Plan; nichts läuft, bis du ihn freigibst",
"fx37": "Im Web suchen, die Seiten lesen und mit Quellen antworten",
"fx38": "Kopiert",
"fx39": "Antwort",
"fx40": "Nachricht",
"fx41": "PDF hinzugefügt",
"fx42": "Markiere den Text und kopiere ihn mit Strg+C.",
"fx43": "{0} ist in diesem Chat. Frag in deiner nächsten Nachricht danach.",
"fx44": "Die Engine ist bereit.",
"fx45": "Als Nächstes: ein Modell auswählen.",
"fx46": "Die Engine wird heruntergeladen",
"fx47": "Du kannst Strata schließen; der Download geht dort weiter, wo er aufgehört hat.",
"fx48": "Willkommen bei Strata",
"fx49": "Deine Grafikkarte wird erkannt…",
"fx50": "Strata hat deine Grafikkarte gefunden:",
"fx51": "{0} Grafikkarten gefunden. Strata nimmt für die Engine die erste; ändere das später in den Einstellungen.",
"fx52": "Strata hat keine unterstützte Grafikkarte erkannt.",
"fx53": "Um darauf Modelle auszuführen, braucht Strata einen Download:",
"fx54": "Die Engine für diese Grafikkarte",
"fx55": "Die Engine braucht etwa {0} GB freien Platz auf diesem Laufwerk, frei sind nur {1} GB. Schaffe Platz und mach dann weiter.",
"fx56": "Sie kommt von Stratas eigenem Update-Server und wird vor der Nutzung geprüft. Deine Modelle und Chats bleiben unberührt.",
"fx57": "Strata konnte github.com nicht erreichen. Prüfe deine Verbindung und versuche es erneut, oder lade die Engine-Datei auf einem anderen PC herunter und wähle sie hier aus.",
"fx58": "Die hier installierte Engine ist für einen anderen Grafikkarten-Hersteller.",
"fx59": "Strata kann aus seiner Update-Liste keine Engine für diesen PC wählen.",
"fx60": "Ohne Engine fortfahren",
"fx61": "Der Download wurde unterbrochen. „Weiter“ setzt ihn dort fort, wo er aufgehört hat.",
"fx62": "Das ist ein sehr großes Modell. Der Download umfasst {0} GB und braucht etwa so viel freien Speicherplatz. Es läuft auf einer Grafikkarte mit 12 GB und 46 bis 64 GB RAM, langsam: etwa 5 bis 7 Wörter pro Sekunde. Was nicht in den Speicher passt, wird von deiner SSD gelesen, deshalb zählt eine NVMe-SSD.",
"fx63": "Du kannst den Download stoppen und später fortsetzen; er macht dort weiter, wo er aufgehört hat. Strata warnt dich hier, hält dich aber nie auf.",
"fx64": "Schneller",
"fx65": "Volle Qualität",
"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)"
}
+71
View File
@@ -0,0 +1,71 @@
{
"fx01": "will stop running and be removed.",
"fx02": "Downloaded, not loaded. Open Models to load it.",
"fx03": "{0} lines added, {1} removed",
"fx04": "Edited {0} files",
"fx05": "Drop files to add them",
"fx06": "Pictures and text files",
"fx07": "Keep goal",
"fx08": "Remove goal",
"fx09": "The assistant stops working towards it. Your chat and files stay as they are.",
"fx10": "This model can read pictures, but the engine installed here does not include the picture reader yet, so Read pictures is off. Update the engine in Settings → About to turn it on.",
"fx11": "Showing the values of your tuning '{0}'.",
"fx12": "Your tuning '{0}' sets",
"fx13": "was {0}",
"fx14": "· {0} RAM",
"fx15": "The message was not cancelled",
"fx16": "Extra settings for the program",
"fx17": "Draft it with AI",
"fx18": "Thinking time",
"fx19": "Writing time",
"fx20": "tokens/s",
"fx21": "% of context",
"fx22": "This page asks for files from the internet, which the preview does not load, so it may look incomplete. View code shows exactly what was written.",
"fx23": "Keep A",
"fx24": "Keep B",
"fx25": "Took",
"fx26": "Variation",
"fx27": "Changed",
"fx28": "Nothing",
"fx29": "Both versions are ready. Compare A and B, then keep one.",
"fx30": "The assistant is still answering",
"fx31": "Send your message when it has finished.",
"fx32": "The message was not queued",
"fx33": "Scroll to the latest message",
"fx34": "Remove task",
"fx35": "Start a new conversation; keep this one",
"fx36": "Have the assistant write a plan first; nothing runs until you approve it",
"fx37": "Search the web, read the pages and answer with sources",
"fx38": "Copied",
"fx39": "Response",
"fx40": "Message",
"fx41": "PDF added",
"fx42": "Select the text and copy it with Ctrl+C.",
"fx43": "{0} is in this chat. Ask about it in your next message.",
"fx44": "The engine is ready.",
"fx45": "Next: pick a model.",
"fx46": "Downloading the engine",
"fx47": "You can close Strata; it continues where it stopped.",
"fx48": "Welcome to Strata",
"fx49": "Looking at your graphics card…",
"fx50": "Strata found your graphics card:",
"fx51": "{0} cards found. Strata uses the first one for the engine; change it later in Settings.",
"fx52": "Strata did not recognise a supported graphics card.",
"fx53": "To run models on it, Strata needs one download:",
"fx54": "The engine for this card",
"fx55": "The engine needs about {0} GB of free space on this drive and only {1} GB is free. Free some space, then continue.",
"fx56": "It comes from Strata’s own update server and is checked before it is used. Your models and chats are never touched by it.",
"fx57": "Strata could not reach github.com. Check your connection and try again, or download the engine file on another PC and choose it here.",
"fx58": "The engine installed here is for another make of graphics card.",
"fx59": "Strata can’t pick an engine for this PC from its update list.",
"fx60": "Continue without an engine",
"fx61": "The download stopped. Continue picks it up where it stopped.",
"fx62": "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.",
"fx63": "You can stop the download and continue later; it picks up where it stopped. Strata warns you here but never blocks you.",
"fx64": "Faster",
"fx65": "Full quality",
"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)"
}
+71
View File
@@ -0,0 +1,71 @@
{
"fx01": "dejará de ejecutarse y se eliminará.",
"fx02": "Descargado, pero no cargado. Abre Modelos para cargarlo.",
"fx03": "{0} líneas añadidas, {1} eliminadas",
"fx04": "{0} archivos editados",
"fx05": "Suelta los archivos para añadirlos",
"fx06": "Imágenes y archivos de texto",
"fx07": "Mantener el objetivo",
"fx08": "Quitar el objetivo",
"fx09": "El asistente deja de trabajar en ello. Tu chat y tus archivos se quedan como están.",
"fx10": "Este modelo puede leer imágenes, pero el motor instalado aquí aún no incluye el lector de imágenes, así que Leer imágenes está desactivado. Actualiza el motor en Ajustes → Acerca de para activarlo.",
"fx11": "Se muestran los valores de tu ajuste «{0}».",
"fx12": "Tu ajuste «{0}» establece",
"fx13": "antes {0}",
"fx14": "· {0} de RAM",
"fx15": "El mensaje no se canceló",
"fx16": "Ajustes adicionales del programa",
"fx17": "Redactar con IA",
"fx18": "Tiempo de pensar",
"fx19": "Tiempo de escribir",
"fx20": "tokens/s",
"fx21": "% del contexto",
"fx22": "Esta página pide archivos de internet, que la vista previa no carga, así que puede verse incompleta. Ver código muestra exactamente lo que se escribió.",
"fx23": "Mantener A",
"fx24": "Mantener B",
"fx25": "Tardó",
"fx26": "Variación",
"fx27": "Cambiado",
"fx28": "Nada",
"fx29": "Las dos versiones están listas. Compara A y B y quédate con una.",
"fx30": "El asistente sigue respondiendo",
"fx31": "Envía tu mensaje cuando termine.",
"fx32": "El mensaje no se puso en cola",
"fx33": "Ir al último mensaje",
"fx34": "Quitar tarea",
"fx35": "Empieza una conversación nueva; esta se conserva",
"fx36": "El asistente escribe primero un plan; no se ejecuta nada hasta que lo apruebes",
"fx37": "Busca en la web, lee las páginas y responde con fuentes",
"fx38": "Copiado",
"fx39": "Respuesta",
"fx40": "Mensaje",
"fx41": "PDF añadido",
"fx42": "Selecciona el texto y cópialo con Ctrl+C.",
"fx43": "{0} está en este chat. Pregunta por él en tu próximo mensaje.",
"fx44": "El motor está listo.",
"fx45": "Siguiente: elige un modelo.",
"fx46": "Descargando el motor",
"fx47": "Puedes cerrar Strata; la descarga sigue donde se quedó.",
"fx48": "Te damos la bienvenida a Strata",
"fx49": "Mirando tu tarjeta gráfica…",
"fx50": "Strata encontró tu tarjeta gráfica:",
"fx51": "Se encontraron {0} tarjetas. Strata usa la primera para el motor; puedes cambiarlo después en Ajustes.",
"fx52": "Strata no reconoció ninguna tarjeta gráfica compatible.",
"fx53": "Para ejecutar modelos en ella, Strata necesita una descarga:",
"fx54": "El motor para esta tarjeta",
"fx55": "El motor necesita unos {0} GB de espacio libre en esta unidad y solo hay {1} GB libres. Libera espacio y sigue.",
"fx56": "Viene del servidor de actualizaciones de Strata y se comprueba antes de usarlo. Tus modelos y chats no se tocan.",
"fx57": "Strata no pudo conectar con github.com. Revisa tu conexión y vuelve a intentarlo, o descarga el archivo del motor en otro PC y elígelo aquí.",
"fx58": "El motor instalado aquí es para otra marca de tarjeta gráfica.",
"fx59": "Strata no puede elegir un motor para este PC a partir de su lista de actualizaciones.",
"fx60": "Continuar sin motor",
"fx61": "La descarga se detuvo. «Continuar» la retoma donde se quedó.",
"fx62": "Es un modelo muy grande. La descarga es de {0} GB y necesita casi el mismo espacio libre en disco. Funciona con una tarjeta gráfica de 12 GB y de 46 a 64 GB de RAM, despacio: unas 5 a 7 palabras por segundo. Lo que no cabe en memoria se lee de tu SSD, así que importa que sea NVMe.",
"fx63": "Puedes detener la descarga y seguir más tarde; continúa donde se quedó. Strata te avisa aquí, pero nunca te lo impide.",
"fx64": "Más rápido",
"fx65": "Calidad completa",
"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)"
}
+71
View File
@@ -0,0 +1,71 @@
{
"fx01": "は停止され、削除されます。",
"fx02": "ダウンロード済みですが、読み込まれていません。「モデル」を開いて読み込んでください。",
"fx03": "{0} 行追加、{1} 行削除",
"fx04": "{0} 個のファイルを編集しました",
"fx05": "ファイルをドロップして追加",
"fx06": "画像とテキストファイル",
"fx07": "目標を残す",
"fx08": "目標を削除",
"fx09": "アシスタントはこの目標に向けた作業をやめます。チャットとファイルはそのまま残ります。",
"fx10": "このモデルは画像を読めますが、ここにインストールされているエンジンにはまだ画像リーダーが含まれていないため、「画像を読む」はオフです。設定 → 情報 でエンジンを更新するとオンにできます。",
"fx11": "チューニング「{0}」の値を表示しています。",
"fx12": "チューニング「{0}」が設定する項目",
"fx13": "変更前: {0}",
"fx14": "· {0} RAM",
"fx15": "メッセージはキャンセルされませんでした",
"fx16": "プログラムの追加設定",
"fx17": "AI で下書きを作成",
"fx18": "思考時間",
"fx19": "執筆時間",
"fx20": "トークン/秒",
"fx21": "% のコンテキスト",
"fx22": "このページはインターネット上のファイルを必要としますが、プレビューでは読み込まれないため、表示が不完全になることがあります。「コードを表示」で書かれた内容をそのまま確認できます。",
"fx23": "A を残す",
"fx24": "B を残す",
"fx25": "所要時間",
"fx26": "ばらつき",
"fx27": "変更点",
"fx28": "なし",
"fx29": "両方のバージョンの準備ができました。A と B を比べて、どちらかを残してください。",
"fx30": "アシスタントはまだ回答中です",
"fx31": "回答が終わってからメッセージを送ってください。",
"fx32": "メッセージは待機列に入りませんでした",
"fx33": "最新のメッセージへスクロール",
"fx34": "タスクを削除",
"fx35": "新しい会話を始めます。今の会話は残ります",
"fx36": "アシスタントにまず計画を書かせます。承認するまで何も実行されません",
"fx37": "ウェブを検索し、ページを読み、出典付きで回答します",
"fx38": "コピーしました",
"fx39": "返信",
"fx40": "メッセージ",
"fx41": "PDF を追加しました",
"fx42": "テキストを選択して Ctrl+C でコピーしてください。",
"fx43": "{0} をこのチャットに追加しました。次のメッセージで質問してください。",
"fx44": "エンジンの準備ができました。",
"fx45": "次へ:モデルを選んでください。",
"fx46": "エンジンをダウンロードしています",
"fx47": "Strata を閉じても大丈夫です。止まった場所から続きをダウンロードします。",
"fx48": "Strata へようこそ",
"fx49": "グラフィックスカードを確認しています…",
"fx50": "Strata がグラフィックスカードを見つけました:",
"fx51": "グラフィックスカードが {0} 枚見つかりました。エンジンには最初の1枚を使います。あとで設定から変更できます。",
"fx52": "Strata は対応しているグラフィックスカードを認識できませんでした。",
"fx53": "このカードでモデルを動かすには、ダウンロードが1つ必要です:",
"fx54": "このカード用のエンジン",
"fx55": "エンジンにはこのドライブの空き容量が約 {0} GB 必要ですが、空きは {1} GB しかありません。空き容量を作ってから続けてください。",
"fx56": "Strata 自身の更新サーバーから取得し、使う前に検証します。モデルやチャットには一切触れません。",
"fx57": "Strata は github.com に接続できませんでした。接続を確認してもう一度試すか、別の PC でエンジンのファイルをダウンロードして、ここで選んでください。",
"fx58": "ここにインストールされているエンジンは、別のメーカーのグラフィックスカード用です。",
"fx59": "Strata は更新リストからこの PC 用のエンジンを選べません。",
"fx60": "エンジンなしで続ける",
"fx61": "ダウンロードが止まりました。「続ける」を押すと、止まった場所から再開します。",
"fx62": "とても大きなモデルです。ダウンロードは {0} GB で、ほぼ同じ大きさの空き容量が必要です。12 GB のグラフィックスカードと 46〜64 GB の RAM で動きますが、速度はゆっくりで、1秒に約 5〜7 語です。メモリに入らない分は SSD から読むため、NVMe ドライブが重要です。",
"fx63": "ダウンロードはいつでも止めて、あとで続けられます。止まった場所から再開します。Strata はここで注意はしますが、実行を止めることはありません。",
"fx64": "高速",
"fx65": "フル品質",
"fx66": "既定(この PC では「高速」)",
"fx67": "既定(この PC では「フル品質」)",
"fx68": "高速(この PC の既定)",
"fx69": "フル品質(この PC の既定)"
}
+71
View File
@@ -0,0 +1,71 @@
{
"fx01": "실행이 중지되고 제거됩니다.",
"fx02": "다운로드했지만 아직 불러오지 않았습니다. 모델 화면을 열어 불러오세요.",
"fx03": "{0}줄 추가, {1}줄 삭제",
"fx04": "파일 {0}개 편집함",
"fx05": "파일을 놓아서 추가하세요",
"fx06": "그림과 텍스트 파일",
"fx07": "목표 유지",
"fx08": "목표 제거",
"fx09": "어시스턴트가 이 목표를 향한 작업을 멈춥니다. 대화와 파일은 그대로 남습니다.",
"fx10": "이 모델은 그림을 읽을 수 있지만, 여기에 설치된 엔진에는 아직 그림 읽기 기능이 없어서 '그림 읽기'가 꺼져 있습니다. 설정 → 정보에서 엔진을 업데이트하면 켤 수 있습니다.",
"fx11": "튜닝 '{0}'의 값을 보여 줍니다.",
"fx12": "튜닝 '{0}'이(가) 설정하는 항목",
"fx13": "이전 값 {0}",
"fx14": "· RAM {0}",
"fx15": "메시지가 취소되지 않았습니다",
"fx16": "프로그램 추가 설정",
"fx17": "AI로 초안 작성",
"fx18": "생각한 시간",
"fx19": "작성한 시간",
"fx20": "토큰/초",
"fx21": "컨텍스트 %",
"fx22": "이 페이지는 인터넷의 파일을 불러와야 하는데 미리보기는 이를 불러오지 않으므로 불완전하게 보일 수 있습니다. '코드 보기'에서 작성된 내용을 그대로 볼 수 있습니다.",
"fx23": "A 유지",
"fx24": "B 유지",
"fx25": "걸린 시간",
"fx26": "편차",
"fx27": "변경됨",
"fx28": "없음",
"fx29": "두 버전이 모두 준비되었습니다. A와 B를 비교한 뒤 하나를 유지하세요.",
"fx30": "어시스턴트가 아직 답하는 중입니다",
"fx31": "답이 끝나면 메시지를 보내세요.",
"fx32": "메시지가 대기열에 들어가지 않았습니다",
"fx33": "최신 메시지로 이동",
"fx34": "작업 제거",
"fx35": "새 대화를 시작합니다. 이 대화는 그대로 둡니다",
"fx36": "어시스턴트가 먼저 계획을 작성합니다. 승인하기 전에는 아무것도 실행되지 않습니다",
"fx37": "웹을 검색하고 페이지를 읽어 출처와 함께 답합니다",
"fx38": "복사했습니다",
"fx39": "답변",
"fx40": "메시지",
"fx41": "PDF를 추가했습니다",
"fx42": "텍스트를 선택한 뒤 Ctrl+C로 복사하세요.",
"fx43": "{0}이(가) 이 채팅에 있습니다. 다음 메시지에서 물어보세요.",
"fx44": "엔진이 준비되었습니다.",
"fx45": "다음: 모델을 고르세요.",
"fx46": "엔진을 다운로드하는 중",
"fx47": "Strata를 닫아도 됩니다. 멈춘 곳에서 이어서 받습니다.",
"fx48": "Strata에 오신 것을 환영합니다",
"fx49": "그래픽 카드를 확인하는 중…",
"fx50": "Strata가 그래픽 카드를 찾았습니다:",
"fx51": "그래픽 카드 {0}개를 찾았습니다. 엔진에는 첫 번째 카드를 사용하며, 나중에 설정에서 바꿀 수 있습니다.",
"fx52": "Strata가 지원되는 그래픽 카드를 인식하지 못했습니다.",
"fx53": "이 카드에서 모델을 실행하려면 다운로드가 하나 필요합니다:",
"fx54": "이 카드용 엔진",
"fx55": "엔진에는 이 드라이브의 여유 공간이 약 {0}GB 필요한데, 남은 공간은 {1}GB뿐입니다. 공간을 확보한 뒤 계속하세요.",
"fx56": "Strata의 자체 업데이트 서버에서 받으며, 사용하기 전에 검증합니다. 모델과 채팅은 건드리지 않습니다.",
"fx57": "Strata가 github.com에 연결하지 못했습니다. 연결을 확인하고 다시 시도하거나, 다른 PC에서 엔진 파일을 받아 여기서 선택하세요.",
"fx58": "여기에 설치된 엔진은 다른 제조사의 그래픽 카드용입니다.",
"fx59": "Strata가 업데이트 목록에서 이 PC에 맞는 엔진을 고르지 못했습니다.",
"fx60": "엔진 없이 계속",
"fx61": "다운로드가 멈췄습니다. 계속을 누르면 멈춘 곳에서 이어서 받습니다.",
"fx62": "매우 큰 모델입니다. 다운로드는 {0}GB이고, 거의 같은 크기의 여유 디스크 공간이 필요합니다. 12GB 그래픽 카드 한 장과 46~64GB RAM에서 느리게 실행되며, 초당 약 5~7단어입니다. 메모리에 들어가지 않는 부분은 SSD에서 읽으므로 NVMe 드라이브가 중요합니다.",
"fx63": "다운로드는 멈췄다가 나중에 이어서 받을 수 있으며, 멈춘 곳에서 다시 시작합니다. Strata는 여기서 알려 드리지만 막지는 않습니다.",
"fx64": "더 빠르게",
"fx65": "전체 품질",
"fx66": "기본값(이 PC에서는 더 빠르게)",
"fx67": "기본값(이 PC에서는 전체 품질)",
"fx68": "더 빠르게(이 PC의 기본값)",
"fx69": "전체 품질(이 PC의 기본값)"
}
+71
View File
@@ -0,0 +1,71 @@
{
"fx01": "vai parar de funcionar e será removido.",
"fx02": "Baixado, mas não carregado. Abra Modelos para carregá-lo.",
"fx03": "{0} linhas adicionadas, {1} removidas",
"fx04": "{0} arquivos editados",
"fx05": "Solte os arquivos para adicioná-los",
"fx06": "Imagens e arquivos de texto",
"fx07": "Manter a meta",
"fx08": "Remover a meta",
"fx09": "O assistente deixa de trabalhar nela. Seu chat e seus arquivos continuam como estão.",
"fx10": "Este modelo consegue ler imagens, mas o motor instalado aqui ainda não inclui o leitor de imagens, então Ler imagens está desligado. Atualize o motor em Configurações → Sobre para ligar.",
"fx11": "Mostrando os valores do seu ajuste \"{0}\".",
"fx12": "Seu ajuste \"{0}\" define",
"fx13": "era {0}",
"fx14": "· {0} de RAM",
"fx15": "A mensagem não foi cancelada",
"fx16": "Configurações extras do programa",
"fx17": "Rascunhar com IA",
"fx18": "Tempo de raciocínio",
"fx19": "Tempo de escrita",
"fx20": "tokens/s",
"fx21": "% do contexto",
"fx22": "Esta página pede arquivos da internet, que a visualização não carrega, então pode parecer incompleta. Ver código mostra exatamente o que foi escrito.",
"fx23": "Manter A",
"fx24": "Manter B",
"fx25": "Levou",
"fx26": "Variação",
"fx27": "Alterado",
"fx28": "Nada",
"fx29": "As duas versões estão prontas. Compare A e B e mantenha uma.",
"fx30": "O assistente ainda está respondendo",
"fx31": "Envie sua mensagem quando ele terminar.",
"fx32": "A mensagem não entrou na fila",
"fx33": "Ir para a última mensagem",
"fx34": "Remover tarefa",
"fx35": "Começar uma nova conversa; esta continua salva",
"fx36": "O assistente escreve um plano primeiro; nada roda até você aprovar",
"fx37": "Pesquisar na web, ler as páginas e responder com fontes",
"fx38": "Copiado",
"fx39": "Resposta",
"fx40": "Mensagem",
"fx41": "PDF adicionado",
"fx42": "Selecione o texto e copie com Ctrl+C.",
"fx43": "{0} está neste chat. Pergunte sobre ele na próxima mensagem.",
"fx44": "O motor está pronto.",
"fx45": "Próximo passo: escolha um modelo.",
"fx46": "Baixando o motor",
"fx47": "Você pode fechar o Strata; o download continua de onde parou.",
"fx48": "Bem-vindo ao Strata",
"fx49": "Verificando sua placa de vídeo…",
"fx50": "O Strata encontrou sua placa de vídeo:",
"fx51": "{0} placas de vídeo encontradas. O Strata usa a primeira para o motor; você pode mudar isso depois nas Configurações.",
"fx52": "O Strata não reconheceu nenhuma placa de vídeo compatível.",
"fx53": "Para executar modelos nela, o Strata precisa de um download:",
"fx54": "O motor para esta placa",
"fx55": "O motor precisa de cerca de {0} GB de espaço livre neste disco e só há {1} GB livres. Libere espaço e continue.",
"fx56": "Ele vem do servidor de atualizações do próprio Strata e é verificado antes do uso. Seus modelos e chats não são tocados.",
"fx57": "O Strata não conseguiu acessar github.com. Verifique a conexão e tente de novo, ou baixe o arquivo do motor em outro PC e escolha-o aqui.",
"fx58": "O motor instalado aqui é para outra marca de placa de vídeo.",
"fx59": "O Strata não consegue escolher um motor para este PC a partir da lista de atualizações.",
"fx60": "Continuar sem motor",
"fx61": "O download parou. “Continuar” retoma de onde parou.",
"fx62": "Este é um modelo muito grande. O download tem {0} GB e precisa de quase o mesmo espaço livre em disco. Ele roda em uma placa de vídeo de 12 GB com 46 a 64 GB de RAM, devagar: cerca de 5 a 7 palavras por segundo. O que não cabe na memória é lido do seu SSD, então um NVMe faz diferença.",
"fx63": "Você pode parar o download e continuar depois; ele retoma de onde parou. O Strata avisa você aqui, mas nunca impede.",
"fx64": "Mais rápido",
"fx65": "Qualidade total",
"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)"
}
+71
View File
@@ -0,0 +1,71 @@
{
"fx01": "перестанет работать и будет удалена.",
"fx02": "Скачана, но не загружена. Откройте «Модели», чтобы загрузить её.",
"fx03": "Добавлено строк: {0}, удалено: {1}",
"fx04": "Изменено файлов: {0}",
"fx05": "Перетащите файлы сюда, чтобы добавить",
"fx06": "Картинки и текстовые файлы",
"fx07": "Оставить цель",
"fx08": "Удалить цель",
"fx09": "Ассистент перестанет работать над ней. Ваш чат и файлы останутся как есть.",
"fx10": "Эта модель умеет читать картинки, но установленный здесь движок пока не содержит модуль чтения картинок, поэтому «Читать картинки» выключено. Обновите движок в разделе «Настройки → О программе», чтобы включить.",
"fx11": "Показаны значения вашей настройки «{0}».",
"fx12": "Ваша настройка «{0}» задаёт",
"fx13": "было {0}",
"fx14": "· {0} ОЗУ",
"fx15": "Сообщение не было отменено",
"fx16": "Дополнительные настройки программы",
"fx17": "Набросать с помощью ИИ",
"fx18": "Время размышления",
"fx19": "Время написания",
"fx20": "токенов/с",
"fx21": "% контекста",
"fx22": "Эта страница запрашивает файлы из интернета, а предпросмотр их не загружает, поэтому она может выглядеть неполной. «Показать код» показывает, что именно было написано.",
"fx23": "Оставить A",
"fx24": "Оставить B",
"fx25": "Заняло",
"fx26": "Разброс",
"fx27": "Изменено",
"fx28": "Ничего",
"fx29": "Обе версии готовы. Сравните A и B и оставьте одну.",
"fx30": "Ассистент ещё отвечает",
"fx31": "Отправьте сообщение, когда он закончит.",
"fx32": "Сообщение не поставлено в очередь",
"fx33": "К последнему сообщению",
"fx34": "Удалить задачу",
"fx35": "Начать новый разговор; этот сохраняется",
"fx36": "Ассистент сначала напишет план; ничего не запустится, пока вы его не одобрите",
"fx37": "Искать в интернете, читать страницы и отвечать со ссылками на источники",
"fx38": "Скопировано",
"fx39": "Ответ",
"fx40": "Сообщение",
"fx41": "PDF добавлен",
"fx42": "Выделите текст и скопируйте его клавишами Ctrl+C.",
"fx43": "{0} добавлен в этот чат. Спросите о нём в следующем сообщении.",
"fx44": "Движок готов.",
"fx45": "Дальше: выберите модель.",
"fx46": "Загрузка движка",
"fx47": "Можно закрыть Strata; загрузка продолжится с того места, где остановилась.",
"fx48": "Добро пожаловать в Strata",
"fx49": "Смотрим вашу видеокарту…",
"fx50": "Strata нашла вашу видеокарту:",
"fx51": "Найдено видеокарт: {0}. Для движка Strata использует первую; позже это можно изменить в настройках.",
"fx52": "Strata не распознала поддерживаемую видеокарту.",
"fx53": "Чтобы запускать на ней модели, Strata нужно одно скачивание:",
"fx54": "Движок для этой видеокарты",
"fx55": "Движку нужно около {0} ГБ свободного места на этом диске, а свободно только {1} ГБ. Освободите место и продолжайте.",
"fx56": "Он приходит с собственного сервера обновлений Strata и проверяется перед использованием. Ваши модели и чаты он не затрагивает.",
"fx57": "Strata не смогла подключиться к github.com. Проверьте соединение и повторите попытку, либо скачайте файл движка на другом компьютере и выберите его здесь.",
"fx58": "Установленный здесь движок предназначен для видеокарт другого производителя.",
"fx59": "Strata не может выбрать движок для этого компьютера по своему списку обновлений.",
"fx60": "Продолжить без движка",
"fx61": "Загрузка остановилась. «Продолжить» возобновит её с того места, где она остановилась.",
"fx62": "Это очень большая модель. Загрузка — {0} ГБ, и нужно почти столько же свободного места на диске. Она работает на одной видеокарте с 12 ГБ и 46–64 ГБ оперативной памяти, медленно: около 5–7 слов в секунду. То, что не помещается в память, читается с вашего SSD, поэтому важен накопитель NVMe.",
"fx63": "Загрузку можно остановить и продолжить позже; она продолжится с того же места. Strata предупреждает вас здесь, но никогда не мешает.",
"fx64": "Быстрее",
"fx65": "Полное качество",
"fx66": "По умолчанию (на этом компьютере — «Быстрее»)",
"fx67": "По умолчанию (на этом компьютере — «Полное качество»)",
"fx68": "Быстрее (по умолчанию на этом компьютере)",
"fx69": "Полное качество (по умолчанию на этом компьютере)"
}
+71
View File
@@ -0,0 +1,71 @@
{
"fx01": "将停止运行并被移除。",
"fx02": "已下载,尚未加载。打开“模型”页面来加载它。",
"fx03": "新增 {0} 行,删除 {1} 行",
"fx04": "已编辑 {0} 个文件",
"fx05": "拖放文件即可添加",
"fx06": "图片和文本文件",
"fx07": "保留目标",
"fx08": "移除目标",
"fx09": "助手将不再朝这个目标工作。你的对话和文件保持不变。",
"fx10": "此模型可以读取图片,但这里安装的引擎还不包含图片读取器,所以“读取图片”已关闭。请在“设置 → 关于”中更新引擎后再开启。",
"fx11": "显示你的调优“{0}”的取值。",
"fx12": "你的调优“{0}”设置了",
"fx13": "原为 {0}",
"fx14": "· {0} 内存",
"fx15": "消息没有被取消",
"fx16": "程序的额外设置",
"fx17": "用 AI 起草",
"fx18": "思考时间",
"fx19": "写作时间",
"fx20": "词元/秒",
"fx21": "% 的上下文",
"fx22": "此页面需要从互联网加载文件,预览不会加载这些文件,所以看起来可能不完整。“查看代码”会显示实际写入的内容。",
"fx23": "保留 A",
"fx24": "保留 B",
"fx25": "用时",
"fx26": "波动",
"fx27": "已更改",
"fx28": "无",
"fx29": "两个版本都已就绪。比较 A 和 B,然后保留其中一个。",
"fx30": "助手仍在回答",
"fx31": "等它回答完再发送你的消息。",
"fx32": "消息没有进入队列",
"fx33": "滚动到最新消息",
"fx34": "移除任务",
"fx35": "开始新对话;保留当前这个",
"fx36": "让助手先写一份计划;在你批准之前不会运行任何内容",
"fx37": "搜索网络、阅读网页,并附上来源作答",
"fx38": "已复制",
"fx39": "回复",
"fx40": "消息",
"fx41": "已添加 PDF",
"fx42": "请选中文字,然后按 Ctrl+C 复制。",
"fx43": "{0} 已在此对话中。请在下一条消息里询问它。",
"fx44": "引擎已就绪。",
"fx45": "下一步:选择一个模型。",
"fx46": "正在下载引擎",
"fx47": "你可以关闭 Strata;下次会从中断的位置继续下载。",
"fx48": "欢迎使用 Strata",
"fx49": "正在查看你的显卡…",
"fx50": "Strata 找到了你的显卡:",
"fx51": "找到 {0} 张显卡。Strata 会用第一张来运行引擎;之后可在设置中更改。",
"fx52": "Strata 没有识别到受支持的显卡。",
"fx53": "要在它上面运行模型,Strata 需要先下载一个文件:",
"fx54": "这张显卡对应的引擎",
"fx55": "引擎大约需要此磁盘上 {0} GB 的可用空间,目前只有 {1} GB 可用。请先腾出空间,然后继续。",
"fx56": "它来自 Strata 自己的更新服务器,使用前会先校验。你的模型和对话不会受到影响。",
"fx57": "Strata 无法连接 github.com。请检查网络后重试,或在另一台电脑上下载引擎文件,再在这里选择它。",
"fx58": "这里已安装的引擎适用于另一品牌的显卡。",
"fx59": "Strata 无法根据更新列表为这台电脑选择引擎。",
"fx60": "不安装引擎,继续",
"fx61": "下载已中断。点“继续”会从中断的位置接着下载。",
"fx62": "这是一个非常大的模型。下载大小为 {0} GB,需要差不多同样大小的可用磁盘空间。它可在一张 12 GB 显卡加 46 到 64 GB 内存上运行,速度较慢:每秒约 5 到 7 个词。内存放不下的部分会从你的 SSD 读取,所以 NVMe 硬盘很重要。",
"fx63": "你可以随时停止下载,之后再继续;它会从中断的位置接着下载。Strata 会在这里提醒你,但从不阻止你。",
"fx64": "更快",
"fx65": "完整质量",
"fx66": "默认(这台电脑上为“更快”)",
"fx67": "默认(这台电脑上为“完整质量”)",
"fx68": "更快(这台电脑上的默认)",
"fx69": "完整质量(这台电脑上的默认)"
}
+5 -4
View File
@@ -39,6 +39,7 @@ import { Btn, Icon, IconBtn, ErrorNotice, type IconName } from '../components/ui
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 { getSlashCommands } from '../lib/commands';
import { useLocation } from '../lib/router';
import { composerSends } from '../lib/shortcut-bindings';
@@ -616,11 +617,11 @@ function Message({ message: m, isLast, onEditPrompt, onPreview, onOpenLink, onOp
const [sentTime, setSentTime] = useState(() => relTime(m.at));
if (m.role === 'user') {
return (<div className="st-msg st-msg--user" onMouseEnter={() => setSentTime(relTime(m.at))} onFocus={() => setSentTime(relTime(m.at))}>
<div className="st-bubble">{tr(m.photos?.length ? <ChatPhotos photos={m.photos}/> : null)}{m.markdown ? <Markdown text={m.content} onOpenLink={onOpenLink} imageSession={activeSession}/> : m.content}</div>
<div className="st-bubble">{tr(m.photos?.length ? <ChatPhotos photos={m.photos}/> : null)}{m.markdown ? <Markdown text={m.content} onOpenLink={onOpenLink} imageSession={activeSession}/> : <span className="st-bubble__text">{m.content}</span>}</div>
<div className="st-msg__meta">
<time dateTime={m.at} title={tr(new Date(m.at).toLocaleString(localeTag()))}>{tr(sentTime)}</time>
<span className="st-msg__actions">
<IconBtn name="copy" title={tr("Copy message")} onClick={() => void navigator.clipboard.writeText(m.content)}/>
<IconBtn name="copy" title={tr("Copy message")} onClick={() => void copyText(m.content, 'Message', { toast: pushToast })}/>
<IconBtn name="refresh" title={tr("Rewind to here")} disabled={streaming} onClick={() => {
const prompt = rewindTo(m.id);
if (prompt !== null)
@@ -659,11 +660,11 @@ function Message({ message: m, isLast, onEditPrompt, onPreview, onOpenLink, onOp
{tr(!m.streaming && !m.timeSkipId && (m.content || m.error) ? (<div className="st-msg__meta">
<span className="st-msg__actions">
{tr(isLast ? <>
<IconBtn name="copy" title={tr("Copy response")} onClick={() => void navigator.clipboard.writeText(m.content)}/>
<IconBtn name="copy" title={tr("Copy response")} onClick={() => void copyText(m.content, 'Response', { toast: pushToast })}/>
<IconBtn name="refresh" title={tr("Ask again")} onClick={() => void retryLast(m.id)}/>
</> : null)}
<MessageMenu label={tr("More response actions")}>
{tr(!isLast ? <><button role="menuitem" className="menu__item" onClick={() => void navigator.clipboard.writeText(m.content)}>{tr("Copy response")}</button><button role="menuitem" className="menu__item" onClick={() => void retryLast(m.id)}>{tr("Ask again")}</button></> : null)}
{tr(!isLast ? <><button role="menuitem" className="menu__item" onClick={() => void copyText(m.content, 'Response', { toast: pushToast })}>{tr("Copy response")}</button><button role="menuitem" className="menu__item" onClick={() => void retryLast(m.id)}>{tr("Ask again")}</button></> : null)}
{tr(isLast ? <button role="menuitem" className="menu__item" onClick={() => void continueLast()}>{tr("Continue")}</button> : null)}
<button role="menuitem" className="menu__item" onClick={() => branchFrom(m.id)}>{tr("Branch from here")}</button>
{tr(m.tokens || m.tps || m.durationMs ? <p className="xs muted response-stats">{tr(m.tokens ? `${tokens(m.tokens)} tokens` : '')}{tr(m.tps ? ` · ${tps(m.tps)} tok/s` : '')}{tr(m.durationMs ? ` · ${duration(m.durationMs)}` : '')}{tr(m.demo ? ' · Example' : '')}</p> : null)}
+6 -2
View File
@@ -1,5 +1,5 @@
import { tr } from '../lib/i18n';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
import { ActionError, Btn, Dialog, ErrorNotice, Icon, Select } from '../components/ui';
import { Screen } from '../components/layout';
import { DemoTest } from '../components/EngineExperience';
@@ -24,7 +24,7 @@ import { familyVersion, modelFamilies } from '../lib/model-versions';
import { sizeGb } from '../lib/format';
import { applyImageModel, getState, useLifecycle, loadRuntimeModel, loadVariant, pushToast, restartEngine, saveEngine, selectImageModel, setConfigDraft, useStore, validateEngine } from '../lib/store';
import type { ImageModelChoice, RuntimeModels, RuntimeModel } from '../lib/types';
import { useNative } from '../lib/native';
import { useNative, nativeSettled, subscribeNative } from '../lib/native';
import { NativeModels } from '../components/NativeModels';
import { NativeLocalModel } from '../components/NativeLocalModel';
import { parseLocation } from '../lib/router';
@@ -35,8 +35,12 @@ type Issue = {
type RunStage = 'idle' | 'checking' | 'loading' | 'ready';
export function ModelRunScreen() {
const native = useNative();
const settled = useSyncExternalStore(subscribeNative, nativeSettled);
const location = parseLocation(window.location.hash), entry = location.param;
const [other, setOther] = useState(entry === 'local' || entry === 'local-import'), [importFirst, setImportFirst] = useState(entry === 'local-import');
// Before the engine's answer arrives the page would show the bundled Starter and then jump to the installed model (#3740): show a neutral placeholder instead.
if (!native && !settled)
return <div className="main__body"><div className="wrap stack" role="status" aria-label={tr("Loading…")}><div className="skel" style={{ height: 20, width: '60%' }}/><div className="skel" style={{ height: 20, width: '80%' }}/><div className="skel" style={{ height: 20, width: '70%' }}/></div></div>;
if (native && other)
return <NativeLocalModel onBack={() => { setOther(false); setImportFirst(false); }}/>;
if (native && !other)
+16 -13
View File
@@ -12,7 +12,7 @@ import { Brand } from '../components/Brand';
import { Bar, Btn, Icon, IconBtn, Select, type IconName } from '../components/ui';
import { Screen } from '../components/layout';
import { contextLabel, duration, tokens, tps } from '../lib/format';
import { gpuCapacity, gpuId, gpuMetric, hasReading } from '../lib/monitor-hardware';
import { defaultGpuSet, gpuCapacity, gpuId, gpuMetric, gpuReported, hasReading, shortGpuName } from '../lib/monitor-hardware';
import { useStore } from '../lib/store';
import { useKept } from '../lib/kept-state';
import type { Hardware, MonitorState, ServeRequest } from '../lib/types';
@@ -107,18 +107,21 @@ function LegacyMonitorScreen({ native }: {
if (!data)
return <Screen title={tr("Monitor")}><p className="muted">{tr("Waiting for engine telemetry\u2026")}</p></Screen>;
// A display adapter that has no sensors and no memory of its own (an integrated one) cannot be measured; counting it would blank every total.
const devices = (hardware?.gpus ?? []).filter(g => g.backend !== 'vulkan' || g.vramGb > 0), processors = hardware?.processors?.length ? hardware.processors : hardware ? [{ ...hardware.cpu, id: 'cpu', socket: '' }] : [];
const devices = (hardware?.gpus ?? []).filter(g => g.backend !== 'vulkan' || g.vramGb > 0), defaultDevices = defaultGpuSet(devices), processors = hardware?.processors?.length ? hardware.processors : hardware ? [{ ...hardware.cpu, id: 'cpu', socket: '' }] : [];
const assignedDevices = selectedInstance ? devices.filter(g => selectedInstance.gpuIds.includes(gpuId(g))) : [];
const gpuChoice = gpuSelection === 'assigned' && selectedInstance ? 'assigned' : devices.some(g => gpuId(g) === gpuSelection) ? gpuSelection! : gpuSelection === 'all' || !assignedDevices.length ? 'all' : 'assigned';
const cpuChoice = processors.some(p => p.id === cpuSelection) ? cpuSelection : 'all';
const selectedGpus = gpuChoice === 'assigned' ? devices.filter(g => selectedInstance?.gpuIds.includes(gpuId(g))) : gpuChoice === 'all' ? devices : devices.filter(g => gpuId(g) === gpuChoice);
const selectedGpus = gpuChoice === 'assigned' ? devices.filter(g => selectedInstance?.gpuIds.includes(gpuId(g))) : gpuChoice === 'all' ? defaultDevices : devices.filter(g => gpuId(g) === gpuChoice);
const selectedCpus = cpuChoice === 'all' ? processors : processors.filter(p => p.id === cpuChoice);
const multipleGpus = selectedGpus.length > 1, shared = selectedGpus.length > 0 && selectedGpus.every(g => g.unified);
const readings = data.hardware.gpus;
const gpuValue = (key: Parameters<typeof gpuMetric>[2], mode: Parameters<typeof gpuMetric>[3] = 'sum') => gpuMetric(selectedGpus, readings, key, mode);
const gpuSource = (key: Parameters<typeof gpuMetric>[2], mode: Parameters<typeof gpuMetric>[3] = 'sum') => hasReading(gpuValue(key, mode)) ? sourceLabel(readings?.find(r => r.id === gpuId(selectedGpus[0]))?.source) : 'Unavailable';
// A card that cannot report a value (an integrated one with no temperature sensor) is left out of that value, and the card the figure is from is named.
const report = (key: Parameters<typeof gpuMetric>[2], mode: Parameters<typeof gpuMetric>[3] = 'sum') => gpuReported(selectedGpus, readings, key, mode);
const gpuValue = (key: Parameters<typeof gpuMetric>[2], mode: Parameters<typeof gpuMetric>[3] = 'sum') => report(key, mode).value;
const whose = (key: Parameters<typeof gpuMetric>[2], mode: Parameters<typeof gpuMetric>[3] = 'sum') => { const r = report(key, mode); return r.partial ? r.devices.map(d => shortGpuName(d.name)).join(' + ') : null; };
const gpuSource = (key: Parameters<typeof gpuMetric>[2], mode: Parameters<typeof gpuMetric>[3] = 'sum') => hasReading(gpuValue(key, mode)) ? sourceLabel(readings?.find(r => r.id === gpuId(report(key, mode).devices[0] ?? selectedGpus[0]))?.source) : 'Unavailable';
const gpuLoad = gpuValue('utilizationPct', 'mean'), vram = gpuValue('memoryUsedGb'), temperature = gpuValue('temperatureC', 'max'), power = gpuValue('powerW'), powerLimit = gpuValue('powerLimitW'), pcieRx = gpuValue('pcieRxMbs');
const vramTotal = gpuCapacity(selectedGpus);
const vramTotal = gpuCapacity(report('memoryUsedGb').partial ? report('memoryUsedGb').devices : selectedGpus);
const singleReading = selectedGpus.length === 1 ? readings?.find(r => r.id === gpuId(selectedGpus[0])) : undefined;
const gen = singleReading?.pcieGenMax ?? singleReading?.pcieGen, width = singleReading?.pcieWidth;
const cpu = cpuChoice === 'all' ? data.hardware.cpuLoad : data.hardware.cpus?.find(p => p.id === cpuChoice)?.utilizationPct;
@@ -132,7 +135,7 @@ function LegacyMonitorScreen({ native }: {
const contextPct = contextKnown ? percent(contextUsed, data.context.maxTokens) : null;
const cacheKnown = ['engine', 'demo'].includes(data.expertCache.source ?? '');
const cacheMemory = cacheKnown ? data.expertCache.memoryGb : null;
const cachePercent = hasReading(cacheMemory) ? percent(cacheMemory, gpuCapacity(devices)) : cacheKnown ? percent(data.expertCache.used, data.expertCache.total) : null;
const cachePercent = hasReading(cacheMemory) ? percent(cacheMemory, gpuCapacity(defaultDevices)) : cacheKnown ? percent(data.expertCache.used, data.expertCache.total) : null;
const ramPct = percent(data.hardware.ramUsedGb, data.hardware.ramTotalGb);
const history = data.history, timestamps = history.map(s => s.t);
const historyGpu = (key: Parameters<typeof gpuMetric>[2], fallback: 'gpuLoad' | 'vramUsedGb' | 'gpuTempC' | 'powerW' | 'pcieRxMbs', mode: Parameters<typeof gpuMetric>[3] = 'sum') => history.map(sample => sample.gpus ? gpuMetric(selectedGpus, sample.gpus, key, mode) : (sample.gpuIds ? selectedGpus.length === sample.gpuIds.length && selectedGpus.every(gpu => sample.gpuIds!.includes(gpuId(gpu))) : gpuChoice === 'all') ? sample[fallback] : null);
@@ -158,17 +161,17 @@ function LegacyMonitorScreen({ native }: {
{tr(multipleModels ? <section className="st-card"><h2 className="h4">{tr("Model performance")}</h2><div className="st-tablewrap"><table className="st-table native-performance-table"><thead><tr><th>{tr("Model / device")}</th><th>{tr("Generating tok/s")}</th><th>{tr("Reading tok/s")}</th><th>{tr("State")}</th><th>{tr("Active / queued")}</th></tr></thead><tbody>{tr(runtime?.instances.map(i => { const recent = (data.requests ?? []).find(r => r.instanceId === i.id); const writing = i.phase === 'generating' || i.phase === 'thinking', reading = i.phase === 'reading'; return <tr key={i.id}><td><button className="text-btn strong" onClick={() => chooseInstance(i.id)}>{tr(instanceLabel(i))}</button><small>{tr(deviceLabel(i.gpuIds, inventory))}</small></td><td>{tr(number(writing ? i.outputTps : recent?.tps, 1))}<small>{tr(writing ? 'Now' : recent ? 'Last request' : 'No request yet')}</small></td><td>{tr(number(reading ? i.promptTps : recent?.promptTps, 0))}<small>{tr(reading ? 'Now' : recent ? 'Last request' : 'No request yet')}</small></td><td>{tr(i.phase)}{tr(runtime.mode === 'simulated' ? <small>{tr("Illustrative")}</small> : null)}</td><td>{tr(number(i.active))} / {tr(number(i.queued))}</td></tr>; }))}</tbody></table></div></section> : null)}
{tr(multipleModels ? <p className="xs muted monitor-scope-note">{tr(selectedInstance ? selectedInstance.gpuIds.length ? 'Requests, context and speed are scoped to this model. GPU readings include all work on the selected devices.' : 'CPU-only model. Requests and context are scoped to this model; system metrics are shared host measurements.' : 'Select a model for its context and speed. GPU readings and system RAM are shared host measurements.')}</p> : null)}
<section className="st-card monitor-state" aria-label={tr("Model state")} data-snapshot-skip>
<div className="monitor-state__head"><h2 className="h4">{tr(multipleScope ? 'Model activity' : 'Model state')}</h2><div className="monitor-state__badges">{tr([{ id: 'idle', label: 'Idle' }, { id: 'reading', label: 'Reading' }, { id: 'generating', label: 'Generating' }, { id: 'queued', label: 'Queued' }, { id: 'error', label: 'Error' }].map(s => <span key={s.id} className="st-badge" data-phase={s.id} data-active={phase === s.id || s.id === 'queued' && data.queue.waiting > 0}>{tr(s.label)}{tr(s.id === 'queued' && data.queue.waiting > 0 ? ` (${data.queue.waiting})` : '')}</span>))}</div><div className="monitor-device-selectors">{tr(devices.length > 1 ? <Select ariaLabel={tr("Monitor GPU")} value={gpuChoice} options={[...(selectedInstance ? [{ value: 'assigned', label: 'Assigned GPUs' }] : []), { value: 'all', label: `All GPUs (${devices.length})` }, ...devices.map((gpu, i) => ({ value: gpuId(gpu), label: `GPU ${i + 1} · ${gpu.name}` }))]} menuMinWidth={340} onChange={setGpuSelection}/> : null)}{tr(processors.length > 1 ? <Select ariaLabel={tr("Monitor processor")} value={cpuChoice} options={[{ value: 'all', label: `All processors (${processors.length})` }, ...processors.map((p, i) => ({ value: p.id, label: `CPU ${i + 1} · ${p.model}` }))]} onChange={setCpuSelection}/> : null)}</div></div>
<div className="monitor-state__head"><h2 className="h4">{tr(multipleScope ? 'Model activity' : 'Model state')}</h2><div className="monitor-state__badges">{tr([{ id: 'idle', label: 'Idle' }, { id: 'reading', label: 'Reading' }, { id: 'generating', label: 'Generating' }, { id: 'queued', label: 'Queued' }, { id: 'error', label: 'Error' }].map(s => <span key={s.id} className="st-badge" data-phase={s.id} data-active={phase === s.id || s.id === 'queued' && data.queue.waiting > 0}>{tr(s.label)}{tr(s.id === 'queued' && data.queue.waiting > 0 ? ` (${data.queue.waiting})` : '')}</span>))}</div><div className="monitor-device-selectors">{tr(devices.length > 1 ? <Select ariaLabel={tr("Monitor GPU")} value={gpuChoice} options={[...(selectedInstance ? [{ value: 'assigned', label: 'Assigned GPUs' }] : []), { value: 'all', label: `All GPUs (${defaultDevices.length})` }, ...devices.map((gpu, i) => ({ value: gpuId(gpu), label: `GPU ${i + 1} · ${gpu.name}` }))]} menuMinWidth={340} onChange={setGpuSelection}/> : null)}{tr(processors.length > 1 ? <Select ariaLabel={tr("Monitor processor")} value={cpuChoice} options={[{ value: 'all', label: `All processors (${processors.length})` }, ...processors.map((p, i) => ({ value: p.id, label: `CPU ${i + 1} · ${p.model}` }))]} onChange={setCpuSelection}/> : null)}</div></div>
<div className="monitor-state__reading small"><span>{tr(multipleScope ? `${number(data.queue.running)} active · ${number(data.queue.waiting)} queued` : data.phase === 'idle' ? 'Waiting for a request' : data.phaseLabel)}</span><span className="muted num">{tr(statusDetail)}</span></div>
{tr(!multipleScope ? <Bar label={tr("Model progress")} value={progress ?? 0} indeterminate={writing && !hasReading(progress)} tone={data.phase === 'reading' ? 'info' : 'accent'}/> : null)}
</section>
<div className="monitor-metrics">
{tr(!multipleScope ? <MonitorMetric label={tr("Speed")} icon="gauge" value={number(decode, 1)} unit={hasReading(decode) ? 't/s' : ''} source={throughput} sub={writing ? 'Decode now' : last ? 'Decode last request' : 'Decode'} secondary={<div><div className="st-metric__value">{tr(number(prefill))}{tr(hasReading(prefill) ? <span className="st-metric__unit">{tr("t/s")}</span> : null)}</div><div className="monitor-metric__sub xs muted">{tr(data.phase === 'reading' ? 'Prefill now' : writing ? 'Prefill this request' : last ? 'Prefill last request' : 'Prefill')}</div></div>} chart={<ReferenceSpark label={tr("Speed")} data={history.map(s => throughputKnown ? s.outputTps : null)} secondary={{ data: history.map(s => throughputKnown ? s.promptTps : null), label: 'Prefill', tone: 'var(--st-chart-prefill)' }} tone="var(--st-chart-decode)" unit="t/s" timestamps={timestamps}/>}/> : null)}
<MonitorMetric label={tr("GPU load")} icon="gpu" value={number(gpuLoad)} unit={hasReading(gpuLoad) ? '%' : ''} source={gpuSource('utilizationPct', 'mean')} sub={gpuName} chart={chart('GPU load', historyGpu('utilizationPct', 'gpuLoad', 'mean'), '%', 100)}/>
<MonitorMetric label={tr(shared ? 'Shared GPU memory' : 'VRAM')} icon="layers" value={number(vram, 1)} unit={hasReading(vram) ? vramTotal ? `/ ${number(vramTotal, 0)} GB` : 'GB' : ''} source={gpuSource('memoryUsedGb')} sub={shared ? 'Same memory pool as system RAM' : multipleGpus ? 'Total across selected GPUs' : cacheKnown && devices.length === 1 ? `${number(data.expertCache.used)} experts cached` : 'GPU memory'} chart={chart(shared ? 'Shared GPU memory' : 'VRAM', historyGpu('memoryUsedGb', 'vramUsedGb'), 'GB', vramTotal)}/>
<MonitorMetric label={tr("GPU temp")} icon="thermometer" value={number(temperature)} unit={hasReading(temperature) ? '°C' : ''} source={gpuSource('temperatureC', 'max')} sub={multipleGpus ? 'Hottest selected GPU' : selectedGpus[0]?.name ?? (selectedInstance && !selectedInstance.gpuIds.length ? native ? 'Automatic GPU placement' : 'CPU only · no GPU assigned' : 'No GPU detected')} chart={chart('GPU temperature', historyGpu('temperatureC', 'gpuTempC', 'max'), '°C', 90, 'var(--st-warn)')}/>
<MonitorMetric label={tr("Power")} icon="bolt" value={number(power)} unit={hasReading(power) ? 'W' : ''} source={gpuSource('powerW')} sub={hasReading(powerLimit) ? `of ${number(powerLimit)} W limit` : multipleGpus ? 'Total across selected GPUs' : 'GPU power draw'} chart={chart('GPU power', historyGpu('powerW', 'powerW'), 'W', powerLimit ?? undefined)}/>
<MonitorMetric label={tr("PCIe")} icon="link" value={hasReading(gen) ? `Gen${gen}` : multipleGpus ? number(pcieRx, 1) : '—'} unit={hasReading(gen) && hasReading(width) ? `x${width}` : multipleGpus && hasReading(pcieRx) ? 'MB/s' : ''} source={hasReading(gen) ? sourceLabel(singleReading?.source) : gpuSource('pcieRxMbs')} sub={hasReading(pcieRx) ? `To GPU ${number(pcieRx, 1)} MB/s${hasReading(gen) && hasReading(singleReading?.pcieGen) && singleReading.pcieGen < gen ? ` · idle Gen${singleReading.pcieGen}` : ''}` : shared ? 'Shared memory · no PCIe link' : multipleGpus ? 'Total transfer to selected GPUs' : 'Transfer to GPU unavailable'} chart={chart('PCIe transfer', historyGpu('pcieRxMbs', 'pcieRxMbs'), 'MB/s', undefined, 'var(--st-info)')}/>
<MonitorMetric label={tr("GPU load")} icon="gpu" value={number(gpuLoad)} unit={hasReading(gpuLoad) ? '%' : ''} source={gpuSource('utilizationPct', 'mean')} sub={whose('utilizationPct', 'mean') ?? gpuName} chart={chart('GPU load', historyGpu('utilizationPct', 'gpuLoad', 'mean'), '%', 100)}/>
<MonitorMetric label={tr(shared ? 'Shared GPU memory' : 'VRAM')} icon="layers" value={number(vram, 1)} unit={hasReading(vram) ? vramTotal ? `/ ${number(vramTotal, 0)} GB` : 'GB' : ''} source={gpuSource('memoryUsedGb')} sub={whose('memoryUsedGb') ?? (shared ? 'Same memory pool as system RAM' : multipleGpus ? 'Total across selected GPUs' : cacheKnown && devices.length === 1 ? `${number(data.expertCache.used)} experts cached` : 'GPU memory')} chart={chart(shared ? 'Shared GPU memory' : 'VRAM', historyGpu('memoryUsedGb', 'vramUsedGb'), 'GB', vramTotal)}/>
<MonitorMetric label={tr("GPU temp")} icon="thermometer" value={number(temperature)} unit={hasReading(temperature) ? '°C' : ''} source={gpuSource('temperatureC', 'max')} sub={whose('temperatureC', 'max') ?? (multipleGpus ? 'Hottest selected GPU' : selectedGpus[0]?.name ?? (selectedInstance && !selectedInstance.gpuIds.length ? native ? 'Automatic GPU placement' : 'CPU only · no GPU assigned' : 'No GPU detected'))} chart={chart('GPU temperature', historyGpu('temperatureC', 'gpuTempC', 'max'), '°C', 90, 'var(--st-warn)')}/>
<MonitorMetric label={tr("Power")} icon="bolt" value={number(power)} unit={hasReading(power) ? 'W' : ''} source={gpuSource('powerW')} sub={whose('powerW') ?? (hasReading(powerLimit) ? `of ${number(powerLimit)} W limit` : multipleGpus ? 'Total across selected GPUs' : 'GPU power draw')} chart={chart('GPU power', historyGpu('powerW', 'powerW'), 'W', powerLimit ?? undefined)}/>
<MonitorMetric label={tr("PCIe")} icon="link" value={hasReading(gen) ? `Gen${gen}` : multipleGpus ? number(pcieRx, 1) : '—'} unit={hasReading(gen) && hasReading(width) ? `x${width}` : multipleGpus && hasReading(pcieRx) ? 'MB/s' : ''} source={hasReading(gen) ? sourceLabel(singleReading?.source) : gpuSource('pcieRxMbs')} sub={hasReading(pcieRx) ? `To GPU ${number(pcieRx, 1)} MB/s${whose('pcieRxMbs') ? ` · ${whose('pcieRxMbs')}` : ''}${hasReading(gen) && hasReading(singleReading?.pcieGen) && singleReading.pcieGen < gen ? ` · idle Gen${singleReading.pcieGen}` : ''}` : shared ? 'Shared memory · no PCIe link' : multipleGpus ? 'Total transfer to selected GPUs' : 'Transfer to GPU unavailable'} chart={chart('PCIe transfer', historyGpu('pcieRxMbs', 'pcieRxMbs'), 'MB/s', undefined, 'var(--st-info)')}/>
<MonitorMetric label={tr("CPU")} icon="cpu" value={number(cpu)} unit={hasReading(cpu) ? '%' : ''} source={hasReading(cpu) ? 'Measured' : 'Unavailable'} sub={cpuName} chart={chart('CPU utilization', historyCpu, '%', 100)}/>
<MonitorMetric label={tr("Disk read")} icon="disk" value={number(hasReading(data.hardware.diskReadGbs) ? data.hardware.diskReadGbs >= 1 ? data.hardware.diskReadGbs : data.hardware.diskReadGbs * 1024 : null, 1)} unit={hasReading(data.hardware.diskReadGbs) ? data.hardware.diskReadGbs >= 1 ? 'GB/s' : 'MB/s' : ''} source={hasReading(data.hardware.diskReadGbs) ? 'Measured' : 'Unavailable'} sub={hasReading(data.hardware.diskWriteMbs) ? `Write ${number(data.hardware.diskWriteMbs, 1)} MB/s` : 'Read throughput'} chart={chart('Disk read', history.map(s => hasReading(s.diskReadGbs) ? s.diskReadGbs * 1024 : null), 'MB/s', undefined, 'var(--st-info)')}/>
</div>
+2 -7
View File
@@ -1,3 +1,4 @@
import { copyText } from '../lib/clipboard';
import { tr } from '../lib/i18n';
import { appBaseUrl } from '../lib/connect-urls';
import { deviceLabel, instanceLabel } from '../lib/runtime-models';
@@ -73,13 +74,7 @@ export function ServingScreen() {
}
}
async function copyConnectionValue(value: string, label: string) {
try {
await navigator.clipboard.writeText(value);
pushToast('success', 'Copied', label);
}
catch {
pushToast('warn', 'Couldn’t copy', 'Select the value and copy it manually.');
}
await copyText(value, label, { toast: pushToast });
}
const code = `curl ${serving.baseUrl}${protocol === 'responses' ? '/responses' : protocol === 'openai' ? serving.endpoints.openai : serving.endpoints.anthropic} \\\n -H 'Content-Type: application/json'${serving.access.apiKeySet ? " \\\n -H 'Authorization: Bearer YOUR_API_KEY'" : ''} \\\n -d '${JSON.stringify({ model: loaded?.apiModelId ?? 'YOUR_MODEL_ID', ...(protocol === 'responses' ? { input: 'Hello', store: false } : { ...(protocol === 'anthropic' ? { max_tokens: 256 } : {}), messages: [{ role: 'user', content: 'Hello' }] }) }, null, 2)}'`;
return <Screen title={tr("Serving")} actions={<><Btn icon="link" onClick={() => setCloudflareOpen(true)}>{tr("Cloudflare tunnel")}</Btn><IconBtn name="info" title={tr("Connection guide")} onClick={() => setGuide(true)}/></>}><div className="stack stack--lg">
+7
View File
@@ -3,10 +3,17 @@ import { GuidedSetup } from '../components/GuidedSetup';
import { useNative } from '../lib/native';
import { useStore } from '../lib/store';
import { useEngineUnpacking } from '../lib/engine-setup';
import { EngineFirstRun, useEngineNeeded } from '../components/EngineFirstRun';
import { useEffect, useState } from 'react';
/** Every setup entry uses the same guided onboarding. */
export function SetupScreen() {
const native = useNative(), setup = useStore(s => s.setup);
const needsEngine = useEngineNeeded(), [engineStepDone, setEngineStepDone] = useState(false), [cardShown, setCardShown] = useState(false);
useEffect(() => { if (needsEngine) setCardShown(true); }, [needsEngine]);
const unpacking = useEngineUnpacking(!native || !setup);
// No engine on this PC yet: the first-run card comes before the model steps (it works before the native snapshot has connected).
if (!engineStepDone && !setup?.complete && (needsEngine || cardShown))
return <EngineFirstRun onDone={() => setEngineStepDone(true)}/>;
if (!native || !setup)
return <div className="setup guided-onboarding"><p className="onboard-loading" role="status">{tr("Getting your setup ready\u2026")}</p>{unpacking ? <p className="onboard-loading small" role="status">{tr("Unpacking the engine that came with the app. This takes about a minute the first time.")}</p> : null}</div>;
return <GuidedSetup />;
+5
View File
@@ -4083,6 +4083,8 @@ textarea.st-input {
.st-msg--user .st-bubble {
background: var(--st-accent-surface);
color: var(--st-accent-surface-ink);
white-space: pre-wrap; /* a typed or pasted message keeps its line breaks (#3720) */
overflow-wrap: anywhere;
}
/* October 2026 clarity pass. App-specific extensions survive design-kit sync. */
@@ -7517,3 +7519,6 @@ textarea {
.md-math-block .md-math--display { margin: 0; }
.md-math .katex { font-size: 1.08em; }
.md-math .katex-display { margin: 0; }
/* A message the person typed keeps its line breaks (#3720). */
.st-bubble__text { white-space: pre-wrap; overflow-wrap: anywhere; }
+106 -1
View File
@@ -5,7 +5,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, readdi
import { createHash } from 'node:crypto';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { installBundledEngine, compareVersions, wrongMakeNotice } from '../server/bundled-engine.mjs';
import { installBundledEngine, removeVerifiedBundle, cleanupAfterInstall, compareVersions, wrongMakeNotice } from '../server/bundled-engine.mjs';
// the make of card is read from this computer unless a test names it; the existing tests are written for an NVIDIA PC
process.env.STRATA_ENGINE_BACKEND = 'cuda';
@@ -185,3 +185,108 @@ test('Windows: the display adapters decide the make, and a leftover nvidia-smi d
assert.equal(windowsDisplayVendors({ platform: 'linux', run: () => { throw new Error('must not run'); } }).size, 0);
assert.equal(windowsDisplayVendors({ platform: 'win32', run: () => { throw new Error('no registry'); } }).size, 0);
});
test('after an update, the engine bundle of the old install folder is removed only when the same engine is complete in the store', async () => {
const t = setup();
try {
const name = makePackage(t.bundle, '0.1.48');
writeFileSync(join(t.bundle, '.gitignore'), 'ignore-all');
const lines = [];
// 1. nothing in the store yet: kept, and one line says why
let r = await removeVerifiedBundle({ bundleDir: t.bundle, components: t.components, log: l => lines.push(l) });
assert.equal(r.removed, false);
assert.equal(lines.length, 1);
assert.match(lines[0], /kept in the install folder.*not \(or not completely\) in the engine store/);
assert.ok(existsSync(join(t.bundle, name)));
// 2. the engine is copied (the app's own first start does this)
assert.equal((await installBundledEngine({ bundleDir: t.bundle, components: t.components })).action, 'installed');
// 3. a store file with the right size and the wrong content: kept (the hash is checked, not only the size)
const target = join(t.engine, name, 'strata.exe');
const good = readFileSync(target);
writeFileSync(target, Buffer.alloc(good.length, 'x'));
lines.length = 0;
r = await removeVerifiedBundle({ bundleDir: t.bundle, components: t.components, log: l => lines.push(l) });
assert.equal(r.removed, false);
assert.equal(lines.length, 1, 'exactly one line');
assert.ok(existsSync(join(t.bundle, name)));
// 4. a store file with the wrong size: kept
writeFileSync(target, Buffer.concat([good, Buffer.from('extra')]));
assert.equal((await removeVerifiedBundle({ bundleDir: t.bundle, components: t.components })).removed, false);
// 5. the store copy is whole: the bundle goes, its .gitignore stays, and the store is untouched
writeFileSync(target, good);
lines.length = 0;
r = await removeVerifiedBundle({ bundleDir: t.bundle, components: t.components, log: l => lines.push(l) });
assert.equal(r.removed, true);
assert.equal(existsSync(join(t.bundle, name)), false);
assert.ok(existsSync(join(t.bundle, '.gitignore')));
assert.equal(readFileSync(target, 'utf8'), 'engine-0.1.48');
assert.match(lines.at(-1), /was removed; the same engine is complete in the engine store/);
// 6. nothing left to do
assert.equal((await removeVerifiedBundle({ bundleDir: t.bundle, components: t.components })).removed, false);
// 7. a different version in the store does not count
const other = makePackage(t.bundle, '0.1.49');
assert.equal((await removeVerifiedBundle({ bundleDir: t.bundle, components: t.components })).removed, false);
assert.ok(existsSync(join(t.bundle, other)));
} finally { t.cleanup(); }
});
test('the bundle cleanup lines reach the output of the host (the desktop log copies it), for "removed" and for "kept"', async () => {
const t = setup();
const seen = [], real = console.log;
try {
const name = makePackage(t.bundle, '0.1.48');
console.log = line => { seen.push(String(line)); };
// kept: the engine is not in the store yet
await cleanupAfterInstall(Promise.resolve({ action: 'kept' }), { bundleDir: t.bundle, components: t.components });
assert.equal(seen.length, 1);
assert.match(seen[0], /^engine bundle: kept in the install folder/);
// removed: the engine is in the store and whole
const installed = await installBundledEngine({ bundleDir: t.bundle, components: t.components });
assert.equal(installed.action, 'installed');
seen.length = 0;
await cleanupAfterInstall(Promise.resolve({ action: 'kept' }), { bundleDir: t.bundle, components: t.components });
assert.equal(seen.length, 1);
assert.match(seen[0], /^engine bundle: the copy that came with the installer \(.*\) was removed/);
assert.equal(existsSync(join(t.bundle, name)), false);
// nothing to say when the engine step did nothing (no bundle) or was refused
seen.length = 0;
await cleanupAfterInstall(Promise.resolve({ action: 'none' }), { bundleDir: t.bundle, components: t.components });
await cleanupAfterInstall(Promise.resolve({ action: 'wrong-make' }), { bundleDir: t.bundle, components: t.components });
await cleanupAfterInstall(Promise.reject(new Error('boom')), { bundleDir: t.bundle, components: t.components });
assert.deepEqual(seen, []);
} finally { console.log = real; t.cleanup(); }
});
test('macOS: the engine bundle inside a signed .app stays (deleting it breaks the signature); outside an .app, or on another system, it is removed as before', async () => {
const t = setup();
const platform = Object.getOwnPropertyDescriptor(process, 'platform');
const as = value => Object.defineProperty(process, 'platform', { value, configurable: true });
try {
const app = join(t.base, 'Strata.app', 'Contents', 'Resources', 'engine-bundle');
mkdirSync(app, { recursive: true });
const inApp = makePackage(app, '0.1.48');
const outside = makePackage(t.bundle, '0.1.48');
// the engine is in the store (the app's own first start copies it), so the bundle would be removable
assert.equal((await installBundledEngine({ bundleDir: t.bundle, components: t.components })).action, 'installed');
const lines = [];
// 1. macOS, bundle inside Strata.app: kept, with one line that says why
as('darwin');
let r = await removeVerifiedBundle({ bundleDir: app, components: t.components, log: l => lines.push(l) });
assert.equal(r.removed, false);
assert.ok(existsSync(join(app, inApp, 'strata.exe')), 'the signed app keeps its engine files');
assert.equal(lines.length, 1);
assert.match(lines[0], /kept in the install folder.*macOS app bundles must remain unchanged after signing/);
// 2. macOS, bundle outside an .app: removed as before
r = await removeVerifiedBundle({ bundleDir: t.bundle, components: t.components, log: () => {} });
assert.equal(r.removed, true);
assert.equal(existsSync(join(t.bundle, outside)), false);
// 3. another system, bundle inside a folder called .app: removed as before (the guard is for macOS only)
as('linux');
r = await removeVerifiedBundle({ bundleDir: app, components: t.components, log: () => {} });
assert.equal(r.removed, true);
assert.equal(existsSync(join(app, inApp)), false);
} finally {
Object.defineProperty(process, 'platform', platform);
t.cleanup();
}
});
+22
View File
@@ -0,0 +1,22 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { transform } from 'esbuild';
const { code } = await transform(await readFile(new URL('../src/lib/chat-attach-file.ts', import.meta.url), 'utf8'), { loader: 'ts', format: 'esm' });
const { uploadChatFile, FILE_ACCEPT } = await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`);
test('the file dialog lists PDFs and text on Linux: the types sit next to the extensions (#3838)', () => {
for (const t of ['image/*', 'application/pdf', 'text/*', '.pdf', '.md']) assert.ok(FILE_ACCEPT.split(',').includes(t), t);
});
test('a PDF is sent to the chat folder through the upload route and named back', async () => {
let seen;
const fake = async (url, init) => { seen = { url, init }; return { ok: true, json: async () => ({ files: [{ name: 'report-2.pdf', attached: true }] }) }; };
const out = await uploadChatFile(new File(['%PDF-1.4'], 'report.pdf', { type: 'application/pdf' }), 's 1', fake);
assert.equal(out.name, 'report-2.pdf');
assert.match(seen.url, /\/api\/chat\/upload\?sessionId=s%201&name=report\.pdf$/);
assert.equal(seen.init.method, 'POST'); assert.equal(seen.init.headers['content-type'], 'application/pdf');
});
test('a refused upload says why', async () => {
await assert.rejects(uploadChatFile(new File(['x'], 'a.pdf'), 's', async () => ({ ok: false, json: async () => ({ error: '"a.pdf" is bigger than 12 MB.' }) })), /bigger than 12 MB/);
});
+30
View File
@@ -0,0 +1,30 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { transform } from 'esbuild';
const { code } = await transform(await readFile(new URL('../src/lib/clipboard.ts', import.meta.url), 'utf8'), { loader: 'ts', format: 'esm' });
const { copyText } = await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`);
const toasts = () => { const list = []; return { list, toast: (...a) => list.push(a) }; };
test('Copy says Copied when the clipboard takes the text (#3731)', async () => {
const t = toasts(); let got = '';
assert.equal(await copyText('hello', 'Response', { clipboard: { writeText: async v => { got = v; } }, toast: t.toast }), true);
assert.equal(got, 'hello'); assert.equal(t.list[0][0], 'success');
});
test('a refused clipboard falls back to the older copy command', async () => {
const t = toasts(); let got = '';
assert.equal(await copyText('x', 'Response', { clipboard: { writeText: async () => { throw new Error('denied'); } }, legacyCopy: v => { got = v; return true; }, toast: t.toast }), true);
assert.equal(got, 'x'); assert.equal(t.list[0][0], 'success');
});
test('when nothing works the person is told, never left with a silent click', async () => {
const t = toasts();
assert.equal(await copyText('x', 'Response', { clipboard: null, legacyCopy: () => false, toast: t.toast }), false);
assert.equal(t.list.length, 1); assert.equal(t.list[0][0], 'warn'); assert.match(t.list[0][2], /Ctrl\+C/);
});
test('the typed message keeps its line breaks (#3720) and the chat uses the shared copy', async () => {
const css = await readFile(new URL('../src/styles/app.css', import.meta.url), 'utf8');
assert.match(css, /\.st-bubble__text\s*\{[^}]*white-space:\s*pre-wrap/);
const chat = await readFile(new URL('../src/screens/Chat.tsx', import.meta.url), 'utf8');
assert.doesNotMatch(chat, /void navigator\.clipboard\.writeText/);
});
+121 -7
View File
@@ -1,11 +1,12 @@
import './helpers/isolate-profile.mjs'; // a test run never writes the owner's real Strata profile
import test from 'node:test';
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, existsSync } from 'node:fs';
import { join, basename } from 'node:path';
import { tmpdir } from 'node:os';
import { bundledPython, controlEnvironment, createControl, findControl, runCli, updatedControlDirectory } from '../server/control-runtime.mjs';
import { checkComponentUpdates } from '../server/updates.mjs';
import { checkComponentUpdates, engineFirstRun } from '../server/updates.mjs';
import { createAppUpdater } from '../server/app-updater.mjs';
import { createNativeControl } from '../server/native-control.mjs';
@@ -103,8 +104,25 @@ test('createControl: the update calls ask the explicit commands; a failed step c
await assert.rejects(failing.update({}), err => err.code === 'engine_serving' && /serving/.test(err.message) && err.exitCode === 7);
});
test('the Settings check says so, in plain words, when the own update check of the app fails, and keeps the raw text for Details', async () => {
const feed = (control = { installed: '0.2.5', offered: '0.2.5', status: 'current' }) => ({ version: '0.2.5', async updateCheck() { return { ok: true, feed: { sequence: 3 }, engine: { installed: { version: '0.1.50' }, offered: { version: '0.1.50' }, status: 'current' }, control, app: { status: 'not-in-feed' } }; } });
const failing = { check: async () => { throw new Error('error decoding response body for url (http://127.0.0.1:19090/latest.json)'); } };
const nothing = await checkComponentUpdates({ control: feed(), appUpdater: failing, appVersion: '0.2.5' });
assert.equal(nothing.strata.status, 'error', 'it no longer says "up to date" when the app could not be checked');
assert.match(nothing.strata.message, /couldn't check for a new version of the app itself/);
assert.doesNotMatch(nothing.strata.message, /decoding|http|latest\.json/, 'no raw text in the main line');
assert.match(nothing.strata.details, /error decoding response body/);
const withControl = await checkComponentUpdates({ control: feed({ installed: '0.2.5', offered: '0.2.6', status: 'available' }), appUpdater: failing, appVersion: '0.2.5' });
assert.equal(withControl.strata.status, 'available');
assert.match(withControl.strata.message, /new version of Strata is available.*couldn't check for a new version of the app itself/s);
assert.match(withControl.strata.details, /decoding/);
const fine = await checkComponentUpdates({ control: feed(), appUpdater: { check: async () => ({ available: false }) }, appVersion: '0.2.5' });
assert.equal(fine.strata.status, 'current');
assert.equal(fine.strata.details, undefined);
});
test('the Settings update check maps the feed to the existing result shape', async () => {
const report = overrides => ({ ok: true, feed: { sequence: 3 }, engine: { installed: { version: '0.1.45' }, offered: { version: '0.1.46' }, status: 'available', download_bytes: 505 * (1 << 20) }, control: { installed: '0.1.45', offered: '0.1.45', status: 'current' }, app: { status: 'not-in-feed' }, ...overrides });
const report = overrides => ({ ok: true, feed: { sequence: 3 }, engine: { installed: { version: '0.1.45' }, offered: { version: '0.1.46' }, status: 'available', download_bytes: 505_000_000 }, control: { installed: '0.1.45', offered: '0.1.45', status: 'current' }, app: { status: 'not-in-feed' }, ...overrides });
const make = value => ({ version: '0.1.45', async updateCheck() { if (value instanceof Error) throw value; return value; } });
let result = await checkComponentUpdates({ control: make(report()), appVersion: '0.1.0' });
assert.equal(result.engine.status, 'available');
@@ -113,7 +131,7 @@ test('the Settings update check maps the feed to the existing result shape', asy
assert.doesNotMatch(result.engine.message, /0\.1\.4|sha|exit/i);
assert.equal(result.engine.canInstall, true);
assert.equal(result.strata.status, 'current');
result = await checkComponentUpdates({ control: make(report({ engine: { installed: null, offered: { version: '0.1.46' }, status: 'install', download_bytes: 505 * (1 << 20) } })) });
result = await checkComponentUpdates({ control: make(report({ engine: { installed: null, offered: { version: '0.1.46' }, status: 'install', download_bytes: 505_000_000 } })) });
assert.equal(result.engine.status, 'available');
assert.match(result.engine.message, /needs an engine to run models\. Press Update engine/);
result = await checkComponentUpdates({ control: make(report({ engine: { installed: { version: '0.1.46' }, offered: { version: '0.1.46' }, status: 'current' } })) });
@@ -165,7 +183,7 @@ test('the shell bridge client exists only inside the desktop app, speaks to loop
// ---------------------------------------------------------------------------------- the Settings buttons in the packaged app
function fixture({ withApp = false, update } = {}) {
function fixture({ withApp = false, update, appInstall } = {}) {
const root = mkdtempSync(join(tmpdir(), 'strata-update-ops-'));
const install = join(root, 'install'), data = join(root, 'data');
mkdirSync(install, { recursive: true });
@@ -187,15 +205,16 @@ function fixture({ withApp = false, update } = {}) {
previous = '0.1.45-windows-x86_64-cuda';
return { ok: true, results: [{ component: 'engine', status: 'updated', installed_after: { version: '0.1.46' } }] };
},
async installEngineFile(path, options) { calls.push(['install-file', path]); options.onEvent?.({ event: 'phase', phase: 'verify', message: 'Checking' }); return { ok: true, results: [{ component: 'engine', status: 'updated', installed_after: { version: '0.1.46' } }] }; },
async rollbackEngine() { calls.push(['rollback']); previous = null; return { ok: true, to: '0.1.45' }; },
};
const spawned = [];
const appUpdater = withApp ? {
check: async () => ({ available: true, version: '0.2.0' }),
install: async ({ onProgress }) => { calls.push(['app-install']); onProgress({ downloaded: 50, total: 100 }); return { state: 'done' }; },
install: appInstall ?? (async ({ onProgress }) => { calls.push(['app-install']); onProgress({ downloaded: 50, total: 100 }); return { state: 'done' }; }),
} : null;
const native = createNativeControl({ control, appUpdater, directory: data, spawnProcess: (...args) => { if (!args[1]?.some(a => String(a).endsWith('strata_mcp.py'))) spawned.push(args); throw new Error('no process but the installer tool (Control strata_mcp.py, absent in this fixture) may be started by the host in packaged mode'); }, fetcher: async () => Response.json({}), requests: () => [] });
return { native, calls, spawned, cleanup: () => rmSync(root, { recursive: true, force: true }) };
return { native, calls, spawned, data, control, cleanup: () => rmSync(root, { recursive: true, force: true }) };
}
const finish = async (native, id) => { for (let i = 0; i < 400; i++) { const job = native.state().jobs.find(j => j.id === id); if (job.state !== 'running') return job; await new Promise(r => setTimeout(r, 10)); } throw new Error('job did not finish'); };
@@ -264,6 +283,101 @@ test('packaged: "Update Strata" updates Control through the feed, then the app t
} finally { await f.native.close(); f.cleanup(); }
});
test('packaged: an app update that ends the app is recorded as finished by the new version, not as an interrupted operation', async () => {
const before = process.env.STRATA_APP_VERSION;
process.env.STRATA_APP_VERSION = '0.2.2';
const f = fixture({ withApp: true, appInstall: () => new Promise(() => {}) }); // the installer ends the app: install() never returns
try {
await f.native.refresh(true);
const started = f.native.operation('update-strata', {});
await new Promise(r => setTimeout(r, 60));
assert.equal(f.native.state().jobs.find(j => j.id === started.id).state, 'running');
const saved = JSON.parse(readFileSync(join(f.data, 'native-jobs.json'), 'utf8')).find(j => j.id === started.id);
assert.equal(saved.appUpdateFrom, '0.2.2', 'the job was saved with the version it started from before the app was ended');
// the app is gone; the installer starts the new one (0.2.3) on the same data
process.env.STRATA_APP_VERSION = '0.2.3';
const again = createNativeControl({ control: f.control, directory: f.data, spawnProcess: () => { throw new Error('no process'); }, fetcher: async () => Response.json({}), requests: () => [] });
const job = again.state().jobs.find(j => j.id === started.id);
assert.equal(job.state, 'done');
assert.equal(job.error, null);
assert.match(job.summary, /updated to 0\.2\.3/);
await again.close();
// the same record read by an app that did NOT change version stays an interruption
process.env.STRATA_APP_VERSION = '0.2.2';
const same = createNativeControl({ control: f.control, directory: f.data, spawnProcess: () => { throw new Error('no process'); }, fetcher: async () => Response.json({}), requests: () => [] });
const unfinished = same.state().jobs.find(j => j.id === started.id);
assert.equal(unfinished.state, 'error');
assert.match(unfinished.error, /update to 0\.2\.[0-9] did not finish.*releases/s, 'the app came back as the old version: the installer did not finish, and the alert says so');
await same.close();
} finally { if (before === undefined) delete process.env.STRATA_APP_VERSION; else process.env.STRATA_APP_VERSION = before; await f.native.close(); f.cleanup(); }
});
test('packaged: an update job an OLDER host left running is finished when the app program file is newer than the job', async () => {
const exe = new URL('../../strata-desktop.exe', import.meta.url); // <install>/strata-desktop.exe as seen from <install>/host
const f = fixture({ withApp: true });
try {
const { fileURLToPath } = await import('node:url');
const old = { id: 'old-1', kind: 'update-strata', state: 'running', startedAt: new Date(Date.now() - 60_000).toISOString(), summary: 'Starting…', result: null, error: null };
writeFileSync(join(f.data, 'native-jobs.json'), JSON.stringify([old]));
const mk = () => createNativeControl({ control: f.control, directory: f.data, spawnProcess: () => { throw new Error('no process'); }, fetcher: async () => Response.json({}), requests: () => [] });
const before = mk();
assert.equal(before.state().jobs[0].state, 'error', 'no new program file: still an interruption');
await before.close();
writeFileSync(join(f.data, 'native-jobs.json'), JSON.stringify([old]));
writeFileSync(fileURLToPath(exe), 'new program');
try {
const after = mk();
assert.equal(after.state().jobs[0].state, 'done');
assert.match(after.state().jobs[0].summary, /updated/i);
await after.close();
} finally { rmSync(fileURLToPath(exe), { force: true }); }
} finally { await f.native.close(); f.cleanup(); }
});
test('packaged: "Update engine" works in the first seconds, before the native snapshot has connected', async () => {
const f = fixture();
try {
assert.equal(f.native.state().available, false, 'nothing has been read yet');
const started = f.native.operation('update-engine', {}); // used to throw "Connect the native Strata installation first."
const job = await finish(f.native, started.id);
assert.equal(job.state, 'done', job.error);
} finally { await f.native.close(); f.cleanup(); }
});
test('packaged: a chosen engine file goes to Control; only a file in the own incoming folder is deleted afterwards', async () => {
const f = fixture();
try {
const incoming = join(f.data, 'incoming-engine');
mkdirSync(incoming, { recursive: true });
const mine = join(incoming, 'strata-engine-x.tar.gz'), theirs = join(f.data, 'keep-me.tar.gz');
writeFileSync(mine, 'x'); writeFileSync(theirs, 'y');
const job = await finish(f.native, f.native.operation('install-engine-file', { path: mine }).id);
assert.equal(job.state, 'done', job.error);
assert.deepEqual(f.calls.at(-1), ['install-file', mine]);
assert.equal(existsSync(mine), false, 'the copy the page uploaded is removed');
const other = await finish(f.native, f.native.operation('install-engine-file', { path: theirs }).id);
assert.equal(other.state, 'done', other.error);
assert.equal(existsSync(theirs), true, 'a file anywhere else is never deleted');
const none = await finish(f.native, f.native.operation('install-engine-file', {}).id);
assert.equal(none.state, 'error');
assert.equal(f.calls.filter(c => c[0] === 'install-file').length, 2, 'no file, no install');
} finally { await f.native.close(); f.cleanup(); }
});
test('first-run card data: an offer with its size, nothing-fits with reasons, offline, and an engine already there', async () => {
const gpus = [{ name: 'NVIDIA GeForce RTX 5070', vramGb: 12 }];
const report = engine => ({ ok: true, hardware: { nvidia_driver: '581.15', backend: 'cuda' }, engine });
const offer = await engineFirstRun({ control: { updateCheck: async () => report({ status: 'install', installed: null, offered: { version: '0.1.48', variant: 'cuda13', backend: 'cuda' }, download_bytes: 581299508 }) }, gpus });
assert.deepEqual([offer.status, offer.offer.bytes, offer.driver, offer.gpus[0].name], ['offer', 581299508, '581.15', 'NVIDIA GeForce RTX 5070']);
const none = await engineFirstRun({ control: { updateCheck: async () => report({ status: 'none-for-this-pc', installed: null, offered: null, reasons: ['needs NVIDIA driver 580.65 or newer, this PC has 560.1'] }) }, gpus });
assert.equal(none.status, 'none');
assert.match(none.reasons[0], /driver 580/);
const installed = await engineFirstRun({ control: { updateCheck: async () => report({ status: 'current', installed: { version: '0.1.48' }, offered: null }) }, gpus });
assert.equal(installed.status, 'installed');
const offline = await engineFirstRun({ control: { updateCheck: async () => { throw new Error('the update feed could not be read: could not read https://github.com/x (URLError: [WinError 10060] timed out)'); } }, gpus });
assert.equal(offline.status, 'offline');
});
test('without the shell bridge "Update Strata" still updates Control and says nothing false about the app', async () => {
const f = fixture();
try {
+38
View File
@@ -0,0 +1,38 @@
/** The Speed label names the default the way the engine decides it: integrated / unified-memory GPU, or cards totalling 16 GiB or less. */
import test from 'node:test';
import assert from 'node:assert/strict';
import { build } from 'esbuild';
import { fileURLToPath } from 'node:url';
const out = await build({ entryPoints: [fileURLToPath(new URL('../src/lib/deepseek-speed.ts', import.meta.url))], bundle: true, format: 'esm', platform: 'node', write: false, logLevel: 'silent' });
const { fasterByDefault, speedOptions } = await import(`data:text/javascript;base64,${Buffer.from(out.outputFiles[0].text).toString('base64')}`);
const card = (vram_gb, extra = {}) => ({ index: 0, name: 'GPU', vram_gb, ...extra });
test('Strix Halo (an integrated / unified-memory GPU) defaults to Faster, whatever its memory figure says', () => {
assert.equal(fasterByDefault([card(96, { uma: true })]), true, 'a 96 GB share is not "16 GiB or less", the integrated rule applies');
assert.equal(fasterByDefault([card(0, { unified_gb: 128 })]), true);
assert.equal(fasterByDefault([card(64, { integrated: true })]), true);
assert.equal(fasterByDefault([card(24), card(96, { uma: true })]), true, 'an integrated card among the cards in use');
});
test('discrete cards: Faster at 16 GiB or less in total, Full quality above', () => {
assert.equal(fasterByDefault([card(11.9)]), true, 'RTX 5070 / 3060 12 GB');
assert.equal(fasterByDefault([card(15.99)]), true, 'a 16 GB card reports 16376 MiB');
assert.equal(fasterByDefault([card(16)]), true);
assert.equal(fasterByDefault([card(8), card(8)]), true, 'two 8 GB cards total 16');
assert.equal(fasterByDefault([card(16.05)]), false);
assert.equal(fasterByDefault([card(24)]), false);
assert.equal(fasterByDefault([card(8), card(12)]), false);
assert.equal(fasterByDefault([card(24, { usable: false }), card(12)]), true, 'a card the engine cannot use does not count');
assert.equal(fasterByDefault([]), false);
assert.equal(fasterByDefault(null), false);
});
test('the labels say "(default on this PC)" beside the choice that is the default, once', () => {
const faster = speedOptions([card(12)]).map(o => o.label);
assert.deepEqual(faster, ['Default (Faster on this PC)', 'Faster (default on this PC)', 'Full quality']);
const full = speedOptions([card(24)]).map(o => o.label);
assert.deepEqual(full, ['Default (Full quality on this PC)', 'Faster', 'Full quality (default on this PC)']);
const strix = speedOptions([card(96, { uma: true })]).map(o => o.label);
assert.deepEqual(strix, ['Default (Faster on this PC)', 'Faster (default on this PC)', 'Full quality']);
});
+2 -1
View File
@@ -3,6 +3,7 @@
* Setup writes --mtp, --spec-min-p and --vision into the arguments every time it runs; the host applies the saved choices on every
* load. Nothing here starts an engine; Control is a fake that records what the host saved and asked it to start.
*/
import './helpers/isolate-profile.mjs'; // a test run never writes the owner's real Strata profile
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
@@ -45,7 +46,7 @@ test('withoutDraftLayer takes out --mtp, --spec-min-p and --batch-mtp and keeps
test('"Draft layer (MTP)" is a setting of the app, not frozen, and "MTP per request" is not frozen either', async () => {
assert.ok(NATIVE_FIELDS.some(f => f.id === 'mtp' && f.type === 'boolean'));
assert.deepEqual(APP_OWNED_FIELDS.sort(), ['batchMtp', 'drafting', 'mtp']);
assert.deepEqual(APP_OWNED_FIELDS.sort(), ['batchMtp', 'drafting', 'ds_experts', 'mtp']);
const f = fixture();
try {
await f.native.refresh(true);
+7 -57
View File
@@ -1,61 +1,11 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { engineRefusal, engineTooOld, markEngine, parseEngineVersion } from '../server/engine-version.mjs';
import { nativeFixture } from './helpers/native-fixture.mjs';
import { shownEngineVersion } from '../src/lib/engine-version.ts';
test('engine versions compare as numbers, a missing part counting as zero', () => {
assert.deepEqual(parseEngineVersion('0.1.45.3'), [0, 1, 45, 3]);
assert.deepEqual(parseEngineVersion('v0.1.45'), [0, 1, 45]);
assert.equal(parseEngineVersion('dev'), null);
assert.equal(engineTooOld('0.1.45', '0.1.45.1'), true, 'the released 0.1.45 stops at v4 files: too old for Gyro');
assert.equal(engineTooOld('0.1.45.1', '0.1.45.1'), false);
assert.equal(engineTooOld('0.1.45.3', '0.1.45.1'), false);
assert.equal(engineTooOld('0.1.46', '0.1.45.1'), false);
assert.equal(engineTooOld('0.1.44.1', '0.1.45.1'), true);
assert.equal(engineTooOld('0.1.45', null), false, 'no minimum: never too old');
assert.equal(engineTooOld(null, '0.1.45.1'), false, 'an unreadable version is not called too old');
assert.equal(engineTooOld('0.1.40', '0.1.45.1', 'local'), false, 'an engine built from this checkout has it whatever its number says');
});
const catalog = () => ({ families: [{ family: 'qwen', sizes: [{ id: 'q2_0', model: 'Q2_0', engine_min: null }] }, { family: 'gyro', sizes: [{ id: 'gyro-gyro-s', model: 'Gyro-S', engine_min: '0.1.45.1' }, { id: 'gyro-gyro-m', model: 'Gyro-M', engine_min: '0.1.45.1' }] }] });
test('the catalog marks the sizes an older engine cannot read; the refusal says what to do', () => {
const marked = markEngine(catalog(), { version: '0.1.45' });
assert.deepEqual(marked.families.flatMap(f => f.sizes.map(s => [s.id, s.needsNewerEngine])), [['q2_0', false], ['gyro-gyro-s', true], ['gyro-gyro-m', true]]);
const text = engineRefusal(marked, 'gyro-gyro-s', { version: '0.1.45' });
assert.match(text, /^This model needs a newer engine: Gyro-S needs engine 0\.1\.45\.1 or newer, and this one is 0\.1\.45\. Update the engine in Settings, then try again\.$/);
assert.equal(engineRefusal(marked, 'q2_0', { version: '0.1.45' }), null);
const fresh = markEngine(catalog(), { version: '0.1.45.3' });
assert.ok(fresh.families.every(f => f.sizes.every(s => !s.needsNewerEngine)));
assert.equal(markEngine(null, {}), null);
});
test('with an older engine installed the app refuses to download or load Gyro, with the plain sentence; a new enough engine goes on', async () => {
const old = nativeFixture({ engineStatus: { version: '0.1.45', backend: 'cuda' } });
try {
old.catalog.families.push({ family: 'gyro', title: 'Qwen3.8-Flash-Next (Gyro)', about: '', images: false, sizes: [{ id: 'iq2_xs', model: 'Gyro-S', download_gb: 58, ram_needed_gb: 28, about: '', on_this_pc: 'fits', installed: true, engine_min: '0.1.45.1' }] });
old.catalog.families[0].sizes[0].engine_min = '0.1.45.1';
const state = await old.control.refresh(true);
const sizes = state.catalog.families.flatMap(f => f.sizes);
assert.ok(sizes.find(s => s.engine_min).needsNewerEngine);
const id = sizes.find(s => s.engine_min).id;
assert.throws(() => old.control.operation('load', { model: id }), /This model needs a newer engine: .* needs engine 0\.1\.45\.1 or newer, and this one is 0\.1\.45\./);
assert.throws(() => old.control.operation('install', { model: id }), /needs a newer engine/);
assert.equal(old.control.state().jobs.length, 0, 'nothing was started');
} finally { await old.cleanup(); }
const ok = nativeFixture({ engineStatus: { version: '0.1.45.3', backend: 'cuda' } });
try {
ok.catalog.families[0].sizes[0].engine_min = '0.1.45.1';
const state = await ok.control.refresh(true);
assert.ok(state.catalog.families[0].sizes.every(s => !s.needsNewerEngine));
} finally { await ok.cleanup(); }
});
test('the screens use the existing "needs a newer engine" notice for a size, and the buttons are held back', async () => {
const models = await readFile(new URL('../src/components/NativeModels.tsx', import.meta.url), 'utf8');
assert.match(models, /selectedFamily && \(!selectedSize \|\| selectedSize\.needsNewerEngine\) \? <><h2 className="h3">\{tr\("This model needs a newer engine\."\)\}/);
assert.match(models, /!!selectedSize\.needsNewerEngine\}/);
const guided = await readFile(new URL('../src/components/GuidedSetup.tsx', import.meta.url), 'utf8');
assert.equal((guided.match(/needsNewerEngine/g) ?? []).length, 3);
test('About never shows the host placeholder engine version while the real one is still being read', () => {
assert.equal(shownEngineVersion({ available: false }, 'strata-engine 0.1.0'), undefined);
assert.equal(shownEngineVersion(null, 'strata-engine 0.1.0'), undefined);
assert.equal(shownEngineVersion({ available: true, installed: { engine: { version: '0.1.48' } } }, 'strata-engine 0.1.0'), '0.1.48');
assert.equal(shownEngineVersion({ available: true, installed: { engine: null } }, 'strata-engine 0.1.0'), undefined);
assert.equal(shownEngineVersion({ available: false }, 'strata-engine 0.1.51'), 'strata-engine 0.1.51');
});
+5 -1
View File
@@ -170,5 +170,9 @@ test('Windows: a PowerShell that is missing or fails is reported as before', asy
f.child.emit('error', Object.assign(new Error('x'), { code: 'ENOENT' }));
await assert.rejects(missing, e => e.code === 'ENOENT');
const g = fakeShell(); const failed = watchedWindowsDialog('powershell.exe', [], { spawnProcess: g.spawnProcess });
g.child.emit('close', 1); await assert.rejects(failed, e => e.code === 1);
g.child.emit('close', 1);
// Ended by security software before any window opened (#3831): the app asks for the path instead of an error.
assert.deepEqual(await failed, { noDialog: true });
const h = fakeShell(); const late = watchedWindowsDialog('powershell.exe', [], { spawnProcess: h.spawnProcess });
h.child.stdout.emit('data', 'READY\n'); h.child.emit('close', 1); await assert.rejects(late, e => e.code === 1);
});
@@ -0,0 +1,19 @@
/**
* Import this FIRST in a test that builds the host's native control or starts the host: it points the per-user places the host writes to
* (the settings file Control reads, the components folder, the app's local data) at a scratch folder.
* On 2026-10-24 a host test run rewrote the owner's real %LOCALAPPDATA%\Strata\app-settings.json (its data_dir became a deleted test folder).
*/
import { mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
if (!process.env.STRATA_TEST_KEEP_PROFILE) {
const scratch = mkdtempSync(join(tmpdir(), 'strata-host-test-profile-'));
process.env.STRATA_SETTINGS_FILE = join(scratch, 'app-settings.json');
process.env.STRATA_COMPONENTS_DIR = join(scratch, 'components');
process.env.LOCALAPPDATA = join(scratch, 'Local');
process.env.APPDATA = join(scratch, 'Roaming');
process.env.XDG_DATA_HOME = join(scratch, 'xdg-data');
process.env.XDG_STATE_HOME = join(scratch, 'xdg-state');
process.env.XDG_CONFIG_HOME = join(scratch, 'xdg-config');
}
+65
View File
@@ -0,0 +1,65 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import ts from 'typescript';
import { readdir, readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { build } from 'esbuild';
const languages = ['en', 'zh', 'ja', 'de', 'es', 'ru', 'pt', 'ko'];
const read = async name => JSON.parse(await readFile(`src/locales/${name}.json`, 'utf8'));
const built = await build({ stdin: { contents: "export * from './src/lib/i18n';", resolveDir: process.cwd() }, bundle: true, write: false, format: 'esm', platform: 'node', loader: { '.json': 'json' } });
const { tr, setTranslationLanguage } = await import(`data:text/javascript;base64,${Buffer.from(built.outputFiles[0].text).toString('base64')}`);
test('every tr("...") literal and every command hint has a key in the English catalog, so no language is stuck in English (#3479)', async () => {
const known = new Set();
for (const name of ['en', 'tune-en', 'tools-en', 'fix-en']) for (const v of Object.values(await read(name))) known.add(String(v));
const missing = [];
const scan = async dir => {
for (const entry of await readdir(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) { if (entry.name !== 'locales') await scan(path); continue; }
if (!/\.tsx?$/.test(path)) continue;
const source = await readFile(path, 'utf8'), sf = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true, path.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
const visit = node => {
if (ts.isCallExpression(node) && node.expression.getText(sf) === 'tr' && node.arguments[0] && (ts.isStringLiteral(node.arguments[0]) || ts.isNoSubstitutionTemplateLiteral(node.arguments[0]))) {
const text = node.arguments[0].text.trim();
if (text && /[a-zA-Z]{2}/.test(text) && !known.has(text)) missing.push(`${path}: ${text}`);
}
if (ts.isPropertyAssignment(node) && node.name.getText(sf) === 'hint' && ts.isStringLiteral(node.initializer) && !path.endsWith('chat-showcase.ts')) {
const text = node.initializer.text.trim();
if (text && !known.has(text)) missing.push(`${path}: hint ${text}`);
}
ts.forEachChild(node, visit);
};
visit(sf);
}
};
await scan('src');
assert.deepEqual(missing, []);
});
test('the fix catalogs have the same keys and placeholders in all eight languages', async () => {
const en = await read('fix-en');
for (const language of languages) {
const catalog = await read(`fix-${language}`);
assert.deepEqual(Object.keys(catalog), Object.keys(en), language);
for (const [id, text] of Object.entries(catalog)) assert.deepEqual((text.match(/\{\d+\}/g) ?? []).sort(), (en[id].match(/\{\d+\}/g) ?? []).sort(), `${language} ${id}`);
}
});
test('the /plan, /research and /new hints and the four "Open ..." hints are translated (#3479, #3825)', () => {
for (const language of ['zh', 'ru']) {
setTranslationLanguage(language);
for (const hint of ['Have the assistant write a plan first; nothing runs until you approve it', 'Search the web, read the pages and answer with sources', 'Start a new conversation; keep this one'])
assert.notEqual(tr(hint), hint, `${language}: ${hint}`);
for (const label of ['Model settings', 'Serving', 'Models', 'Skills']) {
const hint = tr(`Open ${label}`);
assert.notEqual(hint, `Open ${label}`, `${language}: ${label} template`);
assert.ok(!hint.includes(label), `${language}: the label is translated too: ${hint}`);
}
}
setTranslationLanguage('zh');
assert.equal(tr('Open Skills'), '打开 技能');
setTranslationLanguage('en');
assert.equal(tr('Open Skills'), 'Open Skills');
});
@@ -0,0 +1,34 @@
// #3818: a single newline inside a paragraph is a space; a blank line starts a new paragraph; two trailing spaces or a backslash is a line break.
import assert from 'node:assert/strict';
import test from 'node:test';
import { resolve } from 'node:path';
import { createRequire } from 'node:module';
import { build } from 'esbuild';
const root = resolve(import.meta.dirname, '..');
const bundle = await build({
stdin: { contents: `import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { Markdown } from './src/components/Markdown.tsx'; export const html = text => renderToStaticMarkup(React.createElement(Markdown, { text }));`, resolveDir: root, loader: 'ts' },
bundle: true, write: false, platform: 'node', format: 'cjs', jsx: 'automatic', external: ['katex', 'katex/dist/katex.min.css'],
plugins: [{ name: 'stub', setup(b) {
b.onResolve({ filter: /^\.\/ui$/ }, () => ({ path: 'ui', namespace: 'test-stub' }));
b.onLoad({ filter: /.*/, namespace: 'test-stub' }, () => ({ contents: 'export const IconBtn = () => null; export const Btn = () => null;' }));
} }],
});
const mod = { exports: {} };
new Function('require', 'module', 'exports', bundle.outputFiles[0].text)(createRequire(import.meta.url), mod, mod.exports);
const { html } = mod.exports;
const paragraphs = out => (out.match(/<p>/g) ?? []).length;
test('a single newline is a soft break, a blank line a new paragraph', () => {
const soft = html('Line one\nLine two');
assert.equal(paragraphs(soft), 1); assert.match(soft, /Line one Line two/);
assert.equal(paragraphs(html('One\n\nTwo')), 2);
});
test('two trailing spaces or a backslash keep a line break', () => {
assert.match(html('One \nTwo'), /One<br\/>Two/);
assert.match(html('One' + String.fromCharCode(92) + String.fromCharCode(10) + 'Two'), /One<br\/>Two/);
});
test('a list, heading or code block right after a line still starts its own block', () => {
const out = html('Intro\n- a\n- b\n## Head\ntext\n```\ncode\n```');
assert.match(out, /<p>Intro<\/p><ul>/); assert.match(out, /<h2>Head<\/h2><p>text<\/p>/);
});
+8 -1
View File
@@ -4,7 +4,7 @@ import { readFile } from 'node:fs/promises';
import { transform } from 'esbuild';
const { code } = await transform(await readFile(new URL('../src/lib/model-picker.ts', import.meta.url), 'utf8'), { loader: 'ts', format: 'esm' });
const { pickerGroups } = await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`);
const { pickerGroups, pickerNativeModels } = await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`);
const version = (id, family, state, diskGb, extra = {}) => ({ id, family, state, diskGb, ...extra });
test('chat picker groups every downloaded family and puts its running version first', () => {
@@ -35,3 +35,10 @@ test('downloaded files stay visible during load transitions and recoverable fail
assert.deepEqual(pickerGroups(library)[0].versions.map(v => v.id), ['loading', 'unloading', 'failed-load']);
assert.deepEqual(pickerGroups({ loaded: null, variants: [library.variants[0]] }), []);
});
test('downloaded optimized models that are not loaded still appear in the chat picker (#3814)', () => {
const installed = [{ model: 'swift', model_name: 'Swift IQ3_XXS', ready: true }, { model: 'half', model_name: 'Half', ready: false }, { model: 'run', model_name: 'Running one', ready: true }];
const out = pickerNativeModels(installed, [{ label: 'Running one' }]);
assert.deepEqual(out.map(m => m.model), ['swift']);
assert.deepEqual(pickerNativeModels(undefined, null), []);
});
+1
View File
@@ -1,3 +1,4 @@
import './helpers/isolate-profile.mjs'; // a test run never writes the owner's real Strata profile
import test from 'node:test';
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
+26 -1
View File
@@ -4,7 +4,7 @@ import { parseCpuTopology, parseNvidiaReadings, parsePcieThroughput, gpuMemoryTo
import { cpuUtilization, diskRate, parseLinuxDiskCounters, parseMacDiskCounters } from '../server/system-telemetry.mjs';
import { parseEngineMetrics } from '../server/supervisor.mjs';
import { originalDashboard } from '../server/monitor-dashboard.mjs';
import { gpuMetric, gpuCapacity } from '../src/lib/monitor-hardware.ts';
import { gpuMetric, gpuCapacity, gpuReported, shortGpuName, defaultGpuSet, gpuId } from '../src/lib/monitor-hardware.ts';
import { referencePaths } from '../src/lib/monitor-chart.ts';
const gpu=(id,index=0,unified=false)=>({id,index,unified,vramGb:24,name:id,vendor:'nvidia',backend:'cuda'});
@@ -92,3 +92,28 @@ test('original requests, cache, progress, hardware and MCP facts are translated
assert.equal(mapped.generation.total,600);assert.equal(mapped.history[0].gpuTempC,null);
assert.equal(mapped.mcp.servers[0].state,'ready');assert.equal(mapped.mcp.servers[0].tools[0].name,'read_file');
});
test('All GPUs: a card with no temperature sensor is left out and the card that reports is named (#3727 follow-up)',()=>{
const devices=[{...gpu('nv',0),name:'NVIDIA GeForce RTX 5070'},{...gpu('igpu',1),name:'AMD Radeon(TM) Graphics',vramGb:0.4}];
const readings=[reading('nv',3,0.8,34,7),reading('igpu',0,0.001,null,null)];
const temp=gpuReported(devices,readings,'temperatureC','max');
assert.equal(temp.value,34); assert.equal(temp.partial,true); assert.deepEqual(temp.devices.map(d=>shortGpuName(d.name)),['RTX 5070']);
const load=gpuReported(devices,readings,'utilizationPct','mean');
assert.equal(load.partial,false); assert.equal(load.value,1.5);
assert.equal(gpuReported(devices,[reading('nv',3,0.8,null,null),reading('igpu',0,0,null,null)],'temperatureC','max').value,null,'no card reports: still unavailable');
assert.equal(shortGpuName('AMD Radeon RX 9070 XT'),'RX 9070 XT');
assert.equal(shortGpuName('Intel(R) Arc(TM) A750 Graphics'),'A750 Graphics');
});
test('an RTX 5070 beside the unsupported Radeon iGPU: All GPUs is the RTX alone, the iGPU stays pickable',()=>{
const rtx={id:'GPU-5070',index:0,vendor:'nvidia',name:'NVIDIA GeForce RTX 5070',vramGb:11.9,unified:false,backend:'cuda'};
const igpu={id:'PCI\VEN_1002&DEV_164E',index:1,vendor:'amd',name:'AMD Radeon(TM) Graphics',vramGb:0.5,unified:false,backend:'vulkan'};
const set=defaultGpuSet([rtx,igpu]);
assert.deepEqual(set.map(g=>g.id),['GPU-5070']);
assert.equal(gpuCapacity(set),11.9);
// the native engine report numbers each make from 0, so both cards say index 0: the ids stay different
assert.notEqual(gpuId({...rtx,id:undefined}),gpuId({...igpu,id:undefined,index:0}));
// alone, or a real AMD card beside it, nothing is dropped
assert.deepEqual(defaultGpuSet([igpu]).map(g=>g.id),[igpu.id]);
assert.equal(defaultGpuSet([{...igpu,vramGb:96,unified:true},rtx]).length,2);
});
+13 -1
View File
@@ -5,7 +5,7 @@ import { readFile } from 'node:fs/promises';
import { transform } from 'esbuild';
const { code } = await transform(await readFile(new URL('../src/lib/native-fit.ts', import.meta.url), 'utf8'), { loader: 'ts', format: 'esm' });
const { fitOn, memoryNeeded, RECOMMENDED, VERSION_GROUPS, groupOpen, recommendedVersions, PICK_NOTE, OTHER_NOTE, MTP_VRAM_GB } = await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`);
const { fitOn, cacheGb, memoryNeeded, RECOMMENDED, VERSION_GROUPS, groupOpen, recommendedVersions, PICK_NOTE, OTHER_NOTE, MTP_VRAM_GB } = await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`);
const swift = { ram_needed_gb: 48, experts_gb: 35.5 };
const gyroS = { ram_needed_gb: 27.6, experts_gb: 25.8, mem_total: true };
@@ -83,3 +83,15 @@ test('the tiers of the engine are 40 GB Swift IQ2_XS, 48 GB IQ3_XXS, 64 GB IQ3_S
assert.deepEqual([...m[1].matchAll(/\(([\d.]+),\s*"(\w+)",\s*"(\w+)"\)/g)].map(x => [Number(x[1]), x[2], x[3]]), [[62, 'qwen', 'IQ3_S'], [46, 'qwen', 'IQ3_XXS'], [38, 'swift', 'IQ2_XS']], file);
}
});
test('the conversation context and cache format move the estimate (#3832)', () => {
const pc = { ram_gb: 64, vram_gb: 16 };
const base = fitOn(swift, pc);
assert.deepEqual(fitOn(swift, pc, true, { context: 32768, kv: 'int8' }), base, 'the default context is what the fixed figure already held');
const big = fitOn(swift, pc, true, { context: 524288, kv: 'int8' });
assert.ok(big.vramUseGb >= base.vramUseGb && big.ramUseGb > base.ramUseGb, 'a bigger cache takes graphics memory from the experts, which then sit in RAM');
assert.ok(fitOn(swift, pc, true, { context: 524288, kv: 'q4_0' }).ramUseGb < big.ramUseGb, 'a smaller cache format leaves more room');
assert.ok(Math.abs(cacheGb(524288, 'int8') * 1024 - 7361) < 25, 'the 512K Int8 cache is the 7361 MiB the engine reports');
const tight = fitOn(q4, pc, true, { context: 524288, kv: 'int8' });
assert.ok(tight.spillGb > fitOn(q4, pc).spillGb, 'what no longer fits is read from the SSD and the card says so');
});
+21
View File
@@ -0,0 +1,21 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { build } from 'esbuild';
const compiled = await build({ stdin: { contents: "export * from './src/lib/native';", resolveDir: process.cwd() }, bundle: true, write: false, platform: 'node', format: 'esm' });
const lib = await import(`data:text/javascript;base64,${Buffer.from(compiled.outputFiles[0].text).toString('base64')}`);
test('the Models page waits for the first native answer before it picks a model (#3740)', async () => {
let release;
globalThis.fetch = () => new Promise(resolve => { release = () => resolve({ ok: true, status: 200, statusText: 'OK', text: async () => JSON.stringify({ available: true, jobs: [], instances: [] }) }); });
let heard = 0;
const off = lib.subscribeNative(() => { heard++; });
assert.equal(lib.nativeSettled(), false, 'nothing read yet: show a placeholder, not the Starter');
const reading = lib.refreshNative();
assert.equal(lib.nativeSettled(), false);
release();
await reading;
assert.equal(lib.nativeSettled(), true);
assert.ok(heard >= 1, 'the page is told');
off();
});
+1
View File
@@ -1,3 +1,4 @@
import './helpers/isolate-profile.mjs'; // a test run never writes the owner's real Strata profile
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
+56 -2
View File
@@ -1,3 +1,4 @@
import './helpers/isolate-profile.mjs'; // a test run never writes the owner's real Strata profile
import test from 'node:test';
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
@@ -85,13 +86,13 @@ test('runCli passes the arguments untouched (no shell) and reads JSON', async ()
// ---------------------------------------------------------------------------------- native-control in packaged mode
function packagedFixture({ engineUsable = true, updater = null, repair = false } = {}) {
function packagedFixture({ engineUsable = true, updater = null, repair = false, ids = ['iq2_xs', 'iq3_s'] } = {}) {
const root = mkdtempSync(join(tmpdir(), 'strata-packaged-test-'));
const install = join(root, 'install'), data = join(root, 'data');
mkdirSync(install, { recursive: true });
const earlierInstallEnv = process.env.STRATA_INSTALL_DIR;
process.env.STRATA_INSTALL_DIR = install; // the app's own install folder (the host always tells Control which one; it never falls back to an older CLI folder)
const configs = ['iq2_xs', 'iq3_s'].map(id => {
const configs = ids.map(id => {
const path = join(install, `strata-${id}.json`);
writeFileSync(path, JSON.stringify({ exe: 'component:strata-engine', tokenizer: join(install, 'tok'), args: ['--max-context', '32768'], model_name: `Qwen ${id}`, port: 8080 }));
return { id, path };
@@ -201,6 +202,59 @@ test('packaged: load = saved profile + runtime start under Control; unload = run
} finally { await f.native.close(); f.cleanup(); }
});
test('packaged: a model id with dots (DeepSeek) is listed, loaded from its run config, and unloaded', async () => {
const id = 'deepseek-v4.1-flash-ist-das-2.00bpw';
const f = packagedFixture({ ids: [id] });
try {
writeFileSync(join(f.install, `strata-${id}.json`), JSON.stringify({ exe: join(f.install, 'engine-dsv41', 'strata-dsv41'), arch: 'deepseek41', tokenizer: join(f.install, 'tok'), args: ['--pack', join(f.install, 'pack'), '--max-context', '8192', '--eos', '1'], model_name: 'deepseek-v4.1-flash', port: 8080, engine_ready_s: 3600 }));
const state = await f.native.refresh(true);
assert.deepEqual(state.installed.models.map(m => m.model), [id], 'listed (the install finished but the app used to show nothing)');
assert.equal(state.installed.models[0].ready, true);
const job = await finish(f.native, f.native.operation('load', { model: id }).id);
assert.equal(job.state, 'done', job.error); // used to end with "The model configuration is outside the Strata installation."
assert.deepEqual(f.calls.filter(c => ['saveProfile', 'start'].includes(c[0])).map(c => c.slice(0, 2)), [['saveProfile', 'app-1'], ['start', 'app-1']]);
const flags = f.saved[0].args.filter(a => a.startsWith('--'));
assert.deepEqual(flags, ['--pack', '--max-context', '--eos'], 'The own engine of DeepSeek gets none of the Flash-Next options (it ended with "unknown option --batch")');
assert.equal(f.calls.find(c => c[0] === 'start')[2], 600, 'it waits longer than the usual 300 s, up to the 600 s Control allows');
const instance = f.native.state().instances[0];
assert.equal(instance.model, id);
assert.equal(instance.status, 'ready');
await f.native.unload(instance.id);
assert.deepEqual(f.calls.filter(c => c[0] === 'stop').at(-1), ['stop', 'app-1']);
assert.equal(f.native.state().instances.length, 0);
for (const odd of ['a..b', '../x', 'x/y', '.hidden']) assert.equal((await finish(f.native, f.native.operation('load', { model: odd }).id)).state, 'error', odd);
} finally { await f.native.close(); f.cleanup(); }
});
test('packaged: the Speed choice of DeepSeek passes --experts 5 or 6 only when chosen and only for an engine that knows it (0.1.50 or newer)', async () => {
const id = 'deepseek-v4.1-flash-ist-das-2.00bpw';
const run = async (values, extraArgs = [], engine = '0.1.50-windows-x86_64-cuda-cuda13') => {
const f = packagedFixture({ ids: [id] });
try {
writeFileSync(join(f.install, `strata-${id}.json`), JSON.stringify({ exe: join(f.install, 'engine-dsv41', engine, 'strata-dsv41'), arch: 'deepseek41', tokenizer: join(f.install, 'tok'), args: ['--pack', join(f.install, 'pack'), '--max-context', '8192', ...extraArgs], model_name: 'deepseek-v4.1-flash', port: 8080 }));
await f.native.refresh(true);
const capability = f.native.settings(id).capabilities.dsExperts;
const job = await finish(f.native, f.native.operation('load', { model: id, ...(values ? { values } : {}) }).id);
assert.equal(job.state, 'done', job.error);
return { cfg: f.saved[0], capability };
} finally { await f.native.close(); f.cleanup(); }
};
const flag = cfg => { const i = cfg.args.indexOf('--experts'); return i < 0 ? null : cfg.args[i + 1]; };
const none = await run(null);
assert.equal(flag(none.cfg), null, 'nothing chosen: the engine decides (--experts auto)');
assert.equal(none.capability, true);
assert.equal(flag((await run({ ds_experts: '5' })).cfg), '5');
assert.equal(flag((await run({ ds_experts: '6' }, ['--experts', '5'])).cfg), '6', 'the choice replaces an older flag');
assert.equal(flag((await run({ ds_experts: null }, ['--experts', '5'])).cfg), null, 'Default clears an older flag');
assert.equal('ds_experts' in (await run({ ds_experts: '5' })).cfg, false, 'the app-only key is not handed to the engine');
assert.equal(flag((await run({ ds_experts: '5' }, [], '0.1.60-windows-x86_64-cuda-cuda13')).cfg), '5', 'a newer engine knows it');
for (const old of ['0.1.49.2-windows-x86_64-cuda-cuda13', '0.1.49-linux-x86_64-cuda-cuda13']) {
const r = await run({ ds_experts: '5' }, [], old);
assert.equal(r.capability, false, `${old}: the choice is hidden`);
assert.equal(flag(r.cfg), null, `${old}: nothing is passed (it would end the start with "unknown option --experts")`);
}
});
test('packaged: no engine for this PC is the notice, and nothing is started', async () => {
const f = packagedFixture({ engineUsable: false });
try {
@@ -3,6 +3,7 @@
* retry with one when a load with several slots fails. Nothing here starts an engine; Control is a fake that records what
* the host saved and asked it to start.
*/
import './helpers/isolate-profile.mjs'; // a test run never writes the owner's real Strata profile
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
+5
View File
@@ -102,3 +102,8 @@ test('missing model files are named, in every language, with a next step (#3478)
setTranslationLanguage('en');
assert.equal(plainFailure(missingFilesError('m', []).message).kind, 'plain', 'with no names the old sentence is left as it was');
});
test('the engine component is named in words, once (#3900)', async () => {
const { missingNames } = await import('../server/missing-files.mjs');
assert.deepEqual(missingNames([{ part: 'engine', file: 'component:strata-engine' }], ['component:strata-engine']), ['the engine program']);
});
+1 -1
View File
@@ -145,7 +145,7 @@ test('4d: picture reading is on unless the person turned it off (no saved choice
} finally { await f.cleanup(); }
const screen = readSource(new URL('../src/components/NativeModels.tsx', import.meta.url), 'utf8');
assert.match(screen, /setVision\(installed\.visionEnabled === false \? 'no'/);
assert.match(screen, /else\s+setVision\('yes'\)/, 'a version that is not downloaded starts with Read pictures on');
assert.match(screen, /else\s*\{?\s*setVision\('yes'\)/, 'a version that is not downloaded starts with Read pictures on');
});
test('no recognised engine: the app still downloads, warns in the card, and loading says it with the next step', async () => {
+1 -1
View File
@@ -57,5 +57,5 @@ test('Cargo.toml, tauri.conf.json and the desktop package agree on the app versi
const cargo = /^version\s*=\s*"([^"]+)"/m.exec(await readFile(new URL('src-tauri/Cargo.toml', root), 'utf8'))[1];
const pkg = JSON.parse(await readFile(new URL('package.json', root), 'utf8')).version;
assert.equal(cargo, pkg);
assert.equal(cargo, '0.2.3');
assert.equal(cargo, '0.2.6');
});
@@ -0,0 +1,17 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { transform } from 'esbuild';
const { code } = await transform(await readFile(new URL('../src/lib/session-workspace.ts', import.meta.url), 'utf8'), { loader: 'ts', format: 'esm' });
const { workspaceIdForSession } = await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`);
const workspaces = [{ id: 'a', name: 'A', path: 'C:\A', aliases: ['C:\old'] }, { id: 'b', name: 'B', path: 'D:\Project' }];
test('a chat finds the workspace it was made in, by path or by an earlier folder (#3754)', () => {
assert.equal(workspaceIdForSession(workspaces, { workspace: 'D:\Project' }), 'b');
assert.equal(workspaceIdForSession(workspaces, { workspace: 'C:\old' }), 'a');
});
test('a chat with an unknown or no workspace changes nothing', () => {
assert.equal(workspaceIdForSession(workspaces, { workspace: 'E:\gone' }), null);
assert.equal(workspaceIdForSession(workspaces, undefined), null);
});
+17
View File
@@ -0,0 +1,17 @@
/** The first-run card and the update check show decimal megabytes, the same figure as the Terminal and the release page. */
import test from 'node:test';
import assert from 'node:assert/strict';
import { build } from 'esbuild';
import { fileURLToPath } from 'node:url';
const out = await build({ entryPoints: [fileURLToPath(new URL('../src/lib/format.ts', import.meta.url))], bundle: true, format: 'esm', platform: 'node', write: false, logLevel: 'silent' });
const { megabytes } = await import(`data:text/javascript;base64,${Buffer.from(out.outputFiles[0].text).toString('base64')}`);
test('engine sizes are decimal megabytes: 606,513,890 bytes is 607 MB, not 578', () => {
assert.equal(megabytes(606513890), 607);
assert.equal(megabytes(581299508), 581);
assert.equal(megabytes(1), 1);
assert.equal(megabytes(0), 1);
// the same samples as tests/terminal/test_size_label.py: the Terminal and the app agree
for (const [bytes, expected] of [[606513890, 607], [581299508, 581], [1, 1], [0, 1], [999_999, 1], [1_500_000, 2], [2_499_999, 2]]) assert.equal(megabytes(bytes), expected, String(bytes));
});
Loaded 100 of 408 files, more files were not shown because too many files have changed in this diff. Show more