Compare commits

...
310 Commits
Author SHA1 Message Date
Niko1221 d2f0964758 Merge main (9522b5249: f-0213-mmr, make_model_release.py case B) into the 0.2.13 release candidate 2026-11-08 23:44:23 +01:00
Niko1221 9522b52499 Merge f-0213-mmr (e8f2380f3): make_model_release.py case B plans a new-shape Qwen4 engine as its own component (strata-q4-<shape> packages, build-feed --shapes, no Strata engine/Control/app rebuild, entry family qwen4exp_<shape>), with the Mac shape engine in case B (owner signs); full tools/check on Windows and Linux with STRATA_TEST_LLAMA_CPP set: no new failures (p2006's two known base failures; the known flaky test_tuiqa_wizard) 2026-11-08 23:44:15 +01:00
Niko1221 712fec6b42 Version 0.2.13 / engine 0.1.59: Terminal and app 0.2.13, Control and every engine package 0.1.59 (the 0.2.12 bump list, plus the v0.1.59 line in the setup.py version history and docs/data/release.json for v0.2.13) 2026-11-08 23:22:26 +01:00
Niko1221 e8f2380f36 make_model_release: the Mac engine of the shape is part of case B (the owner signs it); no Mac skip 2026-11-08 22:20:42 +01:00
Niko1221 93e0abb845 make_model_release: case B plans the engine of the new shape as its own component (--shape, strata-q4-<shape> packages, build-feed --shapes), the Strata engine, Controls and app are not rebuilt; the entry's family is qwen4exp_<shape>
For cherry-picking onto main after f-0213-p8merge and f-0213-shape (it edits the case-B plan that f-0213-p8merge rewrote).
2026-11-08 22:20:42 +01:00
Niko1221 d5d5b08379 Merge f-0213-shape (61d883315): a new-shape Qwen4 engine ships as its own component strata-q4-<shape> beside the Strata engine, picked by the model (owner 11-08: separate components; wording approved 'A ok, 1 yes, 2 later, 3 yes') - feed member shape_engines, setup/launch/update/Mac paths, strata --geometry checked against the signed manifest at install, older Controls never offer the model; l620 e2e (Flash-Next engine hash unchanged), Windows long-path install, Mac m1002 run 2026-11-08 22:20:12 +01:00
Niko1221 b82a368102 Merge f-0213-p8merge (b5f5e38a4): the Qwen4 rehearsal's fixes - F1-F5 (make_model_release schema, mmproj KeyError, drafter: none honoured, neutral expert profile for another shape with the owner's line, budget for feed qwen4exp sizes), STRATA_CMAKE_EXTRA in the Linux packaging scripts, G14 plan line, G12 release plan, G10/G3 docs, and G2: a real-file feed-model install gate in tools/check (tools/feed_model_install_gate.py; fails if any of F2-F5 is reverted); version bump left for the release candidate 2026-11-08 22:20:06 +01:00
Niko1221 b7bcbcde99 Merge f-0213-fixux (82d3a0345): user screenshots - a data folder on an external drive under /run/media is no longer refused as a system folder (real system folders still are; the doubled sentence is gone); one fit label everywhere (owner 11-08): 'fits' when the model files without the n-gram/PLE/Engram table fit graphics memory plus RAM, else 'may run slower' (the Unsloth sets were measured against RAM only; DeepSeek Needs shows the whole size; no-card PCs and small Macs say 'may run slower'; NVFP4-QAD sized by its 76.8 GB pack) 2026-11-08 22:14:42 +01:00
Niko1221 e3bd5cfdef Merge f-0213-defaults (a04860fad): DFlash2 per-device defaults - Strix Halo and Blackwell cc 12.0 cards with >= 48 GB draft window 6 -> 8 (all sets), conf 0.55 for IQ3_S only on that Blackwell class; end to end through the Terminal's flags, 3 interleaved rounds: Strix UD-Q4_K_XL x1.052 (output differs, deterministic; owner 11-08: ship as is), Strix UD-IQ4_XS-v31 x1.020 (identical), l620 IQ3_S x1.028 (identical); every other card unchanged 2026-11-08 22:09:21 +01:00
Niko1221 61d8833155 tests: the geometry tests that only run where a script is a program (Linux, Mac) no longer clash on a folder and test the refusal line through the start test 2026-11-08 21:31:40 +01:00
Niko1221 9f1550c3cf shape engine: a refused install says the approved line as it is, and the newer-engine line carries only the plain reason (found by the geometry mismatch run) 2026-11-08 21:27:06 +01:00
Niko1221 46ea3947e6 Merge f-0213-g16 (e1da007f8): Strix Halo Terminal launch with no VRAM reserve never started (pictures off or no picture reader) - the engine no longer puts its Windows 2560 MiB reserve floor on an integrated HIP device (d362bc5f0), and setup writes --vram-reserve-mib 700 on a single-card Windows gfx1151 config with no other reserve (e1da007f8); Strix: stale config starts in 77 s, 3 chats 20.5/24.3/23.9 tok/s; CUDA sm_120 binary bit-identical; snapshot changes only the 3 Strix cells 2026-11-08 21:15:45 +01:00
Niko1221 e1da007f82 Setup: write --vram-reserve-mib 700 on a single-card Windows Strix Halo config with no other reserve (G16)
A Strix config with pictures off, or for a model with no picture reader, had no --vram-reserve-mib, and the 0.2.12 engine then applied its Windows floor of
2560 MiB, which on the shared pool only moves memory from the expert cache into the RAM budget's page-locked complement: strata launch of UD-Q4_K_XL never
started (Q4-P8 step A15). Pictures on already write 700 (VISION table), and the app writes its own reserve, so this is one rule for every size: the engine's own
default, written down, on a Windows gfx1151 (uma) config that has no other --vram-reserve-mib. Existing saved configs keep what they have until setup runs again;
the engine commit before this one covers them after `strata update`.

Snapshot: three strix_halo_win cells gain --vram-reserve-mib 700 (setup entry, config and argv): ud_q4_k_xl, ud_iq4_xs_v31, iq3_s. Every other class and set is
byte-identical. tests/snapshot/test_strix_reserve.py pins all three sizes.
2026-11-08 21:14:00 +01:00
Niko1221 d362bc5f00 Engine: no Windows 2560 MiB reserve floor on an integrated (UMA) HIP device (G16)
On Windows HIP, an auto expert cache with no --vram-reserve-mib kept a floor of 2560 MiB free on any card of 15 GiB or more (#1376, made for a 20 GB discrete
card that paged). On a Strix Halo that room is not free: the RAM budget's page-locked complement takes it straight back (cache + complement 71.72 GiB at 2560 and
71.73 GiB at 700 on UD-Q4_K_XL), so the prompt path's own buffers found 0 MiB and the engine stepped its chunk down 16384 ... 1024 for ever; strata launch of a
UD-Q4_K_XL config with no reserve (pictures off, or a model with no picture reader) never started.

The floor now goes through strata::core::wddm_auto_reserve_mib(integrated, total, current, floor): a discrete card of 15 GiB or more still gets 2560, an
integrated GPU (the device property the UMA sizing already reads) keeps the engine's default 700. Discrete cards, Linux and every config that passes
--vram-reserve-mib are unchanged (the block is under _WIN32 + STRATA_USE_HIP and behind !vram_reserve_given). Unit test in uma_cache_test.

CUDA sm_120 sm build (vendored ggml), base vs change from the same path: the binaries are bit-identical (sha256 2b603395...).
Strix (gfx1151, public-0.2.12-style config with no reserve) with the new engine: starts in 77 s and answers 3 chats; the old engine never started it.
2026-11-08 21:13:59 +01:00
Niko1221 90ca808a20 UPDATES.md: the install-time strata --geometry check and the header label 2026-11-08 21:11:22 +01:00
Niko1221 63de72ce17 Mac: a feed model of another shape than the shipped expert profile gets the neutral profile on the Mac path too (the engine refused the shipped one at load) 2026-11-08 21:09:56 +01:00
Niko1221 ab56b84831 Mac: a model of its own shape installs on a Mac too: setup's Mac path fetches the shape engine (shape_engine_fetch, shared with the other platforms), the Mac rows are registered, a one-file model uses its own PLE table and a model whose entry says drafter none gets no draft layer (the approved line) 2026-11-08 20:59:37 +01:00
Niko1221 8920bd493b shape engine: the owner's approved lines (11-08), the update label Model engine, a test that pins them; at install Control checks strata --geometry against the signed manifest's shape and refuses a mismatch (an engine from before the flag is accepted) 2026-11-08 20:56:46 +01:00
Niko1221 febc0226f5 strata --geometry: prints the name of the model shape the kernels are compiled for (the geometry header's kGeometryName; flash_next for the default build). One host line beside --families; no compile flag, no kernel changes. 2026-11-08 20:56:46 +01:00
Niko1221 82d3a03458 NVFP4-QAD is sized by the pack that runs (76.8 GB), not by its 102.2 GB checkpoint minus the table 2026-11-08 20:52:43 +01:00
Niko1221 f16e297e5e Fit label: size is the download minus the lookup table's file; DeepSeek shows its whole size; no card or a small Mac says "may run slower"
Owner's answers of 11-08 evening:
- size = the model's download minus the n-gram / PLE / Engram table's file (28.8 GB for most sets, 54.4 GB for UD-Q5_K_XL / UD-Q6_K_XL / Q8_0, 51.2 GB for NVFP4, the stated
  memory figure for Gyro, the plan's Engram file for DeepSeek), against VRAM + RAM. UD-IQ4_XS and UD-IQ4_XS-v31 are 64.9 GB.
- the Terminal's "Needs" column and the app's version line show that same figure (DeepSeek: the whole size, not the CPU's share).
- no graphics card, or a Mac with too little memory: "may run slower" instead of "too big" / "Doesn't fit"; the app's "Doesn't fit" state and its "Uses SSD N GB" figure are gone.
  DeepSeek's "tight: needs more disk space" and the feed's "not available on this engine" rows are unchanged.
- docs/UNSLOTH_Q4.md names the rule instead of a 48 GB minimum.
2026-11-08 20:52:43 +01:00
Niko1221 a16f3fe39e One fit label for every model: "fits" or "may run slower" (a V100 32 GB with 31 GB of RAM no longer says "too big" for the Unsloth sets)
On a Tesla V100-PCIE-32GB with 31 GB of RAM every Unsloth set said "too big (can still try)" while IQ3_S, with the same 50 GB of experts, said "fits, a bit slower". The
catalog compared an Unsloth set's 48 GB RAM floor with the RAM alone and never counted the card. Not a hardware limit: UD-IQ4_XS is measured on that card.

Owner's rule (11-08): the model's weight files WITHOUT the n-gram / PLE lookup table (the "Needs (RAM + VRAM)" figure) against the graphics memory plus the RAM, nothing
taken off either side: "fits", else "may run slower". strata_control/fit_label.py holds it; the Terminal model menu, `setup.py --check`, the catalog's `on_this_pc` (tools/strata_mcp.py),
the install plan's RAM line, setup's size menus and the app's version list use it. Replaced: "fits, a bit slower", "too big (can still try)", "does not fit", the SSD-GB suffix,
the RAM-floor menu line ("<- needs 48 GB RAM, you have 31") and the install's RAM-floor warning where the set fits. Kept: "(recommended)" / "(installed)", DeepSeek's
"tight: needs more disk space" and "needs one or more graphics cards", a Mac's "too little memory", the feed's "not available on this engine", NVFP4's AMD notes in --check.
2026-11-08 20:52:43 +01:00
Niko1221 a1b9cc89ac A data folder on an external drive is no longer refused as a system folder (Linux)
/run/media/<user>/<drive> (udisks) sat inside /run, which was refused whole, so the AppImage, the Terminal and the AI-tool installer all said "is a system folder" for a
drive the person plugged in. A path strictly inside /run/media is not a system folder now (strata_control/folders.py, tools/strata_mcp.py, the chat project folder in
apps/host/server/chat-files.mjs); whether the drive is theirs is the existing write test. /run, /run/media itself, /run/user, /run/systemd, /usr, /etc, /var/lib and the
rest are still refused, /media/<user> and /mnt stay allowed when writable.

The refusal from setup's data folder said "Nothing was downloaded or changed." twice (the folder words end with it and setup added its own sentence); the first is dropped
for that caller only.
2026-11-08 20:52:43 +01:00
Niko1221 b5f5e38a44 feed model install gate: a stand-in nvidia-smi on a box with no NVIDIA card (setup's step 1), so the Linux CPU runners can run it 2026-11-08 20:50:40 +01:00
Niko1221 6dba262794 shape engine: the lines a person reads name the model's title (Q4 Lite Shape), not the geometry (q4_lite); one switch (shape_engine.USE_TITLE) puts the geometry name back. strata components engine and the JSON keep the shape 2026-11-08 20:41:51 +01:00
Niko1221 a65e14cc05 Leave the engine version bump (0.1.59) out of this branch: the 0.2.13 release candidate bumps every version in one commit; STRATA_CMAKE_EXTRA stays 2026-11-08 20:40:03 +01:00
Niko1221 201ee46b46 UPDATES.md and ENGINE-COMPONENT.md: the shape_engines feed member and package_engine.py --shape 2026-11-08 20:31:11 +01:00
Niko1221 8a9feaedde A new-shape Qwen4 model ships with its own engine component (strata-q4-<shape>), picked by the model, next to the Strata engine
An engine is compiled for one geometry and refuses a GGUF of another, so the engine of a new Qwen4 shape must not replace the Strata engine (Flash-Next users keep theirs).

* feed: member shape_engines (engine entries plus shape); a feed model of family qwen4exp_<shape> runs on that shape's engine; build-feed --shapes refuses a model of a shape with no engine
* packaging: package_engine.py --shape (manifest id strata-q4-lite and the shape in the signed manifest), pack-engine names the archive by the component
* Control: strata_control/shape_engine.py installs it into <components>/shapes/<shape>/engine/ (resumable download, archive and manifest hashes, signature, shape, one test start), the run config says component:strata-q4-lite and engine_component.expand resolves it there; strata update brings an installed shape engine up to date beside the older one; strata components engine reports it
* setup (the app's Download, the Terminal's launch) fetches it when the model is chosen; the Terminal's first step checks it instead of the Strata engine; a shape model's min_engine is not compared with the Strata engine; not offered on a Mac yet
* older Controls ignore the member, read only strata-engine manifests and do not know the family, so they never offer the model

Tests: tests/terminal/test_shape_engine.py, tools/test_setup_shape.py.
2026-11-08 20:18:24 +01:00
Niko1221 a04860fadf DFlash2 defaults: conf 0.55 on the large-VRAM Blackwell class for IQ3_S only (the measured set); window 8 stays for every set there
Per the coordinator: SetDefault gets big_blackwell_conf (set on ("cuda", "iq3_s") = 0.55), applied
with the tuned pack on cc 12.0 with >= 48 GB. UD-IQ4_XS-v31 and UD-Q4_K_XL on that class keep conf
0.35 (they were not measured there); the window of 8 is set-independent in the engine and stays.
Snapshot goldens: nv_resident ud_iq4_xs_v31 and ud_q4_k_xl conf back to 0.35 (spec stays 8);
tests updated.
2026-11-08 20:13:27 +01:00
Niko1221 6ec4d68ab2 DFlash2 per-device defaults (11-08 CONF-SWEEP): Strix Halo window 5 -> 8; Blackwell cc 12.0 with >= 48 GB conf 0.35 -> 0.55 and window 6 -> 8
Strix Halo (gfx1151): engine cap 5 -> 8 and the Terminal writes --spec 8; conf stays 0.35.
Measured (5 interleaved pairs, 5/5 faster): UD-Q4_K_XL +5.3%, UD-IQ4_XS-v31 +2.1%.
UD-Q4_K_XL greedy output differs from the old width's (deterministic run to run); v31 is identical.

Blackwell sm_120 (cc 12.0) with at least 48 GB: cap 6 -> 8, --draft-conf 0.55 (RTX PRO 5000 72 GB,
IQ3_S +2.4%, 5/5 pairs, output identical). The DGX Spark (cc 12.1) and consumer cards under 48 GB keep
conf 0.35 and cap 6. dflash2_auto_cap now takes the compute minor and the card's memory; drafting.py
writes --spec 8 only on these two classes (window_for), so no other card's effective window changes.

draft_cap_test, tests/terminal drafting tests (new test_drafting_card_window.py) and the snapshot
goldens (nv_resident and strix_halo_win only) are updated.
2026-11-08 20:04:02 +01:00
Niko1221 60d0f66de7 feed model install gate: the engine step runs serve.server with the run config 2026-11-08 19:32:34 +01:00
Niko1221 1ecb9de791 UPDATES.md: the feed-model install gate 2026-11-08 19:29:19 +01:00
Niko1221 51b75ea25f tools/test_feed_model_install_realfile.py: the install gate as a test of tools/check (py-tools) 2026-11-08 19:29:09 +01:00
Niko1221 c47e9b9929 tools/feed_model_install_gate.py: a feed model installed from a real synthetic file through setup (release gate G2) 2026-11-08 19:26:39 +01:00
Niko1221 35b776c34d Owner's wording for the two new setup lines; the plan line for a drafter-none feed model (G14); make_model_release plan: right boxes, no app rebuild for case A (G12); tests 2026-11-08 19:23:20 +01:00
Niko1221 132c5809b8 Merge f-q4-p8-fixes: engine 0.1.59 and STRATA_CMAKE_EXTRA in the Linux packaging scripts
# Conflicts:
#	metal/CMakeLists.txt
2026-11-08 19:16:57 +01:00
Niko1221 e6569ae4df Merge f-q4-p8-fixes-a: the Qwen4 rehearsal's setup and feed fixes F1-F5 2026-11-08 19:16:47 +01:00
Niko1221 3289d8280d Merge f-0213-pr5531 (9947e7d6f): community PR #5531 by pjgmobile for #5327 - on Linux the expert arena loads again when free pages, droppable file cache or a reserved hugetlb pool can serve a RAM shortfall (their measure: 1M ctx 47 -> 124 tok/s); review fixes: mlocked file pages are not droppable, stricter hugetlb credit (keeps #4051's refusal); plain warning text, stdout only with --serve; normal starts unchanged (tokens and output identical) 2026-11-08 19:07:24 +01:00
Niko1221 9947e7d6f5 docs/DETAILS.md: the #4051 section no longer says the load never refuses; says when the experts load anyway (#5327) 2026-11-08 19:02:59 +01:00
Niko1221 b12773820f #5327: the owner's wording for the "loads anyway" warning, and the RAM notices on stdout in --serve mode only
The warning now reads "the experts need X GiB of RAM and Y GiB is free, but free memory, file cache and reserved huge pages can hold
them, so they load anyway (same answers). STRATA_ARENA_FILE_CACHE=0 reads them from the file instead."  It and the "not enough RAM for
the expert arena ... reads the experts from the file" notice go to stdout only with --serve (before READY, which the server ignores; a
service log has them).  A plain generate run keeps its token stream clean: stderr only, as before the PR.
2026-11-08 19:02:58 +01:00
Niko1221 458c312c9c settings register: STRATA_ARENA_FILE_CACHE (#5327), which the settings-register check asks for 2026-11-08 17:41:35 +01:00
Niko1221 4890fd9539 #5327 review: the arena shortfall credit leaves out locked file pages, reserved huge pages, a non-2 MiB pool and a pool smaller than the padded mapping
Review of #5531 on top of the contributor's two commits.  The credit read Cached - Shmem - Dirty as droppable, but file pages the
process itself or an engine still exiting has locked (the PLE table's mlock is a mapping of the model file) sit in Cached and the kernel
cannot drop them; MemAvailable leaves them out.  An engine with --ple-io ram and a short arena was told its own locked table could serve
the arena - the shape of #4051's stall.  Now: Mlocked is subtracted; HugePages_Rsvd is subtracted from the free pool; the pool counts only
at the 2 MiB size the arena maps; STRATA_NO_LARGEPAGES (no MAP_HUGETLB try) credits no pool; and the pool must hold the padded mapping
(the pin-by-need stride padding plus the spare blob), not want + blob.  Tests for each.
2026-11-08 17:39:12 +01:00
pjgmobile 41e20da530 #5327: correct the comments - MemAvailable does not credit "only about half" of the page cache (it counts nearly all of it, minus the low watermark); the false alarm this fixes is the reserved hugetlb pool, which MemAvailable does not count at all. The file-cache credit stays as a small secondary margin. Comments only, no code change. 2026-11-08 17:24:35 +01:00
pjgmobile 28bd0185f9 #5327: on Linux an expert-arena shortfall the file cache can serve loads through it again - the 0.1.55 warn-and-proceed (free pages + Cached+Buffers+SReclaimable-Shmem-Dirty cover the need: MemAvailable credits only half the cache), said loudly on stdout as well, because a service setup never sees stderr; the #4846 refusal stays for shortfalls it cannot serve (#4051's anonymous RAM), under a cgroup limit, and on Windows, and STRATA_ARENA_FILE_CACHE=0 turns the credit off; tests 2026-11-08 17:24:35 +01:00
Niko1221 ee1b382e9c feed models: qwen4exp sizes are read in place with a RAM budget (budget True) like the built-in Unsloth sizes; the low-RAM mode wrote a second 77 GB copy of the experts on a 64 GB PC; q4geo_synth --tokenizer-from 2026-11-08 15:44:25 +01:00
Niko1221 88ea8e2a02 setup: a feed model of another shape (layers x experts) than the shipped expert profile gets a neutral profile; the engine refused the shipped one 2026-11-08 15:35:27 +01:00
Niko1221 508e6999ab Merge f-own-1a (451f3e8a6): own-stack phase 1A and cleanup 5.4; the ggml/MMQ sources the engines build are vendored in third_party/strata-ggml (frozen at the pin, manifest and check), default engine builds need no llama.cpp download, the TQ overlay is plain files, CMakeLists.txt is split into cmake/backends/*; every engine unchanged (CUDA/HIP gfx1201/gfx1151/SYCL device code identical, SYCL 22/22 checksums, Metal 104/105 objects with only __TIME__ differing, CUDA tokens 26/26); release scripts still pass -DSTRATA_GGML_DIR (owner go 11-08) 2026-11-08 15:28:57 +01:00
Niko1221 451f3e8a60 tests/terminal/test_engine_manifest: read the CUDA architecture default from cmake/backends/cuda.cmake 2026-11-08 15:28:10 +01:00
Niko1221 03882d09db feed models: drafter none leaves the Flash-Next MTP draft layer out (setup fetched it for every qwen4exp model) 2026-11-08 15:23:11 +01:00
Niko1221 b40d65ea4c setup: a feed model without a picture reader no longer stops install with KeyError 'mmproj' (found installing a feed qwen4exp model from a test feed) 2026-11-08 15:18:18 +01:00
Niko1221 f223047daa make_model_release: write the model's entry in the feed's schema (models/<id>.entry.json) with the values it cannot know as [FILL: ...]; build-feed --models refuses an entry that still has one 2026-11-08 14:57:07 +01:00
Niko1221 eddbf228dc Engine version 0.1.59 (CMake project and Metal host) and STRATA_CMAKE_EXTRA in the Linux packaging build scripts (a new model shape: -DSTRATA_GEOMETRY_FILE) 2026-11-08 14:46:43 +01:00
Niko1221 e66450705e tools/test_gfx906_build_files: read the cmake/*.cmake files the CMakeLists.txt blocks moved to 2026-11-08 14:43:03 +01:00
Niko1221 1115121839 CMake: the K-quant MMQ comments say what the engine does (default on NVIDIA 8.0+ and Linux gfx12) 2026-11-08 14:43:03 +01:00
Niko1221 974c0e3467 tools/check: the docs step also runs check_vendored.py (third_party/strata-ggml manifest) 2026-11-08 14:43:03 +01:00
Niko1221 8b16677015 Split CMakeLists.txt by backend: cmake/backends/{cpu,cuda,hip,sycl,metal}.cmake, strata_ggml.cmake
The 3,100-line CMakeLists.txt is cut into macros, each called where its block
used to be (a macro runs in the caller's scope, so variables, targets, sources,
flags, options and their order are unchanged):
- cmake/strata_ggml.cmake: which ggml (vendored tree or the STRATA_GGML_DIR
  override), the forced GGML_* options of the root project, the MMQ instance
  list and the trellis variants' origin;
- cmake/backends/cpu.cmake: strata_kernels_cpu, the native (ggml-cpu) expert
  rows, the CPU tests, the DeepSeek CPU-lane bench;
- cmake/backends/cuda.cmake: the CUDA toolchain, strata_mmq / strata_mmq_w4a4 /
  strata_kq, the spill guard;
- cmake/backends/hip.cmake: the gfx906 compat toolchain and sources, MMQ on HIP;
- cmake/strata_gpu_engine.cmake: the GPU engine both CUDA and HIP build;
- cmake/backends/{sycl,metal}.cmake: the ggml block of sycl/ and metal/.
sycl/ and metal/ resolve the vendored root with the same macro (normalised
path, so the objects' __FILE__ strings equal the root project's).
2026-11-08 14:43:03 +01:00
Niko1221 19418261e9 Give the quant header Strata's name as a forwarder, nothing else moves
third_party/strata-ggml/strata-quant-common.h includes third_party/ggml/ggml-common.h,
with the provenance and MIT notice. The text stays at the old path: the Metal
runtime reads that file's text under that name and the Mac package ships it as
metal/ggml-common.h, so the runtime lookup, the compiled-in path and the
packaging scripts are untouched (the swap goes with the 1B packaging change).
2026-11-08 14:43:03 +01:00
Niko1221 bc83c088de Fold the trellis MMQ overlay into plain files of the vendored ggml-cuda
The configure-time overlay (cmake/strata_ggml_tq_overlay.cmake) wrote the Gyro
trellis types (TQ2_T, TQK6, TQK7, TQP5-7) into patched copies of ggml-cuda's
headers inside the build tree. They are plain files in
third_party/strata-ggml/ggml/src/ggml-cuda-tq now (patched ggml.h, mmq.cuh,
mmq-load-tiles.cuh, mmq-config-*.cuh; the copies quantize.cu, quantize.cuh,
mmq-vec-dot.cuh; tq.cuh, the tile loader and the six instances), byte for byte
the overlay's output and each listed in UPSTREAM.md; check_vendored.py checks
that the patched files only add lines. The pristine ggml-cuda files stay
untouched, so every regular instance compiles exactly as before. The default
build uses the folder like the overlay dir (first on strata_mmq's include path);
the SHA-256 guard of the MMQ_TAB scheduler is not needed for it. A
-DSTRATA_GGML_DIR build still gets the overlay, reading tq.cuh and the tile
loader from the vendored folder. third_party/ggml-tq is gone.
2026-11-08 14:43:03 +01:00
Niko1221 067b4dda5f Vendor the ggml sources the engines build into third_party/strata-ggml
ggml-base, ggml-cpu (x86, Arm) and the ggml-cuda MMQ headers, quantizer and
instances the engines compile, frozen at llama.cpp 3cf03257 with the MIT
licence, UPSTREAM.md (file list with sha256) and tools/check_vendored.py.
The layout is llama.cpp's, so CMake takes ${root}/ggml from the vendored tree
by default (root, sycl, metal, split) and no longer fetches llama.cpp.
-DSTRATA_GGML_DIR=<checkout> stays as an override. Only ggml/CMakeLists.txt and
ggml/src/CMakeLists.txt differ from upstream (trimmed to ggml-base + CPU).
2026-11-08 14:43:03 +01:00
Niko1221 fdb376f398 qwen4_intake: the Metal and SYCL down-expert lists follow main's f-q4-kquant (Metal adds Q3_K/Q4_K/Q5_K/Q6_K/IQ3_S, SYCL Q5_K/Q6_K); test_metal_lists and test_sycl_lists read the sources again 2026-11-08 13:36:58 +01:00
Niko1221 5c2e80ff43 settings register: the line numbers of two STRATA_ reads that the feed-models merge moved (setup.py, strata_cli/launch/screens.py); --check clean 2026-11-08 11:52:17 +01:00
Niko1221 0fdfa40b47 Merge f-q4-feedmodels (d4b72bc90): the signed feed's optional models member (strata_control/feed.py, feed_models.py), setup tables and prescan, tools/strata_mcp.py catalog, packaging/feed_tool.py --models/model-files/verify-feed, the app family menu (Qwen4 prep) 2026-11-08 11:50:30 +01:00
Niko1221 97a50dc24c Merge f-q4-intake (37e91bfdb): tools/qwen4_intake.py, the tokenizer refuses an unknown pre-tokenizer, the server's tool-call parser follows the chat template (serve/toolformat.py), packaging/make_model_release.py dry run (Qwen4 prep) 2026-11-08 11:50:24 +01:00
Niko1221 7f46672a48 Merge f-q4-metal (ed1f58d27): the Mac (Metal) engine follows the compiled geometry (q4_lite/q4_mid/q4_big kernel parity 40/40, gate 2 like CUDA), plus the PLE row-buffer fix in src/program/generate.cpp (sized by the compiled hidden size, not 2560; it overran 24 KB at hidden 4096 and crashed Metal serve); default build unchanged (CUDA SASS 3573/3573 and 160/160 host objects, HIP gfx1201/gfx1151 string offsets only, SYCL 129/129 objects and 22/22 checksums, Metal per-token functions identical, UD-Q4_K_XL 10/10 tokens, CUDA IQ3_S 26/26 tokens) (Qwen4 prep) 2026-11-08 10:13:39 +01:00
Niko1221 db137d917a Merge f-q4-sycl (41717f29c): the Intel Arc (SYCL) engine follows the compiled geometry, so q4_lite/q4_mid/q4_big build; default build unchanged (CUDA SASS 3573/3573, HIP gfx1201 3920/3920, gfx1151 string offsets only, Metal 85/85 objects, SYCL 22/22 checksums); A750 gate 2 q4_lite 0.00021 vs floor 0.00017; one conflict with f-clean-6-0 in sycl/src/prefill/prefill.cpp resolved as tested by Q4-SYMERGE (Qwen4 prep) 2026-11-08 09:42:22 +01:00
Niko1221 0bf2efe331 Merge f-clean-6-0 (646826312): Intel Arc output is repeatable run to run; skinny bf16 GEMMs (outputs <= 64) run on a fixed-order kernel instead of oneMKL, which is not bit-repeatable for them (STRATA_SYCL_BF16_DET=0 restores oneMKL); A750 50/50 runs identical with the gate recipe, prefill 0.995-1.004; owner OK'd it as the default 11-08 (cleanup 6.0) 2026-11-08 09:09:52 +01:00
Niko1221 ed1f58d275 settings register: regenerated after the rebase onto 09eb82791 (STRATA_METAL_MSL_DUMP, research; source line numbers) 2026-11-08 09:05:16 +01:00
Niko1221 dcaf4755c1 ple_direct_rows_per_s: the probe's output buffer is PLE_N_HEADS x PLE_HEAD_DIM floats a token, not 2560 (heap overflow of 24 KB at hidden 4096; on Metal it corrupted Apple's shader-cache objects and the first window of strata --serve at q4_big died in MTLCompilerFSCache::getElementImpl) 2026-11-08 09:04:39 +01:00
Niko1221 a875d702cf Metal window_probe: --session N carves the engine's session state (session_init) before the window comes up 2026-11-08 09:04:39 +01:00
Niko1221 7893d4972e Metal tests: msl_dump keeps --touch, --map and the pipelines after all libraries (restored after the revert) 2026-11-08 09:04:39 +01:00
Niko1221 97a1df1b30 Revert "Metal: -DSTRATA_METAL_PRELUDE_SALT adds one comment line to the kernel prelude (separate compile-cache keys for a build)"
This reverts commit 362a8c5ebe4b0d25419973859f52b575536da474.
2026-11-08 09:04:39 +01:00
Niko1221 c3bdd42dc2 Metal: window_probe runs the decode window without the engine (init, windows, the gate-2 requests)
metal/probe/window_probe.cpp links the engine's window (init, the prompt chain, decode windows, logits) and runs it on its own: a few windows,
or with --requests / --logits-out the gate-2 requests (prompt + forced tokens) with the logits of every last position written out for
g3kl_mac.py --from-dump. It is how the q4_big shape was judged when `strata --serve` crashed in Apple's shader cache before its first window.
2026-11-08 09:04:39 +01:00
Niko1221 496a5382e4 Metal: -DSTRATA_METAL_PRELUDE_SALT adds one comment line to the kernel prelude (separate compile-cache keys for a build) 2026-11-08 09:04:39 +01:00
Niko1221 906aa63c75 Metal: a crash also prints the open descriptors, their limit and the process's memory; msl_dump --wrap / --map hold a model like the engine 2026-11-08 09:04:39 +01:00
Niko1221 021e07be72 Metal tests: msl_dump --pipes also builds a few pipelines of the core library 2026-11-08 09:04:39 +01:00
Niko1221 a78b2d6278 Metal tests: geo_check prints the shape a build is compiled for and asserts a default build's constants are Flash-Next's 2026-11-08 09:04:39 +01:00
Niko1221 a479b17a15 Metal: the selection budget follows idx_top_k; msl_text_check preprocesses with clang; geo_pins counts the pins
qsa.metal QBUDGET and the partials buffer of the 32-cell chunks take the indexer's top_k from the prelude (GEO_BUD) instead of 2048 + 3.
tools/msl_text_check.py runs the C preprocessor (the Metal toolchain component is not on the Macs) and the files are LF.
tools/geo_pins.py counts the literals that are Flash-Next's shape per file at two revisions.
2026-11-08 09:04:39 +01:00
Niko1221 943e67ea98 Metal tests: the parity programs take their shape from the compiled geometry
gr, gdn, gdn_prompt, qsa, qsa_prompt, route, prompt, expert and moe_mm parity (and gemv_parity --nin) read hidden, expert width, hc_lr,
head counts, GDN heads, top-k and expert count from engine/geo.h instead of 2560 / 640 / 320 / 24 / 48 / 10 / 512, and compile their
kernel libraries with geo::prelude(); qsa_parity dispatches the qsa_prep groups the model has (QH + 2 QKV + IH + 1, was a fixed 33).
Built with -DSTRATA_GEOMETRY_FILE=<header> they are the kernel parity of that shape; without it they are what they were.
2026-11-08 09:04:39 +01:00
Niko1221 ab5a09b6ff Metal engine: sizes and loop bounds come from kCompiledGeometry; Window::init checks all 19 fields
window.cpp, prompt_chain.inc and stream.cpp took hidden 2560, width 640, 48 layers, l % 4, 16/48 GDN heads, 10240/6144/12288/16480 channel
counts, 24 query heads, 2051 selection cells, 33 attention chunks and the per-layer state sizes from literals; they now take them from
engine/geo.h (the numbers they replaced for a Flash-Next build). Window::init asks strata::geometry_unsupported (the 19 fields the CUDA guard
compares, plus rms_eps) instead of 6 fields, and also refuses a file whose experts per token differ from the compiled number; the refusal
message is today's. The MTP draft layer and PromptGemm (SSD streaming) host buffers follow the same names; their kernels and DFlash2 are not
part of this change.
2026-11-08 09:04:39 +01:00
Niko1221 e172375b25 Metal kernels: query and kv heads, experts per token, hc norm and up widths come from the compiled geometry through the #define prelude
engine/geo.h turns kCompiledGeometry into the host constants and into geo::prelude(), the #define lines the kernel libraries start with
(GEO_QH, GEO_QKV, GEO_TOPK, GEO_NJ, GEO_GRU_NU); a define is written only where this build's value differs from Flash-Next's, so a
Flash-Next build hands the compiler the prelude it always did. qsa.metal (QH, QKV, and the indexer-key threadgroup that was r == 32),
moe.metal / router.metal / glue.metal / prompt.metal (top-k 10 in route_tok, route_tok_p, moe_combine_sh2) and gr.metal (gr_norm2's
10 elements a thread = n_embd / 256, gr_up's prefetch of 3 units a lane = ceil(hc_lr / 128)) read them with Flash-Next's number as the
default. Preprocessed, every library of a Flash-Next build is token-identical to before (tools/msl_text_check.py, 12 of 12 libraries,
plain, K-quant and F16 preludes); 8 of the 12 are byte-identical.
2026-11-08 09:04:39 +01:00
Niko1221 0ff18ef93d Metal: the engine's kernel libraries in one header, STRATA_METAL_MSL_DUMP, msl_dump and msl_text_check
msl_libs.h lists every library the engine compiles (files, order, prelude); window.cpp, prompt_chain.inc and dflash2.inc build
them from it, and tests/msl_dump.cpp compiles the same libraries without starting an engine. STRATA_METAL_MSL_DUMP=<dir> makes
mtl_rt.mm write the exact text handed to the compiler (NNN.msl + index.txt); tools/msl_text_check.py compares two dumps (raw bytes,
then the preprocessed text, then the AIR). No kernel and no engine behaviour changes.
2026-11-08 09:04:39 +01:00
Niko1221 269f33859b fused_gr: the dynamic-shared-memory opt-in of the single-token down kernel is set once per device, not once per process (a layer split runs it on several cards); doc_pages lists the .md files git knows about, so an ignored build tree's llama.cpp copy is not link-checked 2026-11-08 08:46:50 +01:00
Niko1221 2b36e8a027 Merge f-clean-3 (321794636): docs and history (cleanup step 3, items 3.0-3.10); release manifest and capability table with checks, history/ with pointers, link check, tools/ split, bench index; owner OK'd the wording 11-08 2026-11-08 08:35:12 +01:00
Niko1221 321794636c READMEs: DeepSeek on Mac reads pictures too (dropped 'text only'); the 7 translations now name Strix Halo and the Mac like the English page; MAC.md and MODELS.md say the same in plain words 2026-11-08 08:34:44 +01:00
Niko1221 41717f29c1 qsa_real_shapes() takes the compiled geometry; gdn_parity's prefill cases and native_grouped_parity follow the build's shape
qsa_real_shapes() (24 / 2 / 256 / 64, indexer 4 x 128) is the shape every QSA parity program builds from; it now reads kCompiledGeometry (the same values for the default build), so the
q4_* builds' kv_stream / qsa_prompt_attn / snapshot checks run at their own head counts instead of being refused.  gdn_parity's prefill cases used 48 value heads; native_grouped_parity's
compiled-shape loop no longer asks the K-quant checker for an IQ4_NL down.
2026-11-08 07:00:42 +01:00
Niko1221 09eb827914 Merge f-q4-hipkg2 (da7b1d022): the AMD prompt-expert kernels (F16 K-quant, native-format fused IQ, Q2 blobs) follow the compiled geometry; default build unchanged (HIP gfx1201 182/182 and gfx1151 194/194 touched functions, CUDA SASS 3573/3573 and 160/160 host objects, SYCL 129/129 objects and 22/22 checksums, Metal 85/85 objects); HIP q4_lite gate 2 0.000183 vs floor 0.000173 (Qwen4 prep) 2026-11-08 06:15:05 +01:00
Niko1221 8893ba5eec SYCL dq_dense_parity: the dense GEMV checks also run at n_in 2048 and 3072 (the hidden widths of q4_lite and q4_mid) 2026-11-08 05:41:56 +01:00
Niko1221 b72b451529 SYCL parity programs: gr, gr_multi, cvec, gdn, s2_expert_grouped, native_expert and native_grouped take the compiled shape
gr_parity, gr_multi_parity, cvec_parity used 2560 / 4 / 320, gdn_parity 128 / 16 / 48 and 2560, s2_expert_grouped_parity 2560 x 640 (and a blob-size assert for that shape only): they now read
kCompiledGeometry, so the q4_lite / q4_mid / q4_big builds check their own kernels (the values for the default build are the ones they had).  native_expert_parity reads
Q4KQ_PARITY_EMBD / Q4KQ_PARITY_FF as the CUDA tree's test does; native_grouped_parity adds the build's hidden width to the K-quant down widths and, for a build that is not
Flash-Next's, the UD mix's pairs at the build's own expert shape.
2026-11-08 05:35:50 +01:00
Niko1221 e1e9d6ff95 SYCL prompt path: split_q, gate_attn and kv_append take the query head count from the geometry
The three kernels had 24 query heads (and the launcher's ld16 default and block counts T x 24 x 256) written in. The CUDA prompt path takes the fused rms_rope_rows route and keeps these only for
STRATA_QSA_FUSE_NR=0; this port has no fused route, so at 16 or 32 heads they would have mixed the heads up silently.
2026-11-08 05:32:21 +01:00
Niko1221 30fabc8ac9 SYCL engine: top-k, layer count and the PLE probe follow the geometry; STRATA_LOGITS_DUMP for the gate-2 judge
verify.cpp and remote_experts.cpp refused k != 10 (the verifier would not start at top-8); generate.cpp had two layer-index checks against 48 and a 2560-float buffer for the
PLE probe.  verify.cpp also gets src/core/verify.cpp's STRATA_LOGITS_DUMP debug tap (off unless set), which the logits judge reads.
2026-11-08 05:14:34 +01:00
Niko1221 c598662834 SYCL prompt path: prefill.cpp, kernels.dp.cpp and the Xe GDN recurrence take the model shape from the compiled geometry
prefill.cpp: N, HC, LR, K, NE, C, ZV, HV from kCompiledGeometry and the loose literals (1280 / 640 / 2560 expert and hidden widths, 12288 / 512 / 128 attention and
indexer widths, the 24 and 2 head counts) became the named widths src/prefill/prefill.cpp uses (kFF, kFF2, kEmbd, kQW, kQFull, kKvW, kIdx*).
kernels.dp.cpp: N, HC, LR, S, HK, HV, C from the geometry; the router, combine and SwiGLU kernels use KU (top-k) and FFC (expert width).  The Q2_0 blob dequantisers keep
Flash-Next's layout (Q2_0 packs only).
gdn_xe.hpp: the Xe GDN recurrence (default on for Alchemist) had its own copy of 128 / 16 / 48 / 10240 with no assert: it now reads the geometry.
moe_fused / moe_fused_iq: the fused expert kernels are Flash-Next's 2560 x 640 and now decline any other compiled shape instead of mis-running.
For the default geometry every value is the one it replaces.
2026-11-08 05:14:34 +01:00
Niko1221 b9de2cdb8c SYCL PLE post-ops, PLE key, native MoE combine and S2 grouped experts take their widths from the compiled geometry
native_ple_postops (N, H), ple (the native key's geometry assert now says what the kernel needs), native_moe (the combine's rank count KU = n_expert_used, 10 for
Flash-Next; the vec4 fast path is taken when k == KU) and s2_expert_grouped (H, FF) follow core/geometry.hpp, as src/kernels/cuda does since the geometry work.
2026-11-08 05:14:11 +01:00
Niko1221 3b64e076c8 SYCL attention: the decode and the three prompt kernels take the heads per KV head from the compiled geometry
G = n_head / n_head_kv replaces the literal 12 in qsa_decode_attn, qsa_prompt_attn, qsa_prompt_attn_xmx and qsa_prompt_attn_esimd (the static_asserts that stopped
every shape but Flash-Next).  The kernels already index by G (the decode kernel's float4 probability rows and 16-lane score reduce need G % 4 == 0 and G <= 16; the prompt
kernels pad the query rows to 16), so 8 (q4_lite) and 16 (q4_mid, q4_big) compile as they are.  The decode kernel's q8_v3 / pre-sm_75 variants that the CUDA side
generalised do not exist in this port.
2026-11-08 05:14:10 +01:00
Niko1221 5306fee502 SYCL fused_gr: the hyper-connection read takes n_embd, hc and hc_lr from the compiled geometry
N, HC and LR come from core/geometry.hpp (the static_assert that stopped q4_lite / q4_mid / q4_big is gone); the plain read's tile is N (N / 2 for the staged tile), the
literal 2560 / 1280 template arguments and tile choices became N and N / 2, as in src/kernels/cuda/fused_gr.cu.  The single-token down kernel's hidden row (D floats)
no longer fits 48 KB at hidden 3072 and up: fused_gr_read then takes the multi-token read of one token (bitwise the same read, the default path already).
The Q8_0 read fixes of the CUDA side (chunk width from n_embd, up-row steps from hc_lr) have no counterpart here: this file has no Q8_0 read.
sycl/tools/fixups.py: the kernel-name fixup also accepts the tile spelled N or (N / 2), so a re-migration of the CUDA source keeps working.
2026-11-08 05:14:04 +01:00
Niko1221 da7b1d022e Strata Q2 blob fused experts (moe_fused.cu CUDA and gfx11 kernels) and their parity test follow the compiled geometry
Weight-row counts, K stages, code/scale row bytes, the scale staging copies, the H row stride, the down store stride and
the launch grids come from kMoeEmbd / kMoeFF.  The CUDA kernel stages a row's gate/up scales in 16-byte copies, so the hidden
width must be a multiple of 512 (static_assert).
2026-11-08 04:48:52 +01:00
Niko1221 2351f2cccb Strata Q2 blob (prompt path): blob_dequant and strata_q2_kernel take the row lengths, scale counts and offsets from the compiled geometry
Gate/up codes [2 F][E/4 B], down codes [E][F/4 B], gate/up scales [2 F][E/64], down scales [E][F/64] (Flash-Next: 1280 x 640 B,
2560 x 160 B, 1280 x 40, 2560 x 10).  Launch sizes follow.
2026-11-08 04:48:52 +01:00
Niko1221 6e4ef31e47 q4geo_synth: --exact-bf16 / --exact-parts and --router-scale
The pack's --compat-bf16 turns the F32 router, the shared-expert gate, the HC inject and the Q8_0 HC projections into BF16. With --exact-bf16 those tensors are written BF16-exact (F32 values rounded, Q8_0 scale 2^-12), so the conversion is lossless and llama.cpp and the engine read the same weights.
--router-scale multiplies the router weights.
2026-11-08 04:48:52 +01:00
Niko1221 78c27655c8 prefill_fused_iq_test follows the compiled geometry and gets --edge; q4geo_synth gets the AMD fused recipes and --small-ple
The HIP/CUDA fused-expert parity test took 2560 / 640 / top 10 / 512 experts as constants; they are now kCompiledGeometry's,
so the test built with -DSTRATA_GEOMETRY=<name> checks the kernels at that shape.  --edge runs the tail and tile-boundary
token counts (33, 1, 130, 517, 640) for every pair.
tools/q4geo_synth.py: recipes fused-kq (Q4_K/Q5_K gate/up with Q5_1/Q8_0 down), fused-iq (IQ3_S gate/up, IQ4_NL down) and
mix-kq (one file with every K-quant expert path of the AMD prompt kernels) at any expert width, and --small-ple (PLE head
sizes of 16 small primes: a 6 MB table instead of 28 GB, for hosts that cannot write the sparse table; the moduli are read
from the file).
2026-11-08 04:48:52 +01:00
Niko1221 b273c8006e HIP/CUDA native-format prompt experts (moe_fused_iq.cu): K of gate/up and down, the weight-row counts, the grids and the down store stride follow the compiled geometry
GU_ROWS_K, D_ROWS_K and the 1280 / 2560 weight rows of the int8, WMMA and F16 kernels come from kMoeEmbd / kMoeFF /
kMoeGuRows, with a static_assert that a product's rows are whole work items.  Flash-Next's device code (gfx1151)
is unchanged.
2026-11-08 04:48:52 +01:00
Niko1221 3d209733d2 HIP F16 K-quant prompt experts: the work-item counts, activation strides and down stride follow the compiled geometry
moe_fused.hpp gets kMoeEmbd, kMoeFF and kMoeGuRows from kCompiledGeometry (2560, 640, 1280 for Flash-Next).
moe_f16_common.cuh and moe_f16_kq.cu take GU_NFB, D_NFB, the activation and H row strides, the stage counts and the down
store stride from them, with a static_assert on the divisibility the tiles need.  The default geometry's device code
(gfx1151) is unchanged.
2026-11-08 04:48:52 +01:00
Niko1221 4dad59ab37 Merge f-q4-geometry (fc1c4e429): the model shape is fixed at build time (-DSTRATA_GEOMETRY / -DSTRATA_GEOMETRY_FILE); the default Flash-Next build is unchanged (CUDA SASS 3573/3573, HIP gfx1201 3920/3920, gfx1151 path-string offsets only, SYCL 22/22 checksums, Metal 10/10 tokens, CUDA 130/130 tokens); Qwen4 test shapes q4_lite/q4_mid/q4_big pass logits gate 2 on CUDA (Qwen4 prep) 2026-11-08 04:48:39 +01:00
Niko1221 fc1c4e4295 fused_gr: the Q8_0 up read walks hc_lr in as many 128-value steps as it needs
The up-projection rows of the STRATA_HC_Q8 read were loaded in a fixed 3 steps of 128 values, which covers hc_lr up to 384. At hc_lr 512 (q4_big) columns 384-511 of every row were dropped (KL 0.0059 against llama.cpp, 15 of 144 positions over the rule; 0.00028 and 3 positions after the fix, the same as without HC_Q8). The step count is now (hc_lr + 127) / 128: 3 for Flash-Next's 320, unchanged.
2026-11-08 04:36:17 +01:00
Niko1221 d10a8cd7e1 fused_gr: the Q8_0 hyper-connection read takes its chunk width from n_embd
The STRATA_HC_Q8 read (the default on gfx12 and gfx1151 HIP) split each stream into chunks of a fixed 640 columns. At n_embd 2048 (q4_lite), 3072 (q4_mid) and 4096 (q4_big) a stream is not a whole number of chunks: 12 chunks of 640 cover 7680 of q4_lite's 8192 columns, so every hyper-connection read missed 6% of its input (3% error in the mixed activation, then wrong logits: KL 0.027 against llama.cpp, 67 of 144 positions over the rule).
The chunk is now the largest multiple of 128 up to 1024 that divides n_embd: 640 for Flash-Next (unchanged), 1024 for 2048, 3072 and 4096 (at most 16 chunks, the scratch's room). Static asserts for n_embd % chunk and hc_lr % 32.
2026-11-08 04:36:17 +01:00
Niko1221 646826312a SYCL: debug-only hash dumps to find where two runs of the same prompt first differ (all off unless set)
STRATA_PF_HASH_FILE: per row hashes of the first GDN layer's stages in the prompt path. STRATA_LAYER_HASH_FILE: per layer hashes of the payload the host sees for the CPU experts and of their output. STRATA_STATE_HASH_FILE / STRATA_STATE_DUMP: hashes (and a raw dump of the first four layers) of the GDN states and filled int8 KV before a window. They found the oneMKL bf16 GEMM repeatability problem fixed in the previous commit.
2026-11-08 04:10:03 +01:00
Niko1221 0f3ae296bf SYCL: the prompt path's skinny bf16 GEMMs run on a fixed-order kernel, because oneMKL's result changes from launch to launch for them
On the Arc A750 the same prompt gave different tokens on different runs. The first thing that differs is the ssm_alpha / ssm_beta projection (48 outputs) of the first GDN layer: oneMKL's bf16 GEMM returns a different result on every launch for M=48,T=86,K=2560 and M=4,T=86,K=10240 (the hyper-connection inject), also at T=1..8 for M<=64 and at T<128-160 for those M and K. The GDN state then differs in every layer, the first decode window's logits differ by 0.1-0.7, and a near-tie flips a token.

Gemm::bf16 now runs outputs <= 64 on a 16-lane sub-group kernel with a fixed accumulation order (repeatable; 3-36x faster than oneMKL on the shapes tried, standalone; max abs error 1.6e-5 on sums of scale 90). STRATA_SYCL_BF16_DET=0 gives the oneMKL call back, =2 uses the kernel for every output count. Registered as a rollback switch.
2026-11-08 04:09:45 +01:00
Niko1221 cb73b620a2 host: ending a child that never started no longer signals the caller's own process group on macOS
On macOS (Node 24.12) child.kill() on a child whose spawn failed (no pid, the 'error' event not yet delivered) sends the signal to pid 0, which is the
caller's whole process group. folder-dialog.test.mjs's Linux case let the real portalFolder spawn a missing gdbus and end it, so on a Mac the SIGTERM
reached the test runner and every file after it was cancelled ('Promise resolution is still pending'). Linux and Windows do nothing for such a child.

- process-tree.endTree only calls child.kill() when the child has a pid; a new test runs a never-started child through endTree/endTreeThenKill in a
  group of its own and checks the caller survives (fails on the Mac without the fix, passes with it).
- folder-dialog.test.mjs: the Linux cases pass portal: async () => null like external-env.test.mjs does, so no real gdbus is started.
- load-pictures, packaged-control and the native fixture build their temp folders under the resolved temp path (/private/var on a Mac), as chat-files
  already does; the host reports resolved paths.

m1001 (M4 Mini, macOS 27.2, Node 24.12.0): npm test went from killed after 2 s (722 of 1632 run, 172 cancelled) to 1632 tests, 1522 pass, 0 fail, 110 skipped.
The time-skip tests (timeskip-real, time-skip, timeskip-fork, timeskip-compare-server, tune-and-timeskip-routes, process-tree) passed 20 of 20 runs
before the change and 20 of 20 after.
2026-11-08 01:41:16 +01:00
Niko1221 61632296f3 tests/terminal: the wizard size test waits for the layout to settle instead of a fixed 0.3 s
test_the_three_choices_and_the_name_are_on_screen_at_every_size paused 0.3 s after each resize and then checked the screen once; under load the
redraw came later and the check failed. It now polls (up to 30 s) until the same checks hold, then runs them. The checks are unchanged.
30 runs under CPU load on Windows (8 burners, 2 jobs of 15) and 30 on Linux p1006 (24 burners on 12 threads): 0 failures.
2026-11-08 01:18:28 +01:00
Niko1221 1631a9fde8 Merge f-clean-7 (045843071): the app loads screens, languages, Prism and Time-skip on first use; main chunk 3,123,528 -> 755,418 B, start screen 248.5 -> 143.4 ms (p2006 Chrome, median of 10); nothing users see changes (cleanup step 7) 2026-11-08 00:59:48 +01:00
Niko1221 0458430719 app: load the Time-skip panel and its progress card when a chat has a Time-skip run
Both are drawn only in a chat that has a Time-skip run, so their code is its own file, fetched then (and a few seconds after start when idle, so the first card is not waiting on it). They draw nothing until the code is here: no new text.
A chat with a sample Time-skip run: the card, the opened panel and the comparison are pixel-identical to before.
Main JS file 783,660 -> 755,420 bytes.
2026-11-08 00:40:54 +01:00
Niko1221 204a503d05 app: load the code-colouring grammars after the first screen
Prism and its 15 grammars (src/lib/syntax.ts) are their own file. A code block shows its text at once and is coloured when the file is here: the page fetches it a few seconds after start when idle, and a code block that appears sooner fetches it itself.
The language name of a code fence (code-language.ts) stays in the main file. Screens with code blocks are pixel-identical 0.4 s and 6 s after load.
Main JS file 844,062 -> 783,660 bytes.
2026-11-08 00:34:59 +01:00
Niko1221 c9e9f133ef app: load a language's strings when that language is used, not all nine at start
English stays in the main file (the English text is the key of every string). Each of the other eight languages is its own file (src/lib/catalog-<lang>.ts: the same 14 locale files merged in the same order as before).
The saved language is fetched while the rest of the code loads and the first screen is drawn once it is in; picking another language in Settings or on the first screen switches everything together once its file is here.
The 17 tests that read translations right after setTranslationLanguage now load the catalogs first (loadCatalog); no test expectation changed.
Main JS file 2,736,232 -> 844,062 bytes (gzip 875 -> 278 kB). Screens are pixel-identical in all nine languages apart from the preview's live telemetry.
2026-11-08 00:22:38 +01:00
Niko1221 38ab10e03e qsa_decode_attn: the q8_v3 value loop compiles for any heads-per-KV-head G
The Flash-Next branch (G == 12) sat in an if constexpr inside a non-template kernel, so its
12-element initialiser was compiled for every G and a build for another geometry stopped with
'too many initializer values'. It moves into a template helper (fold_cell_v3<G>); the
Flash-Next expressions are unchanged.
2026-11-08 00:08:08 +01:00
Niko1221 03dd20f82b app: load the screens after Chat, and the Settings and set-up dialogs, on first use
Models, Tuning, Serving, Monitor, Tasks, Skills, Settings and the first-run set-up load as their own files the first time they open; Chat stays in the first file.
The two dialogs open once their code is here (same first frame and focus as before); a routed screen keeps the old one on screen until the new one is ready (no new text).
Main JS file 3,123,528 -> 2,736,232 bytes; nothing else changes.
2026-11-08 00:02:20 +01:00
Niko1221 d3f696fe4e Tests: the df2ux launch-plan fixtures say NVIDIA too, not whatever Intel Arc the box has (2 failures on p1006) 2026-11-07 23:56:25 +01:00
Niko1221 3d26434f21 tools/: 27 untested, undocumented experiment scripts move to research/tools (3 folders, a README each saying what and when); two machine-local DFlash2 analysis scripts move to history/tools; documented, tested and packaged tools stay; tools/dflash2 files a training branch changes are left 2026-11-07 23:37:51 +01:00
Niko1221 ff6cd56cb3 root: chat.py and highlight.py move to tools/ (docs updated); the user entry points (setup, START-HERE, UPDATE, strata.py, BENCH.bat and bench.sh, which docs and setup name) stay 2026-11-07 23:37:51 +01:00
Niko1221 242e8f333e bench/results: an index page (one row per result folder, built by tools/bench_results_index.py and checked in tools/check docs); the results stay in place 2026-11-07 23:37:51 +01:00
Niko1221 b3f696ecad link check: tools/check_links.py for the current pages (a docs step in tools/check, a docs.yml workflow, a test), a baseline report for the archived pages, and the one broken anchor it found 2026-11-07 23:37:51 +01:00
Niko1221 8c743a697d history/: the finished UI audit-fix campaign, the dated app design documents, the Mac-first rebuild (docs/work) and its acceptance-matrix generator, each moved with a pointer left at the old path; open requirements copied to apps/host/docs/BACKLOG.md; host module map and contracts kept in apps/host/docs/README.md 2026-11-07 23:37:50 +01:00
Niko1221 de9c0448b2 docs: public speed and quality claims that cited private lab files now say they are internal measurements whose run logs are not published 2026-11-07 23:37:50 +01:00
Niko1221 bcee5f92c4 apps/host: remove the broken sync:kit command (its source folder is not in the repository); the checked-in kit styles and sprite are the source; a test keeps the two components.css copies equal 2026-11-07 23:37:50 +01:00
Niko1221 2baea39521 apps/host README: a developer start that works from a fresh clone (Node 24.12, npm ci, build, preview:ui, test, typecheck; preview, source host and packaged Control) 2026-11-07 23:37:50 +01:00
Niko1221 f19907ba77 capability table: one data file and generator for what each model family runs on per hardware, read against the 0.2.12 packages; AI_SETUP, AI_REFERENCE and MEDIA_KIT take their summaries from it (DeepSeek runs on a Mac) 2026-11-07 23:37:50 +01:00
Niko1221 e40597a066 release manifest: docs/data/release.json, a generator and a check for version lines and installer names (docs said 0.2.5 and 0.2.9; the release is 0.2.12 / 0.1.58) 2026-11-07 23:37:50 +01:00
Niko1221 fd5b3c4559 docs: Intel Arc source build needs oneAPI 2026.1 (2025.3 does not build the SYCL engine) 2026-11-07 23:37:50 +01:00
Niko1221 78f5417195 qsa_decode_attn: the q8_v3 kernel keeps Flash-Next's original probability loop 2026-11-07 23:25:52 +01:00
Niko1221 48f17ff35e fold_cell and gr_up_fast keep Flash-Next's original expressions (device code unchanged for the default shape) 2026-11-07 23:11:46 +01:00
Niko1221 e709a5ff44 native_dense_parity: also n_in 512, 1024, 2048, 3072 2026-11-07 22:59:54 +01:00
Niko1221 4e50c30a59 native_expert_parity: expert widths from Q4KQ_PARITY_EMBD / Q4KQ_PARITY_FF
The synthetic check was fixed at 2560 x 640.  Setting the two variables runs
the same GPU and CPU checks at other widths (512 and 1024 for the K-quant
downs a UD file carries when the width divides by 256).  Default unchanged.
2026-11-07 22:59:23 +01:00
Niko1221 fad95e2611 prefill: shared-expert and interleaved SwiGLU kernels use the geometry's expert width (were 640); STRATA_DBG_NAN also checks the shared expert, its gate and the weights; synth tool --set 2026-11-07 22:36:04 +01:00
Niko1221 741310713c setup golden: STRATA_HC_Q8 is no longer written into the config of Unsloth UD files on CUDA (the engine turns it on from the GGUF); five places, nothing else changes 2026-11-07 22:03:28 +01:00
Niko1221 b4e9ac7c29 fused_gr: the single-token down kernel takes its hidden row from dynamic shared memory when it does not fit 48 KB 2026-11-07 21:57:06 +01:00
Niko1221 c5d3f55dfe prefill: routed-expert count follows the geometry in the combine, route and peer kernels 2026-11-07 21:52:27 +01:00
Niko1221 f637d759b9 fused_gr down tile and verify/remote-expert top-k follow the geometry 2026-11-07 21:34:06 +01:00
Niko1221 6f379c8e7c generate.cpp: string literals of the PLE checks 2026-11-07 21:29:14 +01:00
Niko1221 caebc48411 generate.cpp: PLE constants declared at function scope, string literals fixed 2026-11-07 21:24:17 +01:00
Niko1221 f3e99e58d5 geometry: attention G, fused_gr tiles and fast up for hc_lr 256-512, top-k from the geometry in native_moe, native activation buffers follow n_embd/n_ff, PLE hash constants read from the model file 2026-11-07 21:21:16 +01:00
Niko1221 99807e8da4 Merge f-q4-kquant (d19ab7302): Metal and SYCL take Q5_K/Q6_K (Metal also Q3_K/Q4_K/IQ4_XS/IQ3_S) down experts for UD files at expert widths 512/1024; additive, today's sets unchanged (Qwen4 prep) 2026-11-07 21:21:00 +01:00
Niko1221 ddbfa1d5c8 Merge f-clean-1c (3c32937a2): launcher speed settings become engine defaults (parallel stack, GDN stack, gfx12 chunked scan, HC_Q8 on UD files, Metal fp16 for resident IQ3_S/Q2_0, hipBLASLt table on gfx1151 only); row 8 held (cleanup step 1c) 2026-11-07 21:13:39 +01:00
Niko1221 75822f3e9e generate.cpp: the expert layout is loaded for the compiled geometry's layer count 2026-11-07 20:59:19 +01:00
Niko1221 edee9ea256 q4geo_synth: expert tensors first in the file, as the expert pack needs 2026-11-07 20:54:32 +01:00
Niko1221 20f7f1ad0b q4geo_synth: a vocabulary and one merge the tokenizer step accepts 2026-11-07 20:49:22 +01:00
Niko1221 499d971666 q4geo_synth: the PLE tensors and keys (sparse table); ngram.hpp: the PLE head width follows the hidden size 2026-11-07 20:44:07 +01:00
Niko1221 d19ab73020 Metal: instantiate the K-quant / IQ down kernels only for a model that has them
Measured on the M5 Max (cold start, same parity binaries, kernel sources of
main against the branch): the extra instantiations cost moe_mm.metal's library
about +0.5 s on every start (5.7 -> 6.2 s) and moe.metal's about +0.65 s on a
cold shader cache.  A Flash-Next set should not pay for types it never uses.

The new instantiations (DN_SET IQ4_XS / IQ3_S, DNK_SET, MM_DN2_KQ and the other
down tiles, MM3_DN, MM6_DN1) sit under #ifdef STRATA_KQ_DOWN.  window.cpp
kq_down_prelude defines it when any layer's ffn_down_exps is Q3_K, Q4_K, Q5_K,
Q6_K, IQ3_S or IQ4_XS, at the three compile sites of moe.metal / moe_mm.metal
(decode window, prompt GEMM setup, prompt chain).  The parity tests do the same
from the down type.  For today's sets the kernel text is what it was.
2026-11-07 20:08:00 +01:00
Niko1221 6a71879979 Merge f-clean-2 (c94ef2375): drafting and plan test fixtures no longer read the box's real Intel GPU (9 failures on the Arc box) 2026-11-07 19:31:23 +01:00
Niko1221 3c32937a2b snapshot: re-made goldens after the launcher copies moved to the engine (nine cells lose STRATA_GDN_TC and STRATA_GDN_CONVL2 in every launch cell, and STRATA_HC_Q8 in the UD cells; nothing else changes) 2026-11-07 18:45:38 +01:00
Niko1221 d1121b8931 launcher: remove the copies of the settings the engine owns now (parallel stack, GDN stack, Q8_0 hyper-connection read, Metal fp16 windows, the packaged hipBLASLt table path); a config-level hipblaslt_table true/false reaches the engine as STRATA_HIPBLASLT_TABLE on/off; MMQ_KQ stays (row 8 is held) 2026-11-07 18:45:38 +01:00
Niko1221 88e6634390 Revert "engine: on AMD that is not gfx12, a UD-Q5_K_XL / UD-Q6_K_XL style file (most expert gate/up tensors Q5_K or Q6_K) runs the K-quant MMQ prompt kernels by default (STRATA_MMQ_KQ=1; was setup's unsloth_default_env; owner yes 11-07); STRATA_MMQ_KQ=0 turns it off"
This reverts commit ffb94c578c.
2026-11-07 18:40:47 +01:00
Niko1221 055274c882 Merge main b0fc432ed (step 4, step 2, 1A) into f-clean-1c
# Conflicts:
#	data/settings-register.json
2026-11-07 18:40:24 +01:00
Niko1221 2bc8c6692a Metal: K-quant, IQ4_XS and IQ3_S down experts (decode window and prompt GEMMs)
A UD file at an expert width divisible by 256 carries Q5_K / Q6_K down
experts (Qwen3.6 UD-Q4_K_XL: 38 and 3 layers); the Metal expert paths took
Q5_1 / Q5_0 / Q8_0 / IQ4_NL / Q2_0 downs only, and d_qk listed IQ4_XS with no
kernel.  Additive: new kernel instances and one new kernel template.

- moe.metal: moe_down_k (dn_body_k, XD<F>) for Q3_K / Q4_K / Q5_K / Q6_K -
  their decoders read the codes in place and need prescaled x and per-16 sums;
  the down's h is the raw f32 rows moe_gu wrote, so the kernel forms them from
  the raw row (exact power-of-two scalings, as xprep).  Same host names
  (moe_down_<type>_<L>_<R>_<S>), same arguments.  DN_SET for IQ4_XS, IQ3_S.
- moe_mm.metal: W4<Q3_K>; mm_down3 (MM3_DN) and mm_down6 (MM6_DN1) for the
  six types, mm_down2 tiles for the v2 (M4) path.
- iq_host.cpp d_qk: 21, 11, 12, 13, 14 (n_ff % 256).  prompt_chain.inc dn_ok
  and window.cpp dn2_tile: the new types.
- tests: expert_parity and moe_mm_parity take --ff / --embd and Q3_K.
2026-11-07 18:39:43 +01:00
Niko1221 3a8254d046 native_expert_parity: expert widths from Q4KQ_PARITY_EMBD / Q4KQ_PARITY_FF
The synthetic check was fixed at 2560 x 640.  Setting the two variables runs
the same GPU and CPU checks at other widths (512 and 1024 for the K-quant
downs a UD file carries when the width divides by 256).  Default unchanged.
2026-11-07 18:39:43 +01:00
Niko1221 cd14b976af SYCL: Q5_K (and Q6_K) down experts in native_expert_grouped
STRATA_D_FMTS had no K-quant: a UD file at an expert width divisible by 256
(Qwen3.6's UD-Q4_K_XL, 2048 x 512, Q5_K / Q6_K downs) was refused by
native_expert_supported on Intel Arc, as the CUDA list has not been since it
took Q5_K (Q6_K with STRATA_Q6K_EXPERTS).  The lane kernels are the gate/up's
(Fmt / KqSplit of 13 and 14); no kernel code changed, two list entries.

native_grouped_parity: --kqdown (K-quant gate/up x Q5_K/Q6_K down at hidden
2048/2560 x width 512/1024: split vs per-entry bitwise, GPU vs CPU reference),
also run by default; --hash (output checksums of the grouped call for every
pair of today's sets, to compare a build before and after a kernel change).
2026-11-07 18:39:43 +01:00
Niko1221 c94ef2375a Tests: the plan and adopt fixtures say NVIDIA, not whatever Intel Arc the box has (9 failures on p1006) 2026-11-07 18:32:52 +01:00
Niko1221 5e1ea3ff6e tools/q4geo_synth.py: synthetic qwen4exp GGUFs at any shape in the type mix of an Unsloth UD file, and the geometry header of a shape or a real file; routing trace layer bound from the geometry 2026-11-07 18:16:50 +01:00
Niko1221 4041af8e6c prefill, fused_gr, expert kernels: the shape constants are read from kCompiledGeometry (values unchanged for Flash-Next) 2026-11-07 18:08:12 +01:00
Niko1221 af7759b69e geometry: the compiled shape is a header chosen at configure time (STRATA_GEOMETRY / STRATA_GEOMETRY_FILE), with the constraints every shape must meet in one place 2026-11-07 18:04:24 +01:00
Niko1221 b0fc432ed9 Merge commit 'b8d604d28' into f-clean-1a 2026-11-07 17:25:04 +01:00
Niko1221 1300170273 Snapshot setup: setup's own helpers read WIN / EXE / VEXE of the simulated class, not of the host (the R9700 Linux cells) 2026-11-07 17:25:03 +01:00
Niko1221 b8d604d287 Merge f-clean-2 (e8c3acd2d): checks we can trust - CMake CPU preset and x86 link fix, tools/check, test portability, CI workflows (not pushed), bench timeout fix (cleanup step 2) 2026-11-07 17:22:06 +01:00
Niko1221 e8c3acd2d1 bench_standard: a socket TimeoutError is reported as timeout, like the timer (Windows reported connection_TimeoutError when the socket fired first) 2026-11-07 17:19:43 +01:00
Niko1221 391f7ce8dc tools/check: a module-level skip shows its reason 2026-11-07 16:40:41 +01:00
Niko1221 e2acc3a950 Snapshot setup: the simulated class decides the engine file suffix; source-text test follows the moved setup lines
The setup record took ".exe" from the host the test ran on (found by the Linux run: 12 Linux-class Qwen cells); ProbeLib now answers EXE / VEXE / engine_exe per class,
so only those 12 setup entries change (launch and app entries untouched).  test_dsv41_vision_reserve reads setup.py and strata_control/setup_config.py.
2026-11-07 16:35:26 +01:00
Niko1221 e62f3c1ffd Retire STRATA_DF2_GATE and STRATA_DF2_ROW_MS: the calibrated DFlash2 gate (cleanup 4.5 batch D)
Removes AdaptiveGate, df2_gate_on() and df2_gate_row_ms() (window_rule.hpp), their three
sites in generate.cpp and their block in window_rule_test.cpp. The opt-in gate measured
2.7-4.9% slower; with it off (the default) the window rule is unchanged. The register
marks both retired in 0.2.13.
2026-11-07 16:28:46 +01:00
Niko1221 64fac3177c Retire STRATA_GR_Q32_PL: the pipelined HC up-read arm (cleanup 4.5 batch C)
Removes the PL=true template arm of gr_up_q8_q32_kernel (fused_gr.cu) and the three
launch branches behind STRATA_GR_Q32_PL (HIP gfx12 only; no gain over STRATA_GR_Q32_G=2).
The default instantiation is unchanged. The register marks it retired in 0.2.13.
2026-11-07 16:28:46 +01:00
Niko1221 41b87a4bd2 Retire STRATA_METAL_ROUTE, STRATA_METAL_PARTIAL, STRATA_METAL_WPF_MIB: Metal decode variants (cleanup 4.5 batch A)
Removes the window.cpp branches of the fused router, the partial GDN commit and the
weight-prefetch dispatch, and the wpf kernel (gr.metal). The router_route kernel stays:
metal/tests/token_chain.cpp (its default) and metal/tests/fuse_parity.cpp still use it.
docs/MAC.md loses the row that described ROUTE and PARTIAL. The register marks the
three retired in 0.2.13.
2026-11-07 16:28:46 +01:00
Niko1221 dff2c91145 Retire STRATA_MMVQ_PF and STRATA_MMVQ_SG: SYCL mmvq tuning (cleanup 4.5 batch B)
Removes the aligned weight-load prefetch kernel of the wide Q6_K mmvq and the
SIMD16 sub-group variants of the decode mmvq (sycl/src/kernels/cuda/native_mmvq.dp.cpp).
Both measured no gain in the engine (PF=2/3/4/6 slower everywhere; no SG value beats
SIMD32 beyond noise). Default settings run the same kernels as before. The register
marks both retired in 0.2.13 and the line numbers of the register are rescanned.
2026-11-07 16:28:46 +01:00
Niko1221 cf0161bfb3 Give the two standalone drivers optional build targets (cleanup 4.4)
src/dsv41/sampler_stat_main.cu -> target ds4_sampler_stat (CUDA, inside the
STRATA_BUILD_DSV41 block, EXCLUDE_FROM_ALL): the DeepSeek sampler statistics
harness that the Metal engine and `strata-dsv41 --sampler-test` compile from
the same sampler_stat.inc.
src/split/tools/selftest_main.cpp -> target strata-split-selftest (HIP,
inside the split engine's CMake, EXCLUDE_FROM_ALL): the split engine's kernel
self-test on one card, with no model. Its header cited a build script in a
private test folder; it now names the target.
Both are built by name only: nothing in the default build changes.
2026-11-07 16:28:46 +01:00
Niko1221 5d60cf13f3 settings_register --check: a retired entry (retired_in set) is not an orphan
The test already allowed it; the command line check listed it as 'in the register, not read by the engine'.
2026-11-07 16:28:41 +01:00
Niko1221 506f65781e Remove unused imports reported by Ruff F401 (cleanup 4.3)
60 files, 94 names: strata_control, strata_cli, serve, integrations/harness,
metal/setup_mac.py, packaging/make_local_feed.py, sycl/tools/fixups.py and the
tools/ scripts. Left alone: test files and tests/, bench/results, __init__.py
re-exports, apps/host/audit-fixes (moving in 3.5), tools/dflash2/final_tables.py
(its exec() uses the namespace), tools/nvfp4_verify_gguf.py (torch) and the
platform import in strata_control/doctor.py (tests patch doctor.platform).
2026-11-07 16:28:41 +01:00
Niko1221 7b52ab055c Tests: the not-preallocated check is the full size; tools/dflash2/test_port.py skips without its lab files 2026-11-07 16:12:41 +01:00
Niko1221 0ea3b73145 tools/check leaves CI alone; the Git Bash launch tests set their own CI/STRATA_NO_PROMPT; the optional suite needs gguf as well as torch 2026-11-07 15:05:01 +01:00
Niko1221 af9f44e522 metal: a resident IQ3_S or Q2_0 pack (found from the GGUF file name) runs the fp16 decode windows by default (STRATA_METAL_F16 unset; was mac_fit.engine_env; owner yes 11-07); STRATA_METAL_F16=0 turns it off, =1 turns it on for any pack 2026-11-07 15:00:01 +01:00
Niko1221 ffb94c578c engine: on AMD that is not gfx12, a UD-Q5_K_XL / UD-Q6_K_XL style file (most expert gate/up tensors Q5_K or Q6_K) runs the K-quant MMQ prompt kernels by default (STRATA_MMQ_KQ=1; was setup's unsloth_default_env; owner yes 11-07); STRATA_MMQ_KQ=0 turns it off 2026-11-07 14:55:49 +01:00
Niko1221 567c46873f engine: on CUDA a GGUF that carries the hyper-connection projections as Q8_0 (Unsloth UD-* files) reads them as Q8_0 by default (STRATA_HC_Q8=1; was setup's unsloth_default_env; owner yes 11-07); STRATA_HC_Q8=0 turns it off 2026-11-07 14:55:06 +01:00
Niko1221 0a50eda160 engine: on Linux with every card gfx12 the chunked GDN scan is on from the first chunk (STRATA_GDN_CHUNKED=1 by default; was the launcher's GDN_STACK_GFX12; owner yes 11-07); STRATA_GDN_CHUNKED=0 turns it off 2026-11-07 14:54:00 +01:00
Niko1221 6858c919ca engine: STRATA_GDN_TC=1 and STRATA_GDN_CONVL2=2 are the Linux CUDA engine's own defaults (was the launcher's GDN_STACK_CUDA; owner yes 11-07); =0 turns each off 2026-11-07 14:53:32 +01:00
Niko1221 6bb7e7a2a3 Move the unused reserve proposal out of stage_reserve.hpp (cleanup 4.2)
proposed_window_mib / proposed_reserve_mib and their two constants go to
research/stage-reserve-proposal/ with a README and their own checks; the
production header keeps reserve_mib / room_bytes, and stage_reserve_test keeps
its real-arithmetic checks unchanged.
2026-11-07 14:17:30 +01:00
Niko1221 0029379234 Remove the three permanently excluded SYCL #if 0 blocks (cleanup 4.1)
iq_kernels.dp.cpp: the 997-line AMD-tuned S26/S27 grouped-expert block
(canonical code: src/kernels/cuda/iq_kernels.cu).
native_qsa_score.dp.cpp: the 38-line inline-PTX load_a/load_b/mma block
(canonical: src/kernels/cuda/native_qsa_score.cu; the port scores with the
scalar path).
generate.cpp: the 28-line #542 CUDA-runtime mismatch warning (canonical:
src/program/generate.cpp).

Each leaves a one-line comment. sycl/tools/fixups.py gets entry 14, which
removes the same regions after a re-migration (idempotent, depth-aware), and
the comments that named sycl/migrate.sh now name sycl/tools/migrate.sh.
2026-11-07 14:17:30 +01:00
Niko1221 456b07cbe7 backends.yml: SYCL job on oneAPI 2026.1 from Intel's apt repo (the engine does not compile with 2025.3.2) 2026-11-07 14:12:13 +01:00
Niko1221 343b8eae6c hip prefill: the engine takes the hipBLASLt table beside it by itself on gfx1151 only (the owner's 2026-10-19 rule); another card needs STRATA_HIPBLASLT_TABLE=on; the launcher's off words are read the same way 2026-11-07 14:11:34 +01:00
Niko1221 c9abd38a7f engine: the exact parallel-request stack is the Linux CUDA engine's own default with 2 or more batch slots (was the launcher's PARALLEL_STACK_ENV); each switch keeps =0, a value you set is kept 2026-11-07 14:09:51 +01:00
Niko1221 1015f5515d Merge f-clean-step0 (01f9c62a0): the config snapshot tool and its golden files (cleanup step 0) 2026-11-07 14:06:11 +01:00
Niko1221 cb012aa5df Tests: a user named 'user' in the redaction test; tools/trellis scripts are not collected by pytest; tools/check not-run text 2026-11-07 13:54:23 +01:00
Niko1221 96b45a8415 Tests: setup_unsloth pins the cards' speeds; host tests name the reason they skip
tools/test_setup_unsloth.py: setup.card_speeds asks the CUDA driver of the machine that runs the test, so which fake card was Main depended on the box (4 failures on a box with no NVIDIA driver); the fixture says card 0 is the faster.
apps/host/tests: seven tests that skipped with 'true' now skip with a string that names the OS or the variable they need.
tools/check: a skip with no reason is listed with the test's name.
2026-11-07 13:44:49 +01:00
Niko1221 7ce44a1fd3 tools/check: --shard I/N for machines with short job limits 2026-11-07 13:32:14 +01:00
Niko1221 118e20757c Tests: fixtures stop reading the machine they run on (Windows, Linux, a Mac's /var)
Tests and fixtures only, no production code:
- markdown-crlf: the long text goes to the render child in a file, not argv (Windows cuts a command line at 32 KB).
- component-updates: the packaged-update test writes the program file under the name of the platform it runs on (strata-desktop.exe only on Windows).
- connector-setup: the shared-download test pins the installer's look for an installed cloudflared to an empty folder and waits for the request with a deadline; it hung for ever on a box that has cloudflared in PATH.
- chat-files: folders are made under the canonical temp path (a Mac's /var/folders is a refused system folder, /private/var is not).
- test_tester_readme: the version is read from Cargo.toml, bash is found by full path and given C:/ paths (Windows runs the WSL launcher for a bare 'bash').
- test_docker_entrypoint: the never-overwritten case sets ALLOW_DOWNLOAD=1 (the guard for older model files stops the start otherwise), plus a test of the guard.
- test_setup_download: the whole-file GET that Python before 3.13 sends after the HEAD redirect is not a piece; the not-preallocated bound is the full size.
- Windows: split engine and DeepSeek fit fixtures say Linux and NVIDIA (Runner.intel_chosen, sys.platform, _on_windows) instead of reading this PC; setup tests no longer download the engine; path flavour in mac/pictures; utf-8 for a help text; the sh stand-in demangler is skipped on Windows with a reason; update.lock cleanup; private files under STRATA_HOME; a Fake process with kill().
2026-11-07 13:31:45 +01:00
Niko1221 d8db7ced36 The four owner-approved strings (11-07), word for word
1. strata launch --help: "dflash2 (downloads its drafter once, 0.3 to 1.2 GB depending on the model; the size is shown before the download)"; the docs row: "...and downloads its drafter once (0.34 GB for the models that have a tuned drafter, 0.52 GB on Intel Arc, else 1.2 GB)".
2. The Arc launch line ends "No model files were downloaded." (was "Nothing was downloaded.").
3. After "Drafting: off (no drafter is downloaded yet)", only when a recent miss mark exists and the choice is DFlash2 or Auto on a tuned set: "Run strata drafting fetch to get the DFlash2 drafter now."
   (drafting.fetch_hint, printed by the server's start; the miss mark and its 12 h window move to drafting.py, drafting_fetch keeps its names).
4. A bare strata drafting fetch with several DFlash2-capable models set up: "Several models are set up here. Say which one: strata drafting fetch --model <name>, or --config <file>."
   (exit 2, nothing downloaded; before, it fetched the stock drafter).
2026-11-07 13:30:48 +01:00
Niko1221 a37d0a57ef One decider: the app host's speed-switch table and Drafting families are generated from strata_control; the rules run shared vectors
strata_control/app_policy.py writes apps/host/server/policy-generated.mjs (the four app speed switches with their words, defaults and environment entries, from
speed_switches.py; the Drafting families and the model-name rule, from drafting.py).  native-settings.mjs imports it instead of carrying its own copy
(byte-identical values, checked against the old table).  tests/terminal/test_app_policy.py fails when the committed file or the vectors are out of date; the host test
runs speedSwitchApplies / resolveDrafter (host and screen) over policy-vectors.json, which the Python writes from its own answers (240 switch cases, 12 names, 7 families).
GPU roles already run shared vectors (tests/data/gpu_roles_vectors.json); the request-slot default stays the app's own (recorded in tests/snapshot/README.md).
No default changes.
2026-11-07 13:30:44 +01:00
Niko1221 93a1e18bce The app's plain Load follows Auto everywhere: it fetches whatever Control's Auto plan says is needed (standard DFlash2 drafter included)
fetchAutoDrafter and the plain-Load step gate on the plan's `needed` (the Auto decision's drafter is not on this computer) instead of `tuned_needed`, so the
stock-drafter cases (Intel Arc, UD-Q4_K_XL on NVIDIA 72 GB / 12 GB and the Spark) get the drafter the Terminal's Auto uses.  The decision stays Control's.

Snapshot: the app_plain goldens are re-made (18 cells changed against the stored main goldens: the 12 tuned cells the df2ux commit fixed and the 6 stock-drafter cells
this one fixes) and test_entry_points_agree.py pins app_plain == launch and app_models == launch + `drafting: auto` in all 32 cells apart from the allow-list in
tests/snapshot/allowlist.py.  With DFlash2 the NVIDIA 72 GB plain Load writes no `parallel` and no --batch-mtp (request slots: recorded in the README).
2026-11-07 13:30:38 +01:00
Niko1221 b6769b7f9a GitHub Actions: fast checks, config snapshot and backend compile-only workflows
check.yml: tools/check steps as one job each on Linux and Windows (a macOS job that reports and does not block until its first green run), path filters that include the shared contracts and packaging/.
snapshot.yml: the config snapshot on Linux and Windows.
backends.yml: CUDA 12.9 and 13.0, HIP (ROCm 7.2) and SYCL (oneAPI 2025.3) compile-only in the vendors' containers, Metal strata and strata_ds4 on a macOS runner; llama.cpp at the commit pinned in setup.py, ccache caches. The existing workflows are unchanged.
2026-11-07 13:25:01 +01:00
Niko1221 b6f585164c tools/check: one command for the fast suites, with an explicit not-run for a missing tool
python tools/check runs the CPU CMake preset (configure, build, CTest), the Python suites (tests/, serve/, tools/test_*.py, and the torch-only tools folders when torch is installed), the host's typecheck, test and build, and the config snapshot. Each step has its own timeout, runs with temp files, build tree and logs in a scratch folder, and the table lists passed / failed / skipped / not run with the reason for every skip and every not-run step. Exit 1 on a failure or a timeout; --strict also fails on a step that could not run.

requirements-dev.txt pins pytest and textual apart from the runtime requirements. apps/host/scripts/test.mjs gives node --test a per-test timeout (STRATA_TEST_TIMEOUT_MS, default 10 minutes) and passes extra arguments (a reporter) through.
2026-11-07 13:24:50 +01:00
Niko1221 8feca419ff setup seam: config_for / dsv41_config_for in strata_control, and the setup entry point in the config snapshot
The lines of setup.py main() between the model arguments and write_setup_config() (Qwen / Unsloth) and the same stretch of install_dsv41() (DeepSeek)
move, unchanged, to strata_control/setup_config.py.  setup.py builds the inputs and calls them; the helpers and constants still live in setup.py and are
reached through `lib`, so a caller can answer the machine probes.  The snapshot drives them for NVIDIA, Spark, Strix Halo and R9700 (Intel Arc and Mac
setup are other scripts and are recorded as not driven).  Existing launch / app goldens are untouched; only the setup entries are new.
2026-11-07 13:09:53 +01:00
Niko1221 e396ce821f CMake: CPU check preset, tests on by what they need, x86 test programs link without native experts
- CMakePresets.json: cpu-check (no GPU, no network), cpu-check-native, cuda-compile and hip-compile (the release flags, no tests).
- STRATA_BUILD_TESTS defaults on for x86 when the test sources are there (it looked for tests/CMakeLists.txt, which does not exist); the release scripts say OFF themselves.
- With STRATA_NATIVE_EXPERTS=OFF on x86 the kernels library now carries q2_avx1.cpp, which expert_layout.cpp calls: 7 programs on Linux and 12 on MSVC failed to link with undefined *_avx1. No change with it ON.
- expert_parity and pool_test need pack/full/experts.bin (a model file): a stated skip when it is not there, the same test when it is.
- A configure line says which kinds of test were registered; the stale phase-1 header and the 'Linux + CUDA only' split note are corrected.
2026-11-07 13:09:13 +01:00
Niko1221 6ea300dde5 Settings register: every STRATA_* setting the engine reads, with a scanner, a test and a draft settings page
tools/settings_register.py scans src, include, metal/src and sycl/src for the 1,009 STRATA_* names read through getenv and the env helpers
and writes data/settings-register.json: backends, read sites, parse kind, code default where it can be read off the call, the engine's
own arch default tables, what the Terminal, the app and the server set, doc mentions, first and last commit and the first release. The
status, owner, note, retire_after and retired_in fields are hand-kept and survive regeneration.

tools/test_settings_register.py fails when the engine reads a name that is not in the register, when the register lists a name no code
reads (unless it is retired with a version), and when an entry has no valid status or a retire-candidate without a file:line citation.

docs/SETTINGS.md is generated from the register (opt-in and rollback entries only) and is a draft: new visible wording, for review.
No product code is changed.
2026-11-07 13:09:12 +01:00
Niko1221 daf035a8c1 Merge f-clean-step0 (config snapshot test) 2026-11-07 12:51:09 +01:00
Niko1221 be32c7f3fb Merge branch 'f-clean-step0' into f-clean-1a 2026-11-07 12:50:52 +01:00
Niko1221 01f9c62a0a tests/snapshot: golden files of main 146df3c46 (identical when made on Windows and Linux) 2026-11-07 12:37:56 +01:00
Niko1221 affa7d96e7 tests/snapshot: the simulated machine decides the Linux-only stack switches; state folder name normalized 2026-11-07 12:31:42 +01:00
Niko1221 4a5a6ece70 tests/snapshot: drop provisional goldens 2026-11-07 11:09:30 +01:00
Niko1221 c10b746970 tests/snapshot: config snapshot tool (driver, matrix, app driver, goldens pending) 2026-11-07 11:08:56 +01:00
Niko1221 146df3c468 setup.py run(): the app's installer also runs the bundled Mac preparation tools in metal/tools (metal_repack.py, stream_experts.py); a fresh Mac Terminal or app install of UD-Q4_K_XL or UD-IQ4_XS-v31 stopped with "only runs its bundled preparation tools" since af1ffd913. Test: they run, and -m, another interpreter and other folders are still refused 2026-11-07 01:29:36 +01:00
Niko1221 9b02c62ab5 DFlash2 tuned drafter: plan and size follow the drafter Auto really fetches; every app load path fetches it
- drafting plan --model (+ tuned, drafter_gb, tuned_needed, file); mtp or nothing needed says so; launch --plan sees a model not installed yet and a stock pack from 0.2.11
- drafting fetch --throttle (12 h rule, silent when the feed is not there); a bare fetch uses the one installed model's run config
- app: plain load, restart of stopped models and tuning run fetch the tuned drafter when Auto resolves to it; install plan passes the model; Drafting hint number from the plan
- launch plan text shows 0.34 / 0.52 GB, not 0.3 / 0.5
2026-11-07 01:09:42 +01:00
Niko1221 9780a8c27d Merge main (54828c7ef): four sponsors in Loyal supporters, all 8 READMEs 2026-11-06 21:53:35 +01:00
Niko1221 311926416a Merge f-0212-tools (ea765a571): tool-call fixes in serve/ and the native gateway, with their tests and docs 2026-11-06 21:53:34 +01:00
Niko1221 0fe4ae5090 qsa_select tp_select: no __match_any_sync below sm_70 (a plain add per thread there; integer atomics, the same sums): the CUDA 12 release engine (sm_60..89) builds again 2026-11-06 21:39:07 +01:00
Niko1221 ea765a5717 App gateway: an unknown model name on /v1/messages uses the one loaded model (Claude Desktop sends claude-5-5-sonnet); chat and Responses stay exact, and with several models loaded the 400 stays (#5312) 2026-11-06 19:55:58 +01:00
Niko1221 c49f198c77 Test: the Anthropic route serves the loaded model whatever name the request carries (#5312, claude-5-5-sonnet) 2026-11-06 19:51:51 +01:00
Niko1221 b1541ab245 Parser: the prefixed-name and misspelt-opener checks look only at the end of the buffer (the new text and 256 characters before it), so a long call is not rescanned with every token (#5409, #5276) 2026-11-06 19:48:21 +01:00
Niko1221 8a42a9457d moe_iq4_w4a8: the device side compiles for sm_60..75 targets (a trap there, as moe_fused_iq.cu does; arch_ok() already declines those cards, so no behaviour changes): the CUDA 12 and CUDA 13 release engines build again 2026-11-06 19:42:58 +01:00
Niko1221 05506f9532 Tool names: a namespaced MCP tool written by its last segment (get_asset_info for mcp__assets__get_asset_info) is that tool when exactly one declared tool ends that way (#5281) 2026-11-06 19:40:40 +01:00
Gabriel A. Devenyi 95381770f8 Anthropic tool results follow tool_use_id: results sent in the order the tools finished are put in call order before the template pairs them by position (PR #4832) 2026-11-06 19:36:59 +01:00
Gabriel A. Devenyi d7e555bdbb tool_choice "required" with several tools: write only "<tool_call>\n", because from a written "<function=" the model went on with names no tool has and the reply came back as text (PR #4828) 2026-11-06 19:36:43 +01:00
Niko1221 75acfca321 Config undeclared_tool_calls (env STRATA_UNDECLARED_TOOL_CALLS): "call" returns a well-formed call of a tool the request did not declare as a tool call; the default "text" is as before (#4622) 2026-11-06 19:36:04 +01:00
Niko1221 63fd759227 Tool calls: a misspelt opener (<toolcall>, <tool-call>) followed by <function=NAME> of a declared tool opens the call, where a <tool_call> would; prose and code stay text (#5276) 2026-11-06 19:34:47 +01:00
Niko1221 54828c7efb README: Loyal supporters adds themactep, macintog, bisonbet and vizv (public GitHub sponsors) in all 8 languages 2026-11-06 19:33:46 +01:00
Niko1221 68ab15ddc1 Tool names: <function=tools NAME> (a space for the dot) is the declared tool NAME, and a bare <function=tools> is the one declared tool that takes the call's parameters (with tool_call_recovery); anything else stays text (#5409) 2026-11-06 19:32:59 +01:00
Niko1221 7be42963e6 Version 0.2.12 / engine 0.1.58: Terminal and app 0.2.12, Control and every engine package 0.1.58 (the 0.2.11 bump list, plus the v0.1.58 line in the setup.py version history) 2026-11-06 19:21:19 +01:00
Niko1221 d4b72bc901 feed models: the owner's words and warn_allow policy, a family-menu component with a test, a launch model-screen test 2026-11-06 19:21:00 +01:00
Niko1221 37e91bfdbb strata_tokenizer: the approved wording for an unknown pre-tokenizer 2026-11-06 19:19:52 +01:00
Niko1221 8221fb8c1c feed models: read the model host off setup's url builder so strata_control names none (test_the_runtime_has_no_model_host_in_it) 2026-11-06 17:59:13 +01:00
Niko1221 5b1728358b test_download_integrity: a server that keeps serving the wrong file ends with the file kept as .bad (the dlsha behaviour: the new mismatch line, the file not deleted) 2026-11-06 17:43:19 +01:00
Niko1221 e17394fe1c serve/test_tool_format: the real Qwen3 chat template selects the JSON parser 2026-11-06 17:31:44 +01:00
Niko1221 9bbbb66061 packaging/make_model_release.py: plan a model release as a dry run
From the intake report it prints, in the order of the 0.2.11 release, what
would be built, signed and uploaded, with sizes from the previous upload
folder: case A is the feed (a models entry, re-signed at the next sequence)
and the app; case B adds the Qwen engine packages and Controls at a new
version; case C is refused. Steps that need the release key print a
placeholder; the script has no key, build, upload or publish code, and
--execute is refused. --write also writes the plan, the model's DRAFT feed
entry and the release notes, model card and checklist filled from the report
with measured numbers left as [FILL: ...].
2026-11-06 17:31:10 +01:00
Niko1221 a123ba696c feed models: the Mac setup paths, a row for a model the engine does not run (Terminal, app), UNAVAILABLE_POLICY switch 2026-11-06 17:30:11 +01:00
Niko1221 e7d4844637 serve: pick the tool-call parser from the chat template
serve/toolformat.py names the tool-call form a template documents (xml, json,
dsml, none, other). parser_for_template() gives JSON templates (Qwen3 / Hermes)
JsonOutputParser, which reads the JSON call as the template's own form whether
or not tool_call_recovery is on; every other template keeps OutputParser, so
the models that run today get the class they always had. Service takes it from
its template, set_family still picks DSML for DeepSeek.

The server stops at start, with one line and exit status 2, on a tokenizer pack
whose pre-tokenizer it has no rule for. serve/tool_parser_golden.json holds the
events of both parsers for fixed outputs, fed whole and in pieces.
2026-11-06 17:25:06 +01:00
Niko1221 7084a49b7b tools/qwen4_intake.py: read GGUF headers and say what a new model needs
One command on a folder, GGUF files or a Hugging Face repo (range reads of the
headers only). Per set of shards it reports the architecture and every geometry
key against qwen4exp's compiled shape and qwen35moe's limits, the tensor layout
against the family_layout.hpp tables (missing, reshaped, unknown), each quant
type by role against the CUDA / HIP / SYCL / Metal kernel lists, the tokenizer
and chat template (tool-call form, thinking tags), MTP, picture reader, split
and sizes, and a case A / B / C verdict with a DRAFT catalog entry and setup
config. serve/toolformat.py holds the tool-call form detector.

The tables are ports of the C++ and kernel sources; the tests read those files
and fail when a table drifts. Fixtures are real headers recorded from Hugging
Face (Flash-Next sets, Qwen3.6, DeepSeek, picture readers).
2026-11-06 17:22:07 +01:00
Niko1221 1e88e88c5f strata_tokenizer: refuse a pre-tokenizer type it has no rule for
An unknown tokenizer.ggml.pre used to fall back to the qwen35 splitting rule
without a word, which would change every prompt for a model with its own rule.
pre_tokenizer_kind() names the rule (qwen35, deepseek3) and Tokenizer raises
UnknownPreTokenizer (a ValueError) for anything else. The types accepted before
(qwen35, empty/None, deepseek*) build and split as they did.
2026-11-06 17:21:51 +01:00
Niko1221 c152be538c setup: read the feed's model list for an unknown --model only when it is not a built-in alias or a DeepSeek size 2026-11-06 17:21:16 +01:00
Niko1221 aae7721b6d feed: an optional models member (a model list in the signed feed): strict parsing, the cache of the last verified list, feed models in setup's tables, build-feed --models, verify-feed 2026-11-06 17:18:42 +01:00
Niko1221 706efc5b0f prefill_fused_iq_test: skip the Q5_0 down pairs where the native kernels do not cover them (CUDA has none; the gfx12 / gfx1151 builds run them); --only=NAME still insists 2026-11-06 16:29:26 +01:00
Niko1221 0a0e4463ea file tier: the re-check after the RAM copy counts only the page cache the group can really have (on by default; STRATA_FILE_TIER_HONEST=0 turns it off)
I/O path only: the same bytes are read, only HOW the experts outside the RAM copy are read (unbuffered or through the file cache) is chosen differently.

file_tier_unbuffered took its `avail` from cgroup_available_bytes, which credits the group's whole `file` counter as reclaimable. On a RAM-budget
config that credit includes pages the engine holds itself: P2003 (RTX 3060, 46 GB, 36 GiB group), UD-IQ4_XS-v31, 22 GiB RAM copy: the re-check read 18.7 GiB
available when the group had 11 GiB left (limit 36, current 24.8). With held = 0.52 >= 0.5 the timed drive probe was skipped and the file tier switched to the file
cache (direct reads 7.9 GB/s vs 2.5 GB/s cached): prompts 2.5-3x slower (pp2k 85 vs 273 tok/s, pp8k 195 vs 580), decode 10-14% slower. The one start that read
12.0 GiB kept the fast mode, which is why the first start of a session was sometimes fast.

Now the smaller of that figure and the conversation cache's sampler (only the clean inactive file cache is credited, the group's own limit, no operator
cap) decides. The --memory-limit-mib cap is not applied: it bounds the engine's own allocations, not the page cache (counting it sent IQ3_S unbuffered and cut its decode
2-3x). A flip now prints once: "strata: expert reads now go through the file cache (X GiB of RAM free for them, Y GiB limit)"; the technical line is behind STRATA_TRACE.
STRATA_UNBUFFERED_LOAD still forces either mode.

Measured on P2003 (build wip11, n >= 3 starts per arm, interleaved fresh engines, I/O-quiet before each start, memory watch on, floor 14336 MiB):
 v31 unforced: main flipped in 5 of 6 starts, this change 0 of 9 (6 + 3): pp2k 85-94 -> 263-273 tok/s, pp8k 187-198 -> 580-590, code_py decode +14 to +20%;
 v31 DFlash2 vs MTP in the kept state (3 rounds): DFlash2 +2 to +14% per kind (json_extract -3.5%);
 IQ3_S: the file cache stays (9 of 9 starts), decode level with main (code_py 17.0 vs 15.6 tok/s, json_extract 14.2 vs 13.4, page cache evicted before every start);
 low RAM (6-7 GiB ballast, budget 16 GiB): no regression, MemAvailable minimum 14911 MiB;
 memory minimum with the unbuffered state 15.6-15.8 GB (floor 14.3 GB), no arm stopped.
Not P1.8: main and the P1.8 build flip alike.

(cherry picked from commit be3831e9d944fb44ac36d176f4764e30df0f9f6b)
2026-11-06 15:51:21 +01:00
Niko1221 e77bd43bc9 prefill: a Q5_0 down (UD-IQ4_XS-v31: 33 of 48 layers) on the fused int8 kernel on gfx12, opt-in STRATA_PF_FUSED_Q50=1
native_w11_kernel takes a Q5_0 down block (d, qh, qs[16]; 22 bytes): the 5-bit codes are rebuilt with the qh bits and kept as the signed int8 q - 16 (no minimum
term), the scale is d.  d_covered_on() says yes for T_Q5_0 only on gfx12 with the switch; the layer otherwise leaves the fused path for the gather + FP16
dequantize + hipBLASLt route as before.  hip_prefill_fused_iq gets the pairs IQ3_XXS / Q5_0 and IQ4_XS / Q5_0 (reference-checked: the build has no Q5_0 MMQ).

R9700 (p620, v31, DFlash2, same executable with and without the switch, 3 rounds x 2 reps): prompt 2K +44%, 8K +40%, 32K +13%; decode unchanged.
First-token KL against the FP16 route (self-variation 0): 2K mean 0.016, max 0.068, top-1 24/24.  Off by default in this commit.

(cherry picked from commit 97c3033f566d27ea1d741d7b893ac13e0f4e8526)
2026-11-06 15:48:47 +01:00
Niko1221 37b5cf0db2 gfx12: the start-up line for the length-gated chunked GDN scan, and STRATA_GFX12_DEFAULTS=0 turns the gate off
"strata: RDNA4 (gfx12): prompt chunks of 30,000+ tokens use the chunked GDN scan (STRATA_GDN_CHUNKED=0 turns it off)" is printed once at start with the
other gfx12 default lines (owner's wording, 11-06): only on gfx12, only while STRATA_GDN_CHUNKED is unset and STRATA_GFX12_DEFAULTS is not 0.
The gate follows the gfx12 defaults' one off switch, so the line is true whenever it is printed.

(cherry picked from commit b2d1680a864195383697559edb9b6cf8180b9a19)
2026-11-06 15:47:44 +01:00
Niko1221 c0e7a6f4f8 gfx12: the chunked GDN scan is the default for prompt chunks of 30,000 tokens or more
STRATA_GDN_CHUNKED unset on a gfx12 card (RDNA4): the FP16 matrix-core chunked recurrence runs for prompt chunks of 30,000 tokens or more
(--prefill auto takes chunks up to 32,768 there, so a 32K prompt is one chunk of about 31,996 rows).  Shorter chunks keep the kernels they
ran, so every prompt up to 20K is bit for bit what it was.  STRATA_GDN_CHUNKED=0 turns it off, =1 is the old opt-in (every chunk of 64
tokens or more), STRATA_GDN_CHUNKED_MIN=N moves the line.  Owner OK 11-06.

R9700 (p620, IQ3_S, DFlash2, interleaved pairs against the same tree without the change, 5 rounds x 2 reps): prompt 32K +6.1% (5/5),
64K +6.7% (5/5).  First-token KL against the unchanged build (self-variation 0.00000): all 24 prompts of 2K, 8K, 20K and the 8K agent turn
identical; 32K 0.00005, 32K+ 0.00004, 64K 0.00086; top-1 equal on all 30 prompts.  No log line yet (wording is with the coordinator).

(cherry picked from commit 7b579367aaa56978232cb60ef405987a199d736f)
2026-11-06 15:47:44 +01:00
Niko1221 645b9177e3 setup: wording for the kept .bad file (NEEDS OWNER APPROVAL): the mismatch line says the file was kept, and says when the server sends the very same bytes; one line when the kept file is removed for room
(cherry picked from commit ae3c3bad63c23b2b51c72f232832d2db195ef72c)
2026-11-06 15:47:44 +01:00
Niko1221 071e4dc51e setup: a downloaded file with the wrong SHA-256 is kept as <name>.bad instead of deleted; the next run hashes it again and takes it back if it matches, a good new download or too little room removes it (visible text unchanged in this commit)
(cherry picked from commit e547295924d5cb560febafca6d1cbadeed33d2b9)
2026-11-06 15:47:44 +01:00
bulhenvaster bca7931f3d peer tier: STRATA_ADAPT_OVERLAP=1 with --peer-device (the peer's copies land while the next window runs)
In peer mode the adaptive overlap was refused, so every decode window first waited for the previous round's copies. With the overlap the peer
tier issues its copies in layer order, signals each layer's landing the way the primary does (a stream-ordered D2H copy of the layer number into
a pinned int, or host functions), its residency table takes the swaps at the window's start (flip_pending), and a layer waits for a tier's copies
only when that tier swapped in it (AdaptGate: peer_landed, peer_has, per-tier last). The wait before a window clears the peer's marks with the
primary's. Without the variable nothing changes.

Ported from #2720 (bulhenvaster), rebased by hand onto the current gate (the watchdog label stays). Not ported: STRATA_PEER_LAUNCH_IN_POOL
(the author measured no gain on his build), the new decode timing line and the documentation paragraph.

(cherry picked from commit e78b64a7348d685dfd11799fdb395661f2120436)
2026-11-06 15:47:43 +01:00
bulhenvaster 2b9f05093d prefill: STRATA_PF_PEER_CPU=1 (opt-in) runs the CPU share beside a peer GPU
With --peer-device the prompt path never gave the CPU pool a share (cpu_maybe required !m.pp), so a short read at long context streamed every
expert neither card holds over the primary's link while the CPU sat idle. With the switch the share runs beside the peer: it takes only experts
neither card computes, its rows go between the primary's and the peer's block (the peer's stays the tail its combine reads from back_at), and the
staged chunks go up to cpu_share_max()'s 3072 tokens. Without the switch nothing changes.

Ported from #2721 (bulhenvaster), rebased by hand onto the current prompt path (the default-armed share's limit and the routed-stream gate).

(cherry picked from commit c8b6c43c1f2653636d15fe14abf53112357570aa)
2026-11-06 15:47:43 +01:00
sergqwer 24c6c2f682 Admit the PCIe share's fetched misses to the GPU cache (opt-in STRATA_ADAPT_FETCH=1|2)
Decode reads missed expert blobs from host RAM three ways; the PCIe share's
blobs cross into VRAM anyway and were dropped after the window. With the
switch set, the share takes each layer's most-routed misses and a fetched
expert routed enough more than the layer's least-routed resident expert that
the window does not route is copied straight into that expert's slot
(fetch_blobs gets an optional dst2 target per blob). =2 leaves the tier's own
swaps out of a layer where an admission ran. STRATA_ADAPT_FETCH_VERIFY=1 reads
every admitted slot back. Off, with a log line saying why, where it cannot run.
Unset changes nothing.

Ported from PR 2964 (sergqwer) onto f-0216-core; the strata generate verify
loop was merged by hand (main's speculative-q lines kept) and the DETAILS.md
note became a row of the opt-in switch table.

l620 (RTX PRO 5000, IQ3_S, 3,896-slot cache, --pcie-frac 0.25): decode =2 +10.2%,
=1 +5.0% (4 rounds); token forcing over 14,167 tokens: mismatch 2.73% vs 2.53% /
2.53% for two unset arms (+0.19 pt, 90% CI -0.02..+0.42), PASS.

(cherry picked from commit b2bfaef906acee4f86bdf9f052aee6a4d295fb4e)
2026-11-06 15:47:43 +01:00
bitaccesscomau 386f9959f1 Exact two-pass QSA prompt selection (opt-in STRATA_SELECT_TWO_PASS=1)
Prompt-path block scores in single-pass TF32 with a per-block error bound,
3xTF32 rescoring only near each query's top-k cut-off, so qsa_block_topk
returns the same cells in the same order, ties included. Off unless the
variable is set; says once where it cannot run (HIP, Metal, SYCL, layer-major
path). Adds qsa_two_pass_test, qsa_two_pass_status_test and the GPU parity
test qsa_two_pass_parity.

Ported from PR 4119 (bitaccesscomau); the log line of the per-prompt counters
is shortened without dropping a field.

l620 (sm_120, IQ3_S), greedy ids identical off vs on in every pair:
prompt 128K +1.65% (n=12), 256K +4.10% (n=8), 32K -0.14% (noise).

(cherry picked from commit bbf7c55f1351d913233250fe7869642f7c2b98a1)
2026-11-06 15:47:43 +01:00
Niko1221 f281e3fe26 force_check.py: absolute path for the profile copy
(cherry picked from commit 1fb1e134809f6015c72802f19a99d70a1b1636db)
2026-11-06 15:47:43 +01:00
Niko1221 f46e88d610 force_check.py: cfg profile_src gives each engine start a fresh copy of the expert profile
(cherry picked from commit e47164c8e9d4d0ae0826d10177cc4d74369ec0bf)
2026-11-06 15:47:43 +01:00
Niko1221 d486fbe7d6 serve: STRATA_FOLLOW / STRATA_FOLLOW_LOGPOS token forcing through the real decode/verify windows (test and measurement only, off by default); force_check.py score --path decode
(cherry picked from commit af68878f0cc4c5aa4618da8f96b10f9262230721)
2026-11-06 15:47:43 +01:00
Niko1221 65909b2d0f verify: STRATA_VERIFY_PROFILE and STRATA_ROUTE_STATS reports cover windows up to 16 rows (measurement only; default behaviour unchanged); tools/force_check.py: the token-forcing check (scorer driver + the paper's two rules)
(cherry picked from commit b3b8c1d5a088c998947895a3a4007138b085da66)
2026-11-06 15:47:43 +01:00
Niko1221 cae562fa01 verify: STRATA_FORCE_FAULT=<m>:<bias> test knob for the token-forcing scorer (adds a bias to every m-th token's logit in the STRATA_LOGPOS rows; off by default, silent)
(cherry picked from commit dcb0483f06810f397cfdce9c1db976d8e67e2a24)
2026-11-06 15:47:43 +01:00
Niko1221 63f063852f prefill: UD-IQ4_XS-v31's Q5_0 down layers (33 of 48, with IQ3_XXS or IQ4_XS gate/up) take the fused F16 matrix-core kernel on gfx1151 under Windows instead of leaving the fused path for gather + FP16 dequantize + hipBLASLt. The Q5_0 block decode (qh bits spread, int8 codes q - 16, scale d) is the RDNA4 patch's; native_f16_kernel and the FP16 down rows are the K-quant experts' route. On by default there; STRATA_PF_FUSED_Q50=0 or STRATA_GFX1151_DEFAULTS=0 turns it off (the layers keep the earlier route), =1 turns it on for any gfx1151 build; it needs the F16 prompt set (STRATA_PF_F16). Other GPUs are untouched. Parity test pairs IQ3_XXS / Q5_0 and IQ4_XS / Q5_0 against the FP32 dequant reference, with 256/64, 33/16, 1/16, 130/32 and 517/32 token/expert cases: rel RMS 2.7e-3 against the int8-rounded model, 1.4-1.5e-2 against FP32 (the IQ pairs' usual level)
(cherry picked from commit 12f9547785781f77da41d6d08c2f541077de2df0)
2026-11-06 15:47:42 +01:00
Niko1221 cc3f9142e3 Auto on the 12 GB / streaming CUDA class: IQ3_S starts the MTP, UD-IQ4_XS-v31 starts DFlash2 with the tuned pack at the class's own 0.8
drafting.CLASS_DEFAULTS: one removable entry per set, read from the signals the confidence already reads (a CUDA card whose engine slots are under the last-but-one CONF_STEPS step).
IQ3_S: on P2003 the file cache makes the MTP faster than DFlash2 by 12-44% (RTX2 t4b). UD-IQ4_XS-v31: DFlash2 beats the MTP by about 6.5% (t8, 3 starts; DF-VAL forced-fast tuned 0.8 x1.037).
The v31 entry DEPENDS ON RTX2's FILE-TIER FIX (unbuffered reads) BEING IN THE SAME RELEASE; without it remove that entry.
Anything chosen by name wins. Resident CUDA, Strix, gfx1201, Arc and Metal entries and the conf tables are unchanged. The sm_86 support line reads the set: UD-IQ4_XS-v31 gets the existing
'faster on some kinds of text and slower on others' line, every other model keeps 'slower'.

(cherry picked from commit 42412d42ff835d6a66a78fbf6d119911908eda1b)
2026-11-06 15:47:42 +01:00
Niko1221 f138653938 DFlash2 on Strix Halo: the tuned pack for UD-IQ4_XS-v31 too, at Strix's current confidence
One more SET_DEFAULTS line for the measured cell (DF-VAL, setup's config, budget 38, 3 rounds: tuned 0.35 against stock 0.35 x1.046, 3 of 3 rounds, every kind 3 of 3).

(cherry picked from commit c2b801157e71a503da859b66baff12fff28cd299)
2026-11-06 15:47:42 +01:00
Niko1221 ce6bbd7d54 DFlash2 on Intel Arc: the tuned bf16-selector pack for SYCL + IQ3_XXS at Arc's own confidence 0.6
The SYCL 0.2.11 engine reads only bf16 selector codebooks, so Arc gets its own tuned file (qwen3.8-flash-next-dflash2.strata1.q4_0-bf16sel.pack, 522,458,112 bytes, own SHA-256 pin) through
SetDefault.pack. Only IQ3_XXS, the set measured on the A750 (tuned 0.6 x1.054, 3 of 3 rounds); other Arc sets keep the stock pack. SYCL never gets the selq4 file, CUDA / HIP / Metal never the
bf16sel one. NOTICE names the second file and no longer overclaims about the text.

(cherry picked from commit 6111c583b70e3c88a3d54039e13d76b19f15742d)
2026-11-06 15:47:42 +01:00
Niko1221 aba4ddaca7 Auto on HIP gfx1201 starts the MTP until the DFlash2 prompt stall is fixed (one entry: drafting.AUTO_MTP)
11 of 50 engines on 0.2.11 never got past the prompt on this card class. Only Auto changes: DFlash2 chosen by name, a saved choice and the person's own options still win; gfx1151, other AMD, CUDA, Metal and SYCL and the confidence tables are unchanged. The support line under the Drafting dropdown says it in plain words (app catalog entry in all nine languages).

(cherry picked from commit 8b4398743aa67368d72277310bbe8093bbe4923c)
2026-11-06 15:47:42 +01:00
Niko1221 31928fbe34 DFlash2: Strata's tuned drafter pack on CUDA (every expert resident) and Strix Halo, at their current confidence
SET_DEFAULTS entries carry a card condition (tuned_applies: the CONF_STEPS first-step slots or the Spark's shared memory for CUDA, gfx1151 for HIP); no entry names a conf, so
conf_for / CONF_STEPS / BACKEND_CONF run as before and only the pack changes. Enabled only for the cells measured at the shipped conf (CUDA-resident IQ3_S and UD-IQ4_XS-v31, Strix IQ3_S, UD-Q4_K_XL, UD-IQ4_XS).
12 GB and 24 GB cards, several cards, gfx1201, other AMD and SYCL never fetch or use it. Fetch keeps tuned, then stock, then MTP; silent offline; feed backends are asked per start's backend.

(cherry picked from commit f096dcd750b5928ace6f9eca1e2936b9e35578d4)
2026-11-06 15:47:42 +01:00
Niko1221 10831eceb2 Metal build: stubs for the f-0216-cpu 512-bit entry points (q2_rows_512_ok, iq512f_supported, iq512_fused_gu_rows, iq512_reference_gu_rows, q50_512_down_rows) 2026-11-06 09:57:08 +01:00
Niko1221 7604725595 arm64 portable build: stubs for cpu_vnni_ok, q2_rows_512_ok and iq512_reference_gu_rows (the Spark link failed on them) 2026-11-06 09:51:02 +01:00
Niko1221 2a8217c0ce generate: the CPU expert kernel line reads 'CPU expert kernels: <X>' (owner's short wording) 2026-11-06 09:44:37 +01:00
Niko1221 a7cc8faa5d split: one constant for the expert window floor, and the IQ3_S pool test
EXPERT_WINDOW_SLOTS (2 * MAXT * TOPK) is the slots a size pool must hold for one window's picks to have a place; ExpertCache's check, the residency
planner's floor and the arena's keep all read it. expert_residency_test now also covers the GSQ-RCO IQ3_S set's real pools (7, two of them a single
layer), a sweep of RAM targets against a copy of the old global ranking as the negative control, a refusal beyond what the floors allow, and that
the card's sliced 16-byte-padded strides partition the layers like the whole-expert bytes do. Class E (placement only).
2026-11-06 09:36:37 +01:00
MIDHAT NASHAR 20885f7af0 split: retain a window in every expert pool
Mixed-quant models can fail to load in the split engine after cold-expert offload: the global residency ranking can leave one size pool below the
2 * MAXT * TOPK slots Up::expert_cache() needs even while other pools can still give up experts. Group experts by both the gate/up and the
down byte strides, keep the cold-first ranking but skip eviction from a pool that has reached the window floor, count only the extra evictions
that really happen, and say so before the model is uploaded when the request cannot be placed.

Ported from #5158 (midhatn) unchanged; the tests and the engine check follow in the next commit.
2026-11-06 09:36:37 +01:00
Niko1221andhavok2-htwo abf2953be0 native_dense: STRATA_HC_Q8 copies only the stage's own Q8_0 hyper-connection rows on a trimmed layer split (P1.2)
With --trim-stage-weights every later stage's HC_Q8 loop over the GGUF copied the rows of all 48 layers and read its own. The loop now skips a
tensor outside the stage's layer range, the filter the upload loop above it and the bf16-pack path below it already use. The Q8_0 rows copied
and the bytes not copied are recorded in the run manifest (STRATA_MANIFEST). STRATA_HC_Q8_ALL_STAGES=1 copies every layer as before.
No value changes: a stage never reads another stage's layers; without a trim the range is unset and nothing is skipped. Class E.

Co-authored-by: havok2-htwo <[email protected]>
2026-11-06 09:36:37 +01:00
Niko1221 78c5a834e3 Merge f-0216-dfship into f-0216-all 2026-11-06 09:36:34 +01:00
Niko1221 51548e5539 Merge f-0216-strix2 into f-0216-all 2026-11-06 09:36:34 +01:00
Niko1221 7597e383cf Merge f-0216-rtx into f-0216-all 2026-11-06 09:36:30 +01:00
Niko1221 f2eedfe9f2 Merge f-0216-cpu into f-0216-all 2026-11-06 09:36:30 +01:00
Niko1221 bd0e878f42 Merge f-0216-core into f-0216-all 2026-11-06 09:36:29 +01:00
Niko1221 4084979c95 prefill: STRATA_PF_ALLOC_TRACE=1 prints the free-memory figure around Prefill::init's allocations against what the code asked for (stager ring, routing tables, PLE rows, GEMM scratch, the chunk's buffers, library warm-up). Off by default, no behaviour change 2026-11-06 08:56:14 +01:00
Niko1221 530a29ea2d serve: on an integrated GPU with a page-locked expert complement (Strix Halo), the free-VRAM startup line and the request-start warning no longer advise a larger --vram-reserve-mib (it cannot raise that figure: the driver counts pinned host memory and the prompt path's buffers about twice, so the cache giving up experts to the complement only lowers it). New startup line: "N MiB of graphics memory reported free. On this PC the graphics chip shares the system memory, so this figure reads low, and a larger --vram-reserve-mib won't raise it. If decode drops to a few tok/s, lower --max-context." New request-start note, printed once: "Only N MiB of graphics memory reported free at the start of this request. On this PC the figure reads low because the graphics chip shares the system memory. If decode drops to a few tok/s, lower --max-context." Every other GPU keeps the earlier text. Wording only: greedy ids identical to engI on agent8k, agent, pp4k and code_py (UD-Q4_K_XL, setup config) 2026-11-06 08:56:13 +01:00
Niko1221 3e8d3b2940 Mac: DFlash2 is the default drafter for UD-Q4_K_XL and UD-IQ4_XS-v31, with Strata's tuned drafter at --draft-conf 0.7
SET_DEFAULTS is the one per-(backend, set) table (Metal only; IQ3_S stays on the MTP). Setup, strata launch and strata drafting fetch fetch the tuned pack through the signed feed
(pinned size and SHA-256, resumable, silent offline, the standard drafter and then the MTP as fallbacks). Apache-2.0 licence and the modified-by-Strata notice ship in
data/dflash2-tuned and are copied beside the file. The Metal 'not tested' line is replaced for these sets.
2026-11-06 07:52:31 +01:00
Niko1221 039cecc5a5 feed: a drafters member for DFlash2 drafter files Strata hosts (pack-drafter, build-feed --drafters, make_local_feed --drafter) 2026-11-06 07:52:31 +01:00
Niko1221 d735d66a4d docs: STRATA_IQ4_W4A8 on sm_120 and its exit counters; STRATA_FILE_TIER_IDLE and the idle policy of the file tier 2026-11-06 01:49:09 +01:00
cctan 7bf5ec4ebf prefill: opt-in int8 kernel for the IQ4_XS / IQ4_NL expert products (STRATA_IQ4_W4A8), MMQ's bits
Ported from #4871 (cctan): src/prefill/moe_iq4_w4a8.cu, tried first by mmq::Context::run; unset, nothing changes.
Changes on top of the PR: the "would MMQ split this launch along K" test is now mmq::launch_splits_k, which reads the
device's real MMQ configs (tile width, stream_k flag, tile efficiency) instead of an Ampere table, so the kernel declines
exactly the launches whose sums MMQ adds in another order, on any architecture; sm_120 joins sm_80..sm_88 (bit-for-bit
against MMQ on an RTX PRO 5000 and an RTX 3060); the kernel counts what it ran and why it declined and prints that at exit;
prefill_iq4_w4a8_test takes --tokens N (tails), --wide (scale extremes) and --bench (MMQ against the kernel per product set).

Class E: 0 differing output words in every case (tokens 97 / 2049 / 4096 / 8191 / 15360 / 32768, uniform and log-normal routing,
group buffers and pointer table). Kernel time against MMQ on the down products (IQ4_NL): +9..+70% on sm_86, +13..+50% on sm_120.
2026-11-06 01:49:09 +01:00
Niko1221 178d1c74b5 merge f-0215-mac-pp (host-row pipeline, qsa_merge_h, ragged expert tiles) into f-0215-mac-int 2026-11-06 01:25:09 +01:00
Niko1221 eecd7f61d7 merge f-0215-mac-out (step profile, opt-in screens, Metal DFlash2 conf 0.5, calibrated gate) into f-0215-mac-int 2026-11-06 01:25:09 +01:00
Niko1221 86c3367a89 native_expert.cpp: the fused-rows type list terminator is '\0' (a NUL byte had slipped into the character constant; MSVC rejects it) 2026-11-06 01:05:40 +01:00
Niko1221 04b73d061d kq_window_bench: --dump-trace writes the synthetic windows in the route-dump text form 2026-11-06 01:01:14 +01:00
Niko1221 1daec2a5b8 check_isa_guard: iq512_reference_gu_rows and iq512x_rows are listed (main already reports them as unlisted; both are reached only behind cpu_avx512_ok / cpu_vnni_ok) 2026-11-06 00:51:10 +01:00
Niko1221 b94ab5f4c6 kq_window_bench: the CPU pool's part of a decode window replayed without a GPU (per-layer format pairs from the pack, expert and row counts calibrated on the engine's logged means, cold DRAM) 2026-11-06 00:50:40 +01:00
Niko1221 445cdf4540 Run manifest: say what the unset STRATA_ROUTE_TAIL_SKIP means (the engine default applies, tails are skipped only where the configuration allows) 2026-11-06 00:47:28 +01:00
Niko1221 8463d15c84 STRATA_ROUTE_DUMP: terminator fix 2026-11-06 00:42:08 +01:00
Niko1221 1c768f9d12 STRATA_ROUTE_DUMP: newline fix 2026-11-06 00:41:18 +01:00
Niko1221 b0b0348c2b kernels: opt-in fused gate/up rows in ggml order for 2+ tokens (STRATA_IQ512_FUSED_MAX; N, after PR 5243's row_gu_dot); STRATA_ROUTE_DUMP routing trace for offline placement replays 2026-11-06 00:39:11 +01:00
Niko1221 7e3fae57ca PR 5120 tier below VBMI is opt-in (STRATA_VNNI_TIER=1): its 512-bit rows round differently from the AVX-2 rows such a CPU runs today (SDE Cascade Lake: all multi-token gate/up lines and the Q2_0 rows differ), so by default every CPU keeps its output 2026-11-06 00:22:37 +01:00
Niko1221 32944ce1ee cpu_dispatch_dump: the Q2_0 rows (q2_rows_any) next to ggml's, so the substitute unpack can be compared with the multishift 2026-11-06 00:22:30 +01:00
Michel Pollet f000d4a455 cpu/experts: the 512-bit VNNI rows on a CPU without AVX-512-VBMI (PR 5120)
cpu_vnni_ok() = AVX-512 F + DQ + BW + VL + VNNI + OS state, cpu_vbmi_ok() = F + VL + VBMI; the Q2_0 row dot and the q8 quantizer take the 512-bit form
on VNNI without VBMI (the one VBMI op, vpmultishiftqb, has a bit-for-bit substitute unpack), and the i-quant / Q5_0 rows of iq_avx512.cpp, whose object holds
no VBMI instruction, gate on cpu_vnni_ok() instead of cpu_avx512_ok().  Adjustments over the PR: DQ is asked for (the file is compiled with it), the i-quant rows
gate on VNNI (the rows use vpdpwssd/vpdpbusd), and STRATA_NO_VBMI=1 runs the substitute unpack on a CPU that has VBMI (a test knob).
2026-11-06 00:22:30 +01:00
Niko1221 6368dc68ab cpu: drop the IQ4_NL 512-bit down rows (5-11% slower than iq4nl256_down_rows at 2-4 tokens on Zen 4); Q5_0 down keeps its 512-bit rows; vpdpwssd in the i-quant rows' integer sums (same integers, one op less) 2026-11-06 00:22:29 +01:00
Niko1221andbsorensen110 55dc17697c cpu: IQ4_XS gate/up and Q5_0 / IQ4_NL down rows in 512-bit lanes on AVX-512 CPUs, bit-identical to the code they replace
IQ4_XS gate/up: ggml-cpu's dot order (the integer lanes of its AVX2 dot, f16 scale times y.d, eight float lanes, its hsum) for every token count;
before, an AVX-512 CPU ran ggml's one-token dot per token.  Q5_0 down: ggml's dot order, weights decoded once per block pair for all tokens.
IQ4_NL down: iq4nl256_down_rows' order, 512-bit.  Each is checked once against the code it replaces; STRATA_NO_IQ4XS512=1 / STRATA_NO_QD512=1 switch
them off.  The IQ4_XS nibble-plane decode follows the layout of PR 5243.

Co-authored-by: bsorensen110 <[email protected]>
2026-11-06 00:22:29 +01:00
Niko1221 241b2f203c cpu_dispatch_dump: a hash per (pair, token count, row range) of the pool's native expert calls, to compare a change switched off and on 2026-11-06 00:22:29 +01:00
Niko1221 044105d8de kq_cpu_bench: --part gu|down times one projection alone, --hot K walks K distinct experts (cache-resident case) next to the DRAM walk 2026-11-06 00:22:29 +01:00
Phlimosx 994029b02f generate: the file tier's Linux defaults and the routing-aware prefetch stay off while every expert is in the GPU caches
Ported from #4972 (Phlimosx), extended: the residency is checked again at every request's start, so a
smaller cache (VRAM resize) or an eviction turns io prefetch, its decode gate and the look-ahead back on,
and a regrown cache turns them off again. Only what the defaults switched on is touched: an explicit
STRATA_IO_PREFETCH=1 keeps io prefetch and the look-ahead, STRATA_LOOKAHEAD=1 keeps the look-ahead,
STRATA_FILE_TIER_IDLE=0 turns the policy off. set_io_prefetch clears the gate flag with the prefetch.

Class E: the answers are identical (45 of 45 decode answers on l620, --mmap-experts, 24576 of 24576 resident).
2026-11-06 00:22:11 +01:00
Niko1221 89f4fa18ee Run manifest (lite): STRATA_MANIFEST=<file> writes the selected fast paths, fallback reasons, tensor formats, engine choice and budgets as JSON (P0.1)
A diagnostic for testers and for the performance work, off by default. With STRATA_MANIFEST unset every call returns at once
and nothing is printed or written. With it set, the engine writes <file> (whole file, replaced by rename) when it is ready and
again after a request if something new was recorded; the launcher (strata_control/split_engine.py) writes the all-cards split
decision to <file>.launch.json. Sorted keys, no timestamps, process ids or addresses, so two matched runs give the same text.

Content:
- engine: version, engine kind (single GPU or layer split), stages, KV format, drafter, spec window, pipeline windows, PLE I/O.
- budgets: context, KV resident cells, expert slots and cache MiB (all tiers and primary), VRAM reserve, prefill chunk cap and
  stages, pool workers, memory limit, resident budget, batch slots, conversation cache.
- paths: one entry per decision with selected/declined and the reason in plain words: prompt experts per format class (MMQ,
  FP16 WMMA tiles, native fused kernels, FP16 GEMM, with the gate/up and down formats and the layer count), MMQ direct and group
  sort, HC_Q8, the output head (VRAM or pinned host, repacked or not), route tail skip, DMA batch mode.
- tensors: dense and head formats as held (count, bytes), the packed and HC_Q8 copies made at load.
- launch: split-engine mode, engine chosen, reasons, card architecture and count, routed-expert types, and the AMD auto rule
  condition by condition (architecture, card count, expert types), so four gfx1201 with the IQ3_S set is recognised and two or
  three cards are not promoted by the four-card rule.

The only text it prints is "strata: STRATA_MANIFEST: cannot write <file>" on stderr, once, when the variable is set and the file
cannot be written.

Tests: tests/core/run_manifest_test.cpp (off is free and writes nothing, matched runs in any recording order give the same text,
replace-on-rerecord, escaping, dirty tracking, unwritable target), tests/split/test_split_engine.py::LaunchManifest.
2026-11-06 00:21:53 +01:00
Niko1221andAlanJager e2d3159996 Build the flag-order test on HIP too: hip_flag_order_test is the same source through the compat header (P1.1)
Co-authored-by: AlanJager <[email protected]>
2026-11-06 00:20:28 +01:00
Niko1221andmartifaj 1ed923f59e PLE reader: issue() folds a small batch in a stack buffer instead of a hash table and a job vector (P1.6)
Class E: the queued jobs (page offsets, read lengths, Use order inside a job) and the dedup_rows count are the same as
before; the bytes delivered are the same.

PleReader::issue() built a std::unordered_map by page and a std::vector<Job> on the heap on every call, then sorted the
jobs by offset. A decode step asks for 1..16 rows (the prefetch for PLE_N_HEADS = 16), so those allocations and the
hashing were most of the work. A batch of up to 64 rows (PR #4910) now collects its misses into a fixed stack array,
sorts them by (page offset, request order), and merges equal pages in one pass straight into the queue. Nothing is shared
between calls, so concurrent issuers (they hold the reader's mutex when threaded) have no scratch to race on.

Batches above 64 rows (a prompt chunk: thousands of rows) keep the previous hash path. The PR also sorted every miss in
that case with a reusable vector; measured, that is slower there (see below), so it was not taken. A batch of more than
2^32 rows is refused like a failed reader, as the 32-bit order/ticket counters cannot hold it (the old code could not
queue it either).

Measured with tests/ngram/ple_issue_bench.cpp (this thread's heap allocations and thread CPU time inside issue(), threaded
reader, 3000 calls per cell, 2 rounds that agree within 3%; Ryzen 5 5600X, g++ -O2, synthetic 300k-row table, rows are a
4000-row window / uniform / skewed):
  rows   allocations/issue before -> after    CPU us/issue before -> after
     1        5.1 -> 2.1                          1.69 -> 1.54
     4       13.4 -> 5.4                          2.00 -> 1.82
    16       40.3 -> 17.4                         3.44 -> 2.53
    64      125.2 -> 51.8                         8.07 -> 5.01
  above 64: unchanged (the PR's sort-everything variant took 159 vs 66 us at 3072 rows and 2.9 vs 0.55 ms at 49152).
The decode impact is not measured here (see the round's results file).

Tests: ple_reader_test --selftest now also checks, for batch sizes 0, 1, 2, 15, 16, 63, 64, 65, 66, 127..129, 300, 1000,
4096 (random, clustered, one row n times, descending, out-of-range mixed in, page-straddling rows twice), that the bytes
are right, requests grew by the rows asked for and dedup_rows by (rows read - distinct pages). With the existing
selftest and the multi-client stress test (rows across pages, injected delays and read errors, late writers, failed
reader): pass plain, under ASan+UBSan, and the stress test under TSan.

Co-authored-by: martifaj <[email protected]>
2026-11-06 00:12:28 +01:00
Niko1221andcloud4t0r aeaca205eb Mixed-HIP pipelined prompt read: stage 0 is served to its end before the drafter's blocking prefill on its card (P1.9, part 2)
Correctness (class E): the same windows are served in the same order, only a little earlier; the tokens do not change.

With --pipeline-windows 2 on a two-stage layer split the drafter sits on the first stage's card (mtp_first).
read_windows_pl called mtp.prefill(...) right after a stage-1 window finished, while stage 0 still had a window in
flight on that same card. MtpDrafter::prefill blocks the host until its stream is idle (on HIP it synchronizes once
per group whatever its `sync` argument says: prefill_per_group_sync), a stage-0 window only advances when this same
thread serves its doorbells, so the host sat in hipStreamSynchronize and the window it waited for could never
finish: the first request of a fresh engine read its prompt, produced 0 tokens, and the watchdog ended the engine
60 s later (gfx1201 + gfx1030, issue #5147). Neither 20 s guard in Verifier::service / done can fire, because the
loop never runs again.

serve_stage0_to_end() serves stage 0's windows to their end before that prefill, when the drafter shares stage 0's
card. The HIP per-group synchronization (which exists because the unsynchronized E-4 queue hangs on gfx1201 /
ROCm 7.2.4) is left as it is: the caller is fixed, not the mitigation. Pipeline windows 0 and 1 take no new path
(mtp_first is false there). The overlap lost around each drafter prefill has not been measured.

Not testable on the boxes of this round: it needs a mixed pair of HIP cards (gfx1201 + gfx1030) and a two-stage split;
p620 has one R9700. Compiles on CUDA (z440, sm_86).

Co-authored-by: cloud4t0r <[email protected]>
2026-11-05 23:57:51 +01:00
Niko1221andquantforge-br f225844d19 Prompt MMQ row tables get the stream-k fixup's over-read margin, sized from the backend's tile bound (P1.9, part 1)
Correctness (class E, not a speedup): the product values do not change; the rows past T*K are never routed to.

llama.cpp's mul_mat_q_stream_k_fixup loads ids_dst[col_low + jt*J + j] for a whole J-column tile before it compares
the tile with the expert's rows (the main kernel skips an empty tile first; the fixup only skips its writes), and its
tiles run to ceil(max_rows / J) * J for every expert of the launch. A table of exactly T*K entries is read past its
end by up to roundup(max_rows, J), and the main kernel reads up to J - 1 past its last row. Strata's `ids_identity`
was an exact-sized allocation: when it ended a 2 MiB page the over-read faulted on the next (Xid 31 FAULT_PDE,
"prefill copy_i32: an illegal memory access" on a long prompt on a CUDA layer split; issue #1468 and others).

PR #5108 added T + 256 rows by hand. Here the margin is derived: J is bounded by ggml's mul_mat_q_switch_J
(J = 8 .. 128 in steps of 8), the tile loops of moe_mmq.cu now use that one constant (kMmqTileColsMax /
kMmqTileColsStep), and the table is T*K + roundup(T, J) + J rows (an expert takes at most one row per token), with
checked 64-bit arithmetic: a size that does not fit fails the carve instead of wrapping to a small table. The same
size is used by carve, bytes_needed_impl and the three places that write the identity (helper rebind, normal prompt,
layer-major), so the estimate and the carve stay identical. The peer prefill's row table, an exact cudaMalloc used by
the same MMQ launches, gets the same margin.

Tests: tests/prefill/mmq_ids_margin_test.cpp enumerates every index the fixup and the main kernel can read for 2000
random layouts (late experts, empty tiles, every J), checks the exact-sized table of before is overrun by the late
expert layout, and the overflow refusals. Builds on CUDA sm_86 (z440).

Co-authored-by: quantforge-br <[email protected]>
2026-11-05 23:46:57 +01:00
40849df0a8 Transfer completion: flag B stays behind queued copies, and the adaptive-copy range covers the whole round (P1.1)
Correctness (class E): kept even without a speedup.

Flag B (PR #5096): the graph waits for "flag B >= layer + 1" and flag B is one counter that only rises. A non-empty
plan raises it from a host function on the copy stream, after its uploads. An empty plan (n <= 0) raised it on the
spot, even when the copy stream still held an earlier layer's, or the other token group's, uploads: the counter
jumped past a layer whose bytes had not landed. Verifier::raise_flag_b_ordered() now raises it through the copy
stream whenever copies were queued in this window, and directly only when none were. The same ordering is used at
the four places that publish an empty plan when the pool did not (run, run_slot_rows, batch_serve, service), which
had the same hazard. A failed host-function launch used to be ignored (flag B never rose and the window hung); it
now stops the engine like a failed upload does, after releasing the GPU waits.

Adaptive-copy bookkeeping (PR #4844): a round planned on the pipelined loop whose loop drained before step 3 is
marked resident on the serial rule, which used the lo/hi/bi of an earlier round, so only that range of swaps was
marked and the others were lost (the slot held the new expert, the table still named the old one). The pipelined plan
now names the whole round. The range arithmetic moved into AdaptiveCopyRange (adaptive_range.hpp) so a host test can
walk the rounds.

Tests: tests/core/flag_order_test.cu (delayed host-function copy standing in for a slow DMA, a consumer kernel that
reads staging once flag B allows it, a negative control that shows the old direct raise releases it early, flag only
rises, nothing queued, graph replay twice, every device; passes on both A4000s of z440), tests/program/
adaptive_range_test.cpp (serial batches for 0, 1, many swaps and several caps, the stale-range loss, empty round).

Co-authored-by: AlanJager <[email protected]>
Co-authored-by: uvlad23 <[email protected]>
2026-11-05 23:39:06 +01:00
a5b820a415 Request accounting and prompt timing marks: yielded prompt legs summed once, admission time without a double count, unknown stays unknown (P0.4)
Reporting only (class E): no engine speedup, no change to what the engine computes.

serve/server.py
- A prompt read that gives way (BYIELD) and resumes keeps the request's external prefix boundary, and the prompt
  time and tokens of every leg are summed exactly once (prefill_leg_stats; PR #5318).
- The admission's DONE times one token and BDONE counts that token but times only the slot: the slot time now
  includes the admission time, and the output token is not counted twice. A figure the engine never reported
  (a close at the admission token, in the slot, a carry back to the solo path) stays unknown (None) instead of
  turning into 0 or into the API's output count (PR #5307).
- The two patches overlap in the request finalization; ended_on_yield is taken before the unknown-figures step so
  the identity check still sees the yield's own record.

PfTimer::mark(): a mark in the phase already running is not recorded (PR #4006). The totals are the same, a
fraction of the event records; under WDDM every record is a submission of its own and the launch gaps they made
were charged to "dequant". The rule is a pure function in prefill/timer_marks.hpp with a host test.

Tests: serve/test_yield_accounting.py (new: two yields, solo and admission yields, cancel after one and two
yields), serve/test_parallel.py BatchLegAccounting (EOS and length finishes, zero slot time, close at the
admission token and in the slot, slot to solo carry), serve/test_request_accounting.py (unknown stays unknown,
explicit zero), tests/prefill/timer_marks_test.cpp (same totals with 18 of 23 records skipped).

Co-authored-by: YusenZhang0601 <[email protected]>
Co-authored-by: shoucandanghehe <[email protected]>
Co-authored-by: Vop3p <[email protected]>
2026-11-05 23:31:14 +01:00
Niko1221 b97433df05 spec: STRATA_DF2_GATE=1 (opt-in): DFlash2's verify window from a per-request calibrated survival (position x confidence bucket, prior C^1.3) against the cost of one more row, instead of a fixed --draft-conf; window_rule_test covers it 2026-11-05 23:18:51 +01:00
Niko1221 64cc20a8e2 Merge f-0215-fit (setup wording) into f-0215-int 2026-11-05 22:47:02 +01:00
Niko1221 1d342f0836 setup: on a unified-memory GPU whose RAM budget leaves no room for a numeric expert cache, say where the cache is sized from instead of warning that it may take the OS's room: "The expert cache is sized from the graphics memory (<N> GB carve-out + shared), not from the RAM." N is the carve-out setup detected (no number when none was). The old warning and its --resident-budget-gib tip are gone; the test checks the new line with 2 GB and 48 GB carve-outs 2026-11-05 22:44:44 +01:00
Niko1221 0e1d440713 Merge f-0215-pk into f-0215-int 2026-11-05 22:36:02 +01:00
Niko1221 19136808f9 Merge f-0215-fit into f-0215-int 2026-11-05 22:35:59 +01:00
Niko1221 2c0ad33f71 Metal prompt: ragged expert tiles on by default for the types that have kernels (STRATA_PROMPT_V6=0 off); drop the unused Mac PLE threading and the tall-tile probes 2026-11-05 21:59:37 +01:00
Niko1221 3cc5714f37 Strix Halo (Windows gfx1151): STRATA_SMALL_UNR=3 and STRATA_GR_Q8_PFS=1 (load-ordering variants of the decode kernels, same arithmetic) join the default table. UD-Q4_K_XL, DFlash2 serve, 4 rounds x 2 passes x 4 prompts interleaved: window time -0.25% (25 of 32 pairs faster, same window and token count in every pair); first-token logits and 220-token outputs identical on 3 prompts for UD-Q4_K_XL and UD-IQ4_XS. Linux gfx1151 and STRATA_DEFAULTS=026 keep their tables; arch_defaults_test updated and passing 2026-11-05 21:19:24 +01:00
Niko1221 687a7d79d7 Metal prompt: host rows beside the GPU (embedding + PLE rows of the next chunk on a thread, parallel PLE gather on Macs), qsa_merge_h (merge writes the fp16 operand), ragged expert tiles (mm_gu6/mm_down6/mm_scan6, STRATA_PROMPT_V6), STRATA_PROMPT_KPROF per-dispatch profile, matmul2d probes 2026-11-05 20:36:54 +01:00
Niko1221 6e31946cbe Strix Halo (gfx1151, Windows): the F16 prompt expert kernels write the down rows as FP16 and a combine kernel reads them (STRATA_PF_DM16, on by default there, =0 turns it off; from the Strix dm16 work patch, extended to the F16 K-quant and IQ kernels). 8K prompt, same engine, interleaved pairs: IQ3_S +2.2% (4 of 4), UD-IQ4_XS +2.6% (3 of 3), UD-Q4_K_XL +1.2% (3 of 4); first-token KL 1e-4 at 8K, 0.02-0.04 on agent transcripts 2026-11-05 19:59:50 +01:00
Niko1221 66b346c29f drafting: Metal gets its own DFlash2 confidence (0.5, BACKEND_CONF) instead of the CUDA slot steps, which wrote 0.8 for the Mac's --expert-cache 64; M5 Max UD-Q4_K_XL +14.3% over MTP spec 2 on six kinds 2026-11-05 19:54:31 +01:00
Niko1221 830732a33d metal: step profile scales every pass by the cool whole-window time; opt-in screens STRATA_METAL_HCLD=1 (HC loads in flight: -10.9%, off), STRATA_METAL_WPF_MIB (weight prefetch beside gr_up), STRATA_METAL_MOE_GU / MOE_DN (expert tile override) 2026-11-05 19:54:31 +01:00
Niko1221 e431a4a8e2 Strix Halo (Windows gfx1151): the V2K grouped decode experts for Q4_K / Q5_K gate/up and Q5_1 / Q8_0 down are on by default (STRATA_EXPERT_V2K=0 turns them off). UD-Q4_K_XL, DFlash2 serve, 16 request pairs: window time -4.0% (16 of 16 faster, mean x1.040), same window and token count in every pair; first-token logits and 220-token outputs identical on 3 prompts (the kernels are bitwise the old ones) 2026-11-05 19:27:35 +01:00
Niko1221 4dbaf703fd Strix Halo (gfx1151, Windows): the K-quant prompt experts of UD-Q4_K_XL (Q4_K / Q5_K gate/up, Q5_1 / Q8_0 down) take the fused F16 matrix-core kernels by default (STRATA_PF_FUSED_KQ=0 or STRATA_GFX1151_DEFAULTS=0 keeps the earlier route). Setup config, n=1: 8K prompt 885 -> 1,387 tok/s, 4K 611 -> 1,316; first-token KL against the earlier route 1e-5 .. 6e-2 on prompts of 1K+ tokens (information), same greedy ids 2026-11-05 18:54:42 +01:00
Niko1221 c4fa1d9e1e metal: STRATA_METAL_STEPPROF=T, the in-situ step profile by part (prefix runs at every barrier group, weight bytes and GB/s per part, distinct experts per layer); run_graph can run the first N dispatches 2026-11-05 18:41:45 +01:00
Niko1221 9f7d977d22 README: a Loyal supporters section under the Sponsor and coffee buttons in all 8 languages, with the public GitHub sponsors as linked profile pictures (first: ryan4yin) and a one-line template for the next one 2026-11-05 14:54:54 +01:00
Niko1221 5d37280db1 Docker: build.sh copies metal/tools/metal_repack.py and stream_experts.py into the build context (the Terminal bundle carries them since the Mac Unsloth sets; the v0.2.11 image build stopped at the missing metal_repack.py); the context test checks every INSTALLER_METAL_TOOLS name 2026-11-05 14:02:27 +01:00
Niko1221 dc9dfcfd29 GitHub Sponsors: FUNDING.yml names github: Niko1221 (the repository's Sponsor button), and every README gets a Sponsor badge beside the coffee badge, a Sponsor button in Support Strata and a support line that names both 2026-11-05 14:02:27 +01:00
900 changed files with 200728 additions and 7303 deletions

No files matched your search

+1
View File
@@ -28,3 +28,4 @@ docker/compose/* text eol=lf
# vendored KaTeX (serve/web/vendor): the files are hashed byte for byte (PROVENANCE.json), never convert their line endings.
serve/web/vendor/** -text
third_party/strata-ggml/** text eol=lf
+1
View File
@@ -1 +1,2 @@
github: Niko1221
buy_me_a_coffee: strataengine
+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.11 · engine 0.1.57"
placeholder: "Strata 0.2.13 · engine 0.1.59"
validations:
required: true
- type: input
+218
View File
@@ -0,0 +1,218 @@
name: Backend compile checks
# Compile and link every engine backend with a pinned toolchain, without a GPU: nothing is run, so a call into a kernel that only
# exists for another backend (or an API a toolkit version dropped) fails here instead of on a user's machine. Hardware runs are separate.
# CUDA 12.9 and 13.0 nvidia/cuda devel container targets strata, strata_kq (CMake preset cuda-compile)
# HIP (ROCm 7.2) rocm/dev container target strata (CMake preset hip-compile)
# SYCL (oneAPI 2026.1) ubuntu + Intel's apt repo target strata of sycl/ (the commands of packaging/build_sycl_linux.sh)
# Metal macOS runner targets strata and strata_ds4 (metal/; metal-build.yml keeps its own strata build)
# llama.cpp (ggml) comes at the commit pinned in setup.py (LLAMA_CPP_COMMIT), the same one the release builds use.
# Architectures are fewer than the release package's (compile time); the flags that decide which sources and defines are built are the release's.
on:
push:
branches: [main]
paths:
- .github/workflows/backends.yml
- CMakeLists.txt
- CMakePresets.json
- cmake/**
- src/**
- include/**
- third_party/**
- sycl/**
- metal/**
- setup.py
- packaging/build_cuda_linux.sh
- packaging/build_hip_linux.sh
- packaging/build_sycl_linux.sh
pull_request:
paths:
- .github/workflows/backends.yml
- CMakeLists.txt
- CMakePresets.json
- cmake/**
- src/**
- include/**
- third_party/**
- sycl/**
- metal/**
- setup.py
- packaging/build_cuda_linux.sh
- packaging/build_hip_linux.sh
- packaging/build_sycl_linux.sh
workflow_dispatch:
permissions:
contents: read
concurrency:
group: backends-${{ github.ref }}
cancel-in-progress: true
env:
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_MAXSIZE: 2G
jobs:
cuda:
name: CUDA ${{ matrix.cuda }}
runs-on: ubuntu-24.04
timeout-minutes: 120
container: nvidia/cuda:${{ matrix.image }}
strategy:
fail-fast: false
matrix:
include:
# the cuda12 package is the one for Pascal and Volta: sm_60 needs STRATA_EXPERIMENTAL_SM60 (packaging/build_cuda_linux.sh)
- { cuda: '12.9', image: '12.9.1-devel-ubuntu24.04', archs: '60-real;61-real;70-real;86-real', sm60: 'ON' }
- { cuda: '13.0', image: '13.0.2-devel-ubuntu24.04', archs: '75-real;86-real;120-real', sm60: 'OFF' }
steps:
- name: Tools
run: |
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends git ca-certificates cmake ninja-build ccache
cmake --version | head -1
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: llama.cpp (ggml) at the pinned commit
run: |
commit=$(sed -n 's/^LLAMA_CPP_COMMIT = "\(.*\)"/\1/p' setup.py)
mkdir llama.cpp && cd llama.cpp && git init -q
git remote add origin https://github.com/ggml-org/llama.cpp.git
git fetch -q --depth 1 origin "$commit" && git checkout -q FETCH_HEAD
- uses: actions/cache@v4
with:
path: .ccache
key: ccache-cuda${{ matrix.cuda }}-${{ hashFiles('CMakeLists.txt', 'src/**', 'include/**', 'cmake/**') }}
restore-keys: ccache-cuda${{ matrix.cuda }}-
- name: Configure (preset cuda-compile)
run: |
nvcc --version | tail -2
cmake --preset cuda-compile -B build -DSTRATA_GGML_DIR="$PWD/llama.cpp" \
"-DCMAKE_CUDA_ARCHITECTURES=${{ matrix.archs }}" -DSTRATA_EXPERIMENTAL_SM60=${{ matrix.sm60 }} \
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_CUDA_COMPILER_LAUNCHER=ccache
- name: Build strata and strata_kq
run: cmake --build build --target strata strata_kq -j "$(nproc)"
- name: Cache statistics
run: ccache -s | head -8
hip:
name: HIP ROCm 7.2
runs-on: ubuntu-24.04
timeout-minutes: 120
container: rocm/dev-ubuntu-24.04:7.2.4-complete
steps:
- name: Tools
run: |
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends git ca-certificates cmake ninja-build ccache
cmake --version | head -1
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: llama.cpp (ggml) at the pinned commit
run: |
commit=$(sed -n 's/^LLAMA_CPP_COMMIT = "\(.*\)"/\1/p' setup.py)
mkdir llama.cpp && cd llama.cpp && git init -q
git remote add origin https://github.com/ggml-org/llama.cpp.git
git fetch -q --depth 1 origin "$commit" && git checkout -q FETCH_HEAD
- uses: actions/cache@v4
with:
path: .ccache
key: ccache-hip72-${{ hashFiles('CMakeLists.txt', 'src/**', 'include/**', 'cmake/**') }}
restore-keys: ccache-hip72-
- name: Configure (preset hip-compile)
run: |
/opt/rocm/llvm/bin/clang++ --version | head -1
cmake --preset hip-compile -B build -DSTRATA_GGML_DIR="$PWD/llama.cpp" \
-DCMAKE_HIP_COMPILER=/opt/rocm/llvm/bin/clang++ -DCMAKE_HIP_COMPILER_ROCM_ROOT=/opt/rocm -DCMAKE_PREFIX_PATH=/opt/rocm \
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_HIP_COMPILER_LAUNCHER=ccache
- name: Build strata
run: cmake --build build --target strata -j "$(nproc)"
- name: Cache statistics
run: ccache -s | head -8
sycl:
# The engine needs oneAPI 2026.1 or newer (sycl/include/dpct/util.hpp uses the entangle and chunked_partition extensions, which the
# 2025.3.2 compiler does not have: it stops at "no member named 'entangle'"; the intel/oneapi-basekit images end at 2025.3), so the
# compiler comes from Intel's apt repository at a pinned version.
name: SYCL oneAPI 2026.1
runs-on: ubuntu-24.04
timeout-minutes: 120
container: ubuntu:24.04
steps:
- name: Tools and oneAPI 2026.1
run: |
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends git ca-certificates cmake ninja-build ccache wget gpg
wget -qO- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB | gpg --dearmor -o /usr/share/keyrings/oneapi-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main" > /etc/apt/sources.list.d/oneAPI.list
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
intel-oneapi-compiler-dpcpp-cpp-2026.1=2026.1.1-325 intel-oneapi-mkl-devel-2026.1=2026.1.0-236
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: llama.cpp (ggml) at the pinned commit
run: |
commit=$(sed -n 's/^LLAMA_CPP_COMMIT = "\(.*\)"/\1/p' setup.py)
mkdir llama.cpp && cd llama.cpp && git init -q
git remote add origin https://github.com/ggml-org/llama.cpp.git
git fetch -q --depth 1 origin "$commit" && git checkout -q FETCH_HEAD
- uses: actions/cache@v4
with:
path: .ccache
key: ccache-sycl-${{ hashFiles('CMakeLists.txt', 'src/**', 'include/**', 'sycl/**') }}
restore-keys: ccache-sycl-
- name: Configure and build (the commands of packaging/build_sycl_linux.sh)
shell: bash
run: |
. /opt/intel/oneapi/setvars.sh --force >/dev/null
icpx --version | head -1
cmake -S sycl -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=icx -DCMAKE_CXX_COMPILER=icpx \
-DSTRATA_SYCL_PARITY=OFF -DSTRATA_SYCL_IQ_DECODE_FAST=ON -DSTRATA_GGML_DIR="$PWD/llama.cpp" \
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
cmake --build build --target strata -j "$(nproc)"
- name: Cache statistics
run: ccache -s | head -8
metal:
name: Metal strata + strata_ds4
# the macOS 26 SDK: metal/src/mtl_rt.mm names MTLGPUFamilyMetal4 behind @available (a run-time check); the names must be in the SDK
runs-on: macos-26
timeout-minutes: 60
env:
# metal/setup_mac.py's metal-cpp pin (the same as metal-build.yml)
METAL_CPP_URL: https://developer.apple.com/metal/cpp/files/metal-cpp_26.zip
METAL_CPP_SHA256: 4df3c078b9aadcb516212e9cb03004cbc5ce9a3e9c068fa3144d021db585a3a4
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Tools
run: brew install ninja ccache
- name: metal-cpp
run: |
curl -fsSL "$METAL_CPP_URL" -o metal-cpp.zip
echo "$METAL_CPP_SHA256 metal-cpp.zip" | shasum -a 256 -c -
unzip -q metal-cpp.zip -d _mcpp
- name: llama.cpp (ggml) at the pinned commit
run: |
commit=$(sed -n 's/^LLAMA_CPP_COMMIT = "\(.*\)"/\1/p' setup.py)
mkdir llama.cpp && cd llama.cpp && git init -q
git remote add origin https://github.com/ggml-org/llama.cpp.git
git fetch -q --depth 1 origin "$commit" && git checkout -q FETCH_HEAD
- uses: actions/cache@v4
with:
path: .ccache
key: ccache-metal-${{ hashFiles('CMakeLists.txt', 'src/**', 'include/**', 'metal/**') }}
restore-keys: ccache-metal-
- name: Configure
run: |
cmake -S metal -B build-metal -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DMETAL_CPP_DIR="$PWD/_mcpp/metal-cpp" -DSTRATA_GGML_DIR="$PWD/llama.cpp" \
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
- name: Build and link strata and strata_ds4
run: cmake --build build-metal --target strata strata_ds4
+185
View File
@@ -0,0 +1,185 @@
name: Checks
# The fast checks (tools/check), one job per step so a failure names its step: the CPU CMake preset, the Python suites, the host's
# typecheck / test / build. No GPU, no model and no download of weights. The config snapshot has its own workflow (snapshot.yml)
# and the backend compile checks theirs (backends.yml). Run the same thing by hand: python tools/check --only <step>
on:
push:
branches: [main]
paths:
- .github/workflows/check.yml
- tools/check
- requirements*.txt
- CMakeLists.txt
- CMakePresets.json
- cmake/**
- src/**
- include/**
- bench/micro/**
- third_party/**
- tests/**
- serve/**
- strata.py
- setup.py
- strata_cli/**
- strata_control/**
- tools/**
- packaging/**
- docker/**
- apps/host/**
- apps/desktop/**
- metal/**
- sycl/**
pull_request:
paths:
- .github/workflows/check.yml
- tools/check
- requirements*.txt
- CMakeLists.txt
- CMakePresets.json
- cmake/**
- src/**
- include/**
- bench/micro/**
- third_party/**
- tests/**
- serve/**
- strata.py
- setup.py
- strata_cli/**
- strata_control/**
- tools/**
- packaging/**
- docker/**
- apps/host/**
- apps/desktop/**
- metal/**
- sycl/**
workflow_dispatch:
permissions:
contents: read
concurrency:
group: checks-${{ github.ref }}
cancel-in-progress: true
jobs:
linux:
name: Linux / ${{ matrix.name }}
runs-on: ubuntu-24.04
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
include:
- { name: cpu CMake preset, only: cmake-cpu }
- { name: Python tests/, only: py-tests }
- { name: Python serve/, only: py-serve }
- { name: Python tools/, only: py-tools }
- { name: host typecheck + test + build, only: "host-typecheck,host-test,host-build" }
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
cache-dependency-path: requirements*.txt
- uses: actions/setup-node@v4
if: ${{ startsWith(matrix.only, 'host') }}
with:
node-version: '24'
cache: npm
cache-dependency-path: apps/host/package-lock.json
- name: Python packages
run: python -m pip install -r requirements.txt -r requirements-dev.txt -r requirements-build.txt
- name: tools/check
run: python tools/check --only "${{ matrix.only }}" --keep --scratch "$RUNNER_TEMP/check" --json "$RUNNER_TEMP/check/result.json"
- name: Logs
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: check-linux-${{ strategy.job-index }}
path: |
${{ runner.temp }}/check/*.log
${{ runner.temp }}/check/result.json
retention-days: 7
windows:
name: Windows / ${{ matrix.name }}
runs-on: windows-2022
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- { name: cpu CMake preset (MSVC), only: cmake-cpu }
- { name: Python tests/, only: py-tests }
- { name: Python serve/, only: py-serve }
- { name: Python tools/, only: py-tools }
- { name: host typecheck + test + build, only: "host-typecheck,host-test,host-build" }
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
cache-dependency-path: requirements*.txt
- uses: actions/setup-node@v4
if: ${{ startsWith(matrix.only, 'host') }}
with:
node-version: '24'
cache: npm
cache-dependency-path: apps/host/package-lock.json
- name: Python packages
run: python -m pip install -r requirements.txt -r requirements-dev.txt -r requirements-build.txt
# tools/check finds Visual Studio's compiler itself (vcvars64.bat); bash is Git for Windows' (the tester-README tests need it)
- name: tools/check
run: python tools/check --only "${{ matrix.only }}" --keep --scratch "$env:RUNNER_TEMP\check" --json "$env:RUNNER_TEMP\check\result.json"
- name: Logs
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: check-windows-${{ strategy.job-index }}
path: |
${{ runner.temp }}\check\*.log
${{ runner.temp }}\check\result.json
retention-days: 7
macos:
# Cheap and useful (the audit found the host and Python fixtures reading the Mac's own paths), but this job has not run on a
# Mac runner yet: it reports and does not block until its first green run.
name: macOS / ${{ matrix.name }}
runs-on: macos-15
timeout-minutes: 60
continue-on-error: true
strategy:
fail-fast: false
matrix:
include:
- { name: Python tools/, only: py-tools }
- { name: host typecheck + test + build, only: "host-typecheck,host-test,host-build" }
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
cache-dependency-path: requirements*.txt
- uses: actions/setup-node@v4
if: ${{ startsWith(matrix.only, 'host') }}
with:
node-version: '24'
cache: npm
cache-dependency-path: apps/host/package-lock.json
- name: Python packages
run: python -m pip install -r requirements.txt -r requirements-dev.txt
- name: tools/check
run: python tools/check --only "${{ matrix.only }}"
+72
View File
@@ -0,0 +1,72 @@
name: Docs
# The documentation checks (python tools/check --only docs): every local link in the current pages resolves, the version lines and
# installer names agree with docs/data/release.json, the capability table and its summaries are what docs/data/capabilities.json says, and the
# settings page matches the register. Python standard library only; nothing is built, downloaded or run on a GPU.
# Archived pages (history/, release notes, bench/results) are not checked; their baseline is history/LINK-BASELINE.md.
on:
push:
branches: [main]
paths:
- .github/workflows/docs.yml
- '**/*.md'
- docs/data/release.json
- docs/data/capabilities.json
- data/settings-register.json
- tools/check
- tools/check_links.py
- tools/doc_pages.py
- tools/release_manifest.py
- tools/capability_table.py
- tools/settings_register.py
- strata_cli/__init__.py
- CMakeLists.txt
- apps/host/package.json
- apps/desktop/package.json
pull_request:
paths:
- .github/workflows/docs.yml
- '**/*.md'
- docs/data/release.json
- docs/data/capabilities.json
- data/settings-register.json
- tools/check
- tools/check_links.py
- tools/doc_pages.py
- tools/release_manifest.py
- tools/capability_table.py
- tools/settings_register.py
- strata_cli/__init__.py
- CMakeLists.txt
- apps/host/package.json
- apps/desktop/package.json
permissions:
contents: read
concurrency:
group: docs-${{ github.ref }}
cancel-in-progress: true
jobs:
docs:
name: Links, release manifest, capability table
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: tools/check docs
run: python tools/check --only docs --keep --scratch "$RUNNER_TEMP/check" --json "$RUNNER_TEMP/check/result.json"
- name: Log
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: docs-check
path: ${{ runner.temp }}/check/*.log
retention-days: 7
+83
View File
@@ -0,0 +1,83 @@
name: Config snapshot
# tests/snapshot: what `strata launch`, the app's plain Load and the app's Models-screen Load hand the engine (arguments, environment, drafter,
# confidence), for every simulated machine class and model set, against the golden files in tests/snapshot/golden. Nothing is loaded,
# downloaded or run on a GPU. Windows and Linux must give byte-identical output, so both run. A change that moves what users
# get has to update the golden files in the same commit (STRATA_SNAPSHOT_UPDATE=1 python -m pytest tests/snapshot, see tests/snapshot/README.md).
on:
push:
branches: [main]
paths:
- .github/workflows/snapshot.yml
- tests/snapshot/**
- serve/**
- strata_cli/**
- strata_control/**
- setup.py
- strata.py
- requirements*.txt
- apps/host/server/**
- apps/host/package*.json
- packaging/**
- tools/check
pull_request:
paths:
- .github/workflows/snapshot.yml
- tests/snapshot/**
- serve/**
- strata_cli/**
- strata_control/**
- setup.py
- strata.py
- requirements*.txt
- apps/host/server/**
- apps/host/package*.json
- packaging/**
- tools/check
workflow_dispatch:
permissions:
contents: read
concurrency:
group: snapshot-${{ github.ref }}
cancel-in-progress: true
jobs:
snapshot:
name: ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, windows-2022]
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
cache-dependency-path: requirements*.txt
- uses: actions/setup-node@v4
with:
node-version: '24'
cache: npm
cache-dependency-path: apps/host/package-lock.json
- name: Python packages
run: python -m pip install -r requirements.txt -r requirements-dev.txt
- name: Host packages (the app's Load runs in Node)
working-directory: apps/host
run: npm ci --no-audit --no-fund
- name: tools/check snapshot
run: python tools/check --only snapshot --keep --scratch "${{ runner.temp }}/check"
- name: Logs
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: snapshot-${{ matrix.os }}
path: ${{ runner.temp }}/check/*.log
retention-days: 7
+2 -2
View File
@@ -1,6 +1,6 @@
# AGENTS.md
Version 0.2.11, engine 0.1.57. The public model families are DeepSeekV4.1-Flash-1017,
Version 0.2.13, engine 0.1.59. 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
@@ -15,7 +15,7 @@ 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. One installer per system ([latest release](https://github.com/Niko1221/Strata/releases/latest)):
`Strata-0.2.9-windows-x64-setup.exe`, `Strata-0.2.9-linux-x86_64.AppImage`, `Strata-0.2.9-macos-arm64.dmg`.
`Strata-0.2.13-windows-x64-setup.exe`, `Strata-0.2.13-linux-x86_64.AppImage`, `Strata-0.2.13-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
+107 -2490
View File
File diff suppressed because it is too large. Load diff
+78
View File
@@ -0,0 +1,78 @@
{
"version": 5,
"cmakeMinimumRequired": { "major": 3, "minor": 24, "patch": 0 },
"configurePresets": [
{
"name": "cpu-check",
"displayName": "CPU check build (no GPU, no network)",
"description": "The fast check: the CPU-only engine pieces and every test that needs no GPU and no model. STRATA_NATIVE_EXPERTS=OFF, so ggml is not needed. tools/check runs this preset.",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"STRATA_BUILD_TESTS": "ON",
"STRATA_NATIVE_EXPERTS": "OFF"
}
},
{
"name": "cpu-check-native",
"displayName": "CPU check build with the native expert tests",
"description": "As cpu-check plus the native (IQ) expert kernels and their tests. ggml comes from the vendored third_party/strata-ggml (no download); -DSTRATA_GGML_DIR=<llama.cpp checkout> overrides it.",
"inherits": "cpu-check",
"cacheVariables": { "STRATA_NATIVE_EXPERTS": "ON" }
},
{
"name": "cuda-compile",
"displayName": "CUDA compile-only (the release flags of packaging/build_cuda_linux.sh, no tests)",
"description": "Builds the strata and strata_kq targets for the given CUDA architectures. No GPU is needed to compile; nothing is run. ggml is the vendored third_party/strata-ggml; -DSTRATA_GGML_DIR=<llama.cpp checkout> overrides it (CI still passes one).",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"STRATA_ENABLE_CUDA": "ON",
"STRATA_BUILD_TESTS": "OFF",
"STRATA_NATIVE_EXPERTS": "ON",
"STRATA_PORTABLE": "ON",
"STRATA_Q6K_EXPERTS": "ON",
"STRATA_MMQ_KQUANTS": "ON",
"STRATA_EXPERIMENTAL_SM60": "OFF",
"CMAKE_CUDA_ARCHITECTURES": "86-real;89-real;89-virtual"
}
},
{
"name": "hip-compile",
"displayName": "HIP compile-only (the release flags of packaging/build_hip_linux.sh, no tests)",
"description": "Builds the strata target for gfx1100 and gfx1201 with ROCm's hipcc. ggml is the vendored third_party/strata-ggml (-DSTRATA_GGML_DIR=<llama.cpp checkout> overrides it); if hipcc is not on PATH pass -DCMAKE_HIP_COMPILER=<rocm>/llvm/bin/clang++.",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"STRATA_ENABLE_HIP": "ON",
"STRATA_ENABLE_CUDA": "OFF",
"STRATA_BUILD_TESTS": "OFF",
"STRATA_PREFILL_MMQ": "ON",
"STRATA_PORTABLE": "ON",
"CMAKE_HIP_ARCHITECTURES": "gfx1100;gfx1201"
}
}
],
"buildPresets": [
{ "name": "cpu-check", "configurePreset": "cpu-check", "jobs": 0 },
{ "name": "cpu-check-native", "configurePreset": "cpu-check-native", "jobs": 0 },
{ "name": "cuda-compile", "configurePreset": "cuda-compile", "targets": ["strata", "strata_kq"], "jobs": 0 },
{ "name": "hip-compile", "configurePreset": "hip-compile", "targets": ["strata"], "jobs": 0 }
],
"testPresets": [
{
"name": "cpu-check",
"configurePreset": "cpu-check",
"output": { "outputOnFailure": true, "shortProgress": true },
"execution": { "timeout": 120, "noTestsAction": "error", "jobs": 4 }
},
{
"name": "cpu-check-native",
"configurePreset": "cpu-check-native",
"inherits": "cpu-check"
}
]
}
+17 -3
View File
@@ -17,6 +17,7 @@
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Strata herunterladen"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT-Lizenz"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Strata über GitHub Sponsors unterstützen"></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>
@@ -62,7 +63,7 @@ In der **Desktop-App** und im **Terminal**. Was nicht in den Arbeitsspeicher pas
| 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.
Läuft auf NVIDIA RTX 30, 40 und 50 (Windows und Linux), AMD Radeon AI PRO R9700 (Linux), AMD Ryzen AI Max — Strix Halo (Windows) und Apple-Silicon-Macs mit 64 GB oder mehr ([Mac](docs/DEEPSEEK_MAC.md)). 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
@@ -95,7 +96,7 @@ Unsicher, was passt? [Wähle ein Modell für deinen Computer](docs/MODELS.md#pic
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).
<sub>Dateien: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
<sub>Dateien: `Strata-0.2.13-windows-x64-setup.exe` · `Strata-0.2.13-linux-x86_64.AppImage` · `Strata-0.2.13-macos-arm64.dmg`</sub>
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="Strata-Desktop-App mit Chat und Live-Codevorschau nebeneinander">
@@ -175,10 +176,23 @@ Führe **`strata connect`** aus, um die Einrichtung für dein Tool zu sehen. [Ve
## Strata unterstützen
**Hilf mit, lokale KI für alle offen zu halten.** Ein Kaffee hilft, Entwicklung und Tests auf mehr Hardware zu finanzieren.
**Hilf mit, lokale KI für alle offen zu halten.** Ein GitHub-Sponsoring oder ein Kaffee hilft, Entwicklung und Tests auf mehr Hardware zu finanzieren.
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: Strata unterstützen" 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: Strata unterstützen" height="48"></a>
### Treue Unterstützer
Danke an alle, die Strata unterstützen.
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
<!-- supporters:end -->
[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>
+17 -3
View File
@@ -17,6 +17,7 @@
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Descargar Strata"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licencia MIT"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Patrocina Strata en GitHub Sponsors"></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>
@@ -62,7 +63,7 @@ En la **aplicación de escritorio** y en el **Terminal**. Lo que no cabe en la m
| 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.
Funciona en NVIDIA RTX 30, 40 y 50 (Windows y Linux), AMD Radeon AI PRO R9700 (Linux), AMD Ryzen AI Max — Strix Halo (Windows) y Macs con Apple Silicon de 64 GB o más ([Mac](docs/DEEPSEEK_MAC.md)). 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
@@ -95,7 +96,7 @@ Medido con el conjunto de 2.00 bpw en Strata 0.2.5. «Salida» es la velocidad d
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).
<sub>Archivos: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
<sub>Archivos: `Strata-0.2.13-windows-x64-setup.exe` · `Strata-0.2.13-linux-x86_64.AppImage` · `Strata-0.2.13-macos-arm64.dmg`</sub>
<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">
@@ -175,10 +176,23 @@ Ejecuta **`strata connect`** para ver las instrucciones de tu herramienta. [Guí
## Apoya Strata
**Ayuda a mantener la IA local abierta a todos.** Un café ayuda a financiar el desarrollo y las pruebas en más hardware.
**Ayuda a mantener la IA local abierta a todos.** Un patrocinio en GitHub o un café ayuda a financiar el desarrollo y las pruebas en más hardware.
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: patrocina 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: apoya Strata" height="48"></a>
### Apoyos fieles
Gracias a todas las personas que apoyan Strata.
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
<!-- supporters:end -->
[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>
+17 -3
View File
@@ -17,6 +17,7 @@
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Télécharger Strata"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licence MIT"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Soutenir Strata sur GitHub Sponsors"></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>
@@ -62,7 +63,7 @@ Dans l'**application de bureau** et dans le **Terminal**. Ce qui ne tient pas en
| 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.
Fonctionne sur les NVIDIA RTX 30, 40 et 50 (Windows et Linux), l'AMD Radeon AI PRO R9700 (Linux), l'AMD Ryzen AI Max — Strix Halo (Windows) et les Mac Apple Silicon avec 64 Go ou plus ([Mac](docs/DEEPSEEK_MAC.md)). 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
@@ -95,7 +96,7 @@ Vous ne savez pas ce qui tient ? [Choisir un modèle pour votre ordinateur](docs
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).
<sub>Fichiers: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
<sub>Fichiers: `Strata-0.2.13-windows-x64-setup.exe` · `Strata-0.2.13-linux-x86_64.AppImage` · `Strata-0.2.13-macos-arm64.dmg`</sub>
<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">
@@ -175,10 +176,23 @@ Lancez **`strata connect`** pour voir la configuration de votre outil. [Guide de
## Soutenir Strata
**Aidez à garder l'IA locale ouverte à tous.** Un café aide à financer le développement et les tests sur davantage de matériel.
**Aidez à garder l'IA locale ouverte à tous.** Un parrainage sur GitHub ou un café aide à financer le développement et les tests sur davantage de matériel.
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors : soutenir 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 : soutenir Strata" height="48"></a>
### Fidèles soutiens
Merci à toutes les personnes qui soutiennent Strata.
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
<!-- supporters:end -->
[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>
+17 -3
View File
@@ -17,6 +17,7 @@
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Strata をダウンロード"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT ライセンス"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="GitHub Sponsors で Strata を支援する"></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>
@@ -62,7 +63,7 @@
| 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 エンジンをダウンロードし、モデルを起動します。
NVIDIA RTX 30・40・50 シリーズ(Windows と Linux)、AMD Radeon AI PRO R9700(Linux)、AMD Ryzen AI Max — Strix Halo(Windows)、メモリ 64 GB 以上の Apple シリコン搭載 Mac([Mac](docs/DEEPSEEK_MAC.md))で動きます。セットアップがファイルと DeepSeek エンジンをダウンロードし、モデルを起動します。
```sh
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
@@ -95,7 +96,7 @@ Strata 0.2.5 で 2.00 bpw のセットを使って測定しました。「出力
アプリをインストールし、モデルを選んで、チャットを始めましょう。インストーラーはシステムごとに1つです。初回起動時に、アプリがお使いのグラフィックスカード用のエンジンをダウンロードします。[インストーラーのヘルプ](docs/INSTALL.md#the-app)。
<sub>ファイル: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
<sub>ファイル: `Strata-0.2.13-windows-x64-setup.exe` · `Strata-0.2.13-linux-x86_64.AppImage` · `Strata-0.2.13-macos-arm64.dmg`</sub>
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="チャットとライブのコードプレビューを並べて表示した Strata デスクトップアプリ">
@@ -175,10 +176,23 @@ strata launch
## Strata を支援する
**ローカル AI をみんなに開かれたものに。** コーヒー1杯が、開発と、より多くのハードウェアでのテストの支えになります。
**ローカル AI をみんなに開かれたものに。** GitHub でのスポンサーやコーヒー1杯が、開発と、より多くのハードウェアでのテストの支えになります。
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: 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: Strata を支援する" height="48"></a>
### いつも支えてくださる方々
Strata を支えてくださるすべての方に感謝します。
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
<!-- supporters:end -->
[MIT ライセンス](LICENSE) · [クレジットとモデルのライセンス](docs/HOW_IT_WORKS.md#credits) · [プライバシー](docs/SAFETY.md)
<a id="option-c-let-your-ai-set-it-up"></a>
+17 -3
View File
@@ -17,6 +17,7 @@
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Download Strata"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Sponsor Strata on GitHub Sponsors"></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>
@@ -65,7 +66,7 @@ disk space.
| ISTA-DAS Coder (half of the experts removed) | 191 GB | about 197 GB |
Runs on NVIDIA RTX 30, 40 and 50 series (Windows and Linux), AMD Radeon AI PRO R9700 (Linux), AMD Ryzen AI Max — Strix Halo
(Windows), and Apple Silicon Macs with 64 GB or more, text only ([Mac](docs/DEEPSEEK_MAC.md)). Setup downloads the files and the DeepSeek engine, then starts the model.
(Windows), and Apple Silicon Macs with 64 GB or more ([Mac](docs/DEEPSEEK_MAC.md)). Setup downloads the files and the DeepSeek engine, then starts the model.
```sh
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
@@ -100,7 +101,7 @@ Not sure what fits? [Choose a model for your computer](docs/MODELS.md#pick-by-me
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).
<sub>Files: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
<sub>Files: `Strata-0.2.13-windows-x64-setup.exe` · `Strata-0.2.13-linux-x86_64.AppImage` · `Strata-0.2.13-macos-arm64.dmg`</sub>
<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">
@@ -181,10 +182,23 @@ Run **`strata connect`** to see setup instructions for your tool. [Connection gu
## Support Strata
**Help keep local AI open to everyone.** A coffee helps fund development and testing on more hardware.
**Help keep local AI open to everyone.** A GitHub sponsorship or a coffee helps fund development and testing on more hardware.
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: sponsor 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: support Strata" height="48"></a>
### Loyal supporters
Thank you to everyone who supports Strata.
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
<!-- supporters:end -->
[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>
+17 -3
View File
@@ -17,6 +17,7 @@
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Pobierz Stratę"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licencja MIT"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Wesprzyj Stratę na GitHub Sponsors"></a>
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Wesprzyj Stratę na Buy Me a Coffee"></a>
</p>
@@ -64,7 +65,7 @@ dużo wolnego miejsca na dysku.
| 2.25 bpw | 274 GB | około 280 GB |
Działa na kartach NVIDIA RTX serii 30, 40 i 50 (Windows i Linux), AMD Radeon AI PRO R9700 (Linux) oraz AMD Ryzen AI Max — Strix Halo
(Windows). Instalacja pobiera pliki i silnik DeepSeek, a potem uruchamia model.
(Windows) oraz na komputerach Mac z Apple Silicon i co najmniej 64 GB pamięci ([Mac](docs/DEEPSEEK_MAC.md)). Instalacja pobiera pliki i silnik DeepSeek, a potem uruchamia model.
```sh
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
@@ -99,7 +100,7 @@ Nie wiesz, co się zmieści? [Wybierz model do swojego komputera](docs/MODELS.md
Zainstaluj aplikację, wybierz model i zacznij rozmawiać. Dla każdego systemu jest jeden instalator. Przy pierwszym
uruchomieniu aplikacja pobiera silnik dla twojej karty graficznej. [Pomoc z instalatorem](docs/INSTALL.md#the-app).
<sub>Pliki: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
<sub>Pliki: `Strata-0.2.13-windows-x64-setup.exe` · `Strata-0.2.13-linux-x86_64.AppImage` · `Strata-0.2.13-macos-arm64.dmg`</sub>
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="Aplikacja Strata na komputer: czat i podgląd kodu na żywo obok siebie">
@@ -179,10 +180,23 @@ Uruchom **`strata connect`**, aby zobaczyć instrukcję konfiguracji dla swojego
## Wesprzyj Stratę
**Pomóż zachować lokalną sztuczną inteligencję otwartą dla wszystkich.** Kawa pomaga finansować rozwój i testy na większej liczbie sprzętu.
**Pomóż zachować lokalną sztuczną inteligencję otwartą dla wszystkich.** Sponsoring na GitHubie lub kawa pomaga finansować rozwój i testy na większej liczbie sprzętu.
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: wesprzyj Stratę" 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: wesprzyj Stratę" height="48"></a>
### Wierni wspierający
Dziękujemy wszystkim, którzy wspierają Stratę.
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
<!-- supporters:end -->
[Licencja MIT](LICENSE) · [Podziękowania i licencje modeli](docs/HOW_IT_WORKS.md#credits) · [Prywatność](docs/SAFETY.md)
<a id="option-c-let-your-ai-set-it-up"></a>
+17 -3
View File
@@ -17,6 +17,7 @@
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Baixar o Strata"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licença MIT"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Apoie o Strata no GitHub Sponsors"></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>
@@ -62,7 +63,7 @@ No **aplicativo de desktop** e no **Terminal**. O que não cabe na memória é l
| 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.
Roda em NVIDIA RTX 30, 40 e 50 (Windows e Linux), AMD Radeon AI PRO R9700 (Linux), AMD Ryzen AI Max — Strix Halo (Windows) e Macs com Apple Silicon e 64 GB ou mais ([Mac](docs/DEEPSEEK_MAC.md)). 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
@@ -95,7 +96,7 @@ Não sabe o que cabe? [Escolha um modelo para o seu computador](docs/MODELS.md#p
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).
<sub>Arquivos: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
<sub>Arquivos: `Strata-0.2.13-windows-x64-setup.exe` · `Strata-0.2.13-linux-x86_64.AppImage` · `Strata-0.2.13-macos-arm64.dmg`</sub>
<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">
@@ -175,10 +176,23 @@ Execute **`strata connect`** para ver as instruções da sua ferramenta. [Guia d
## Apoie o Strata
**Ajude a manter a IA local aberta para todos.** Um café ajuda a financiar o desenvolvimento e os testes em mais hardware.
**Ajude a manter a IA local aberta para todos.** Um patrocínio no GitHub ou um café ajuda a financiar o desenvolvimento e os testes em mais hardware.
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: apoie o 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: apoie o Strata" height="48"></a>
### Apoiadores fiéis
Obrigado a todas as pessoas que apoiam o Strata.
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
<!-- supporters:end -->
[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>
+17 -3
View File
@@ -17,6 +17,7 @@
<p align="center">
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="下载 Strata"></a>
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT 许可证"></a>
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="在 GitHub Sponsors 上支持 Strata"></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>
@@ -62,7 +63,7 @@
| 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 引擎,然后启动模型。
支持 NVIDIA RTX 30、40 和 50 系列(Windows 和 Linux)、AMD Radeon AI PRO R9700(Linux)、AMD Ryzen AI Max — Strix Halo(Windows),以及内存 64 GB 或以上的 Apple 芯片 Mac([Mac](docs/DEEPSEEK_MAC.md))。安装程序会下载模型文件和 DeepSeek 引擎,然后启动模型。
```sh
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
@@ -95,7 +96,7 @@ strata launch --family deepseek41 --model ist-das-2.00bpw --plan
安装应用,选择模型,就可以开始聊天。每个系统只有一个安装程序。首次启动时,应用会下载适合你显卡的引擎。[安装程序帮助](docs/INSTALL.md#the-app)。
<sub>文件: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
<sub>文件: `Strata-0.2.13-windows-x64-setup.exe` · `Strata-0.2.13-linux-x86_64.AppImage` · `Strata-0.2.13-macos-arm64.dmg`</sub>
<p align="center">
<img src="docs/media/app-chat.png" width="900" alt="Strata 桌面应用,聊天和实时代码预览并排显示">
@@ -175,10 +176,23 @@ strata launch
## 支持 Strata
**帮助本地 AI 保持开放。** 一杯咖啡能帮助我们在更多硬件上开发和测试。
**帮助本地 AI 保持开放。** GitHub 赞助或一杯咖啡,都能帮助我们在更多硬件上开发和测试。
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors:支持 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:支持 Strata" height="48"></a>
### 忠实支持者
感谢每一位支持 Strata 的人。
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
<!-- supporters:end -->
[MIT 许可证](LICENSE) · [致谢和模型许可证](docs/HOW_IT_WORKS.md#credits) · [隐私](docs/SAFETY.md)
<a id="option-c-let-your-ai-set-it-up"></a>
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "strata-desktop",
"productName": "Strata",
"version": "0.2.11",
"version": "0.2.13",
"private": true,
"description": "Strata Desktop - local AI, tuned to one model.",
"scripts": {
+1 -1
View File
@@ -3625,7 +3625,7 @@ checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7"
[[package]]
name = "strata-desktop"
version = "0.2.11"
version = "0.2.13"
dependencies = [
"gtk",
"serde",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "strata-desktop"
version = "0.2.11"
version = "0.2.13"
description = "Strata Desktop — local AI, tuned to one model."
edition = "2021"
rust-version = "1.77"
+44 -5
View File
@@ -1,15 +1,54 @@
# Strata app source
# Strata app: developer guide
Start with [the complete developer handoff](../handoff.md) and [the runbook](../docs/RUNBOOK.md).
This folder is the app's Node host (`server/`) and its web interface (`src/`, React and Vite). The desktop shell that wraps
them is [`../desktop`](../desktop) (Tauri). The engine and Strata Control are Python and C++ and live elsewhere in the
repository; see [docs/SOURCE_INSTALL.md](../../docs/SOURCE_INSTALL.md) and [packaging/README.md](../../packaging/README.md).
## Start from a fresh clone
You need **Node 24.12 or newer** (`engines` in `package.json`) and npm. Nothing else: no model, no engine, no GPU, no
sibling design folder.
```sh
cd apps/host
npm ci
npm run preview:ui
npm run build # makes dist/, which the preview and `npm run serve` need (dist/ is not checked in)
npm run preview:ui # simulated hardware and models; open http://localhost:5272/#/models
```
The built UI is included. To rebuild after source edits, run `npm run build`. `preview:ui` uses isolated simulated data; `npm run dev` connects the normal host.
`preview:ui` starts an isolated host and a fixture server on temporary data and prints "UI PREVIEW ONLY". Nothing it shows
is real: no engine is started, no model is loaded, no files outside its temporary folder are touched. Ctrl+C stops it.
The previous project README is retained in [docs/README-historical.md](docs/README-historical.md). It contains older implementation status and is historical reference.
| Command | What it does |
| --- | --- |
| `npm run build` | Builds the interface into `dist/`. |
| `npm run typecheck` | `tsc --noEmit`. |
| `npm test` | The host and interface tests (`tests/*.test.mjs`, `node --test`). They write to scratch copies of the per-user folders, never to your own profile. A test that hangs fails after 10 minutes (`STRATA_TEST_TIMEOUT_MS`). |
| `npm run dev` | The source host (port 8791) and the Vite dev server (port 5199) together; Vite proxies `/api` to the host. |
| `npm run serve` | The host with the built `dist/` (run `npm run build` first). |
| `npm run check` | Typecheck plus the interface audits (`audit`, `contrast`, `measure`, `luminance`). |
## Preview, source host and packaged Control
- **Simulated preview** (`npm run preview:ui`): fixtures only. Use it to look at screens.
- **Source host** (`npm run dev`, `npm run serve`): the real host code from this checkout. Without `STRATA_REAL_ENGINE=1` it
runs a clearly labelled example engine; with it, the host looks for a Strata installation as a source checkout does.
Loading a real model needs an engine and model files; none of the commands above do that.
- **Packaged app**: the installer ships the host next to Strata Control (Python plus `strata_control`), which does installs,
updates and engine starts. A source checkout has no Control, so those paths keep their older behaviour; how Control is
packaged is in [packaging/README.md](../../packaging/README.md).
The version numbers in `package.json` are the app's. They are checked against `docs/data/release.json` by `python tools/release_manifest.py --check`.
## Styles, icons and fonts
The checked-in files are the source. There is no generator and no sibling design folder.
- Kit styles: `src/styles/kit/tokens.css` and `components.css`; app styles sit on top in `src/styles/*.css`.
- Icons: `public/icons/sprite.svg`. Add an icon as a `<symbol id="i-NAME">`; `npm run audit` fails on an icon that is used but missing.
- Fonts: `public/fonts`.
- The standalone server page (`serve/web/`) serves its own copy of `components.css`. The two copies are byte-identical and
`tests/kit-copy.test.mjs` fails when they differ: change both together.
## Languages
+4 -70
View File
@@ -1,71 +1,5 @@
# Strata UI fixes — handoff for Claude Code
# Moved
This package comes from the Strata UI audit of 7 October 2026: 177 findings across 210 screen states. Here they are split into 19 tasks. Each task file lists the findings it covers, the files to change, and how to check the result.
## What's inside
- `tasks/` has one file per task, ready to give to a subagent.
- `findings.json` has all 177 findings with severity, state, issue, suggested fix and assigned task.
- `designs/` holds the reference designs for T09 (Model) and T10 (Engine v3). They are design-component source files: read them for structure, copy, colours and behaviour. They won't run on their own.
- `.claude/agents/strata-fixer.md` is a subagent definition (Sonnet, focused on one task).
- `PROMPTS.md` has the prompts to paste into Claude Code.
- `REVIEW.md` is the checklist for the review after each phase.
- `PROGRESS.md` is the log that each task appends to.
## Setup
1. Copy this folder into the repo root as `audit-fixes/` (so paths read `audit-fixes/tasks/T01-…`).
2. Copy `audit-fixes/.claude/agents/strata-fixer.md` to `.claude/agents/strata-fixer.md` in the repo.
3. Make sure `docs/evidence/audit-2026-10-07/` (the screenshots) is in the repo, because the task files point to it.
4. Run `npm install`, then `npm run check` and `node --test tests/` once. Note anything that already fails, so it isn't blamed on a task.
## Order
Phase 1 fixes shared components, so later tasks get smaller. Tasks in the same phase with no dependency between them can run in parallel on separate branches.
### Phase 1 — root causes (shared components)
| Task | Title | Findings | High / Med / Low | Depends on |
|---|---|---|---|---|
| T01 | One model, one name | 11 | 6 / 2 / 3 | — |
| T02 | Overlay layering and dialogs | 5 | 2 / 3 / 0 | — |
| T03 | Primary, disabled and on states | 11 | 1 / 7 / 3 | — |
| T04 | One error component, no fixture text | 10 | 0 / 9 / 1 | — |
| T05 | Show numbers when they matter | 6 | 2 / 2 / 2 | T01 |
| T06 | Fix the response hover layout | 1 | 1 / 0 / 0 | — |
| T07 | Hide what is not built | 2 | 0 / 1 / 1 | — |
| T08 | Engine and Model in the main nav | 1 | 1 / 0 / 0 | T01 |
### Phase 2 — rebuild Model and Engine
| Task | Title | Findings | High / Med / Low | Depends on |
|---|---|---|---|---|
| T09 | Build the single-model Model page | 17 | 0 / 9 / 8 | T01, T03, T04 |
| T10 | Build Engine v3 | 17 | 2 / 9 / 6 | T01, T02, T03 |
### Phase 3 — remaining findings per screen
| Task | Title | Findings | High / Med / Low | Depends on |
|---|---|---|---|---|
| T11 | Chat: remaining findings | 38 | 0 / 20 / 18 | T01, T05, T06, T07 |
| T12 | Time-skip: remaining findings | 6 | 0 / 3 / 3 | — |
| T13 | Serving: remaining findings | 19 | 0 / 6 / 13 | T04 |
| T14 | Monitor: remaining findings | 2 | 0 / 1 / 1 | T05 |
| T15 | Skills and Tasks: remaining findings | 9 | 0 / 2 / 7 | T02, T03, T04 |
| T16 | Settings and Setup: remaining findings | 7 | 0 / 2 / 5 | T02, T09 |
| T17 | Navigation and overlays: remaining findings | 4 | 0 / 2 / 2 | T02, T08 |
| T18 | Preview panel: remaining findings | 7 | 0 / 3 / 4 | — |
| T19 | Dark mode: remaining findings | 4 | 0 / 1 / 3 | T02, T03 |
## Running it cheaply
- Do high and medium first. Every task file puts low findings under "Optional polish". Skip them in the first pass. That cuts about a third of the work.
- Use a fresh session per task. Each subagent starts clean with only its task file, so context stays small.
- Check with tests first, and use screenshots only for the states listed in the task.
- After each phase, run one review (see REVIEW.md) instead of reviewing after every task.
Rough cost at API list prices (Sonnet 5.5, medium effort, for the subagents; one review per phase):
- Phase 1 (8 tasks): $10–20
- Phase 2 (2 large tasks): $8–16
- Phase 3 (9 tasks): $10–20, or about half that if you skip the low findings
- Reviews: $8–15
## Ground rules for every task
- Stay within the task's scope. If a fix needs a file that another task owns, make the smallest change and note it in PROGRESS.md.
- Don't change the design kit tokens in `src/styles/kit/` unless the task says so. Run `npm run sync:kit` if you do.
- Keep both themes working. Check light and dark.
- Existing tests must stay green. Add tests for new logic (formatters, clamping, fit rules).
- A finding can be closed as "already fixed by Txx" or "not reproducible", with a one-line reason.
The UI audit-fix campaign (7 October 2026: 19 tasks, the finding register, the review and the scripts that applied them) is finished.
It is kept whole, unchanged, in [history/apps/host/audit-fixes](../../../history/apps/host/audit-fixes/README.md).
Open items from it are in [../docs/BACKLOG.md](../docs/BACKLOG.md).
+54
View File
@@ -0,0 +1,54 @@
# UI backlog from the October 2026 campaign
When the dated UI documents moved to [history/apps/host](../../../history/apps/host/docs), the requirements in them that were not done, or not shown to be done, were copied here.
Each row names its source in `history/`. "Open" means a search of `src/` and `server/` on 7 November 2026 found nothing for it. "Check" means related
code exists but nothing shows the requirement was met or tested. This is a list to triage, not a promise.
## Open
| Item | Source |
| --- | --- |
| Measure dragging, scrolling and streaming together on representative hardware (`scripts/motionbudget.mjs` reports NOT MEASURED). | docs/engine-models-onboarding-2026-10-05.md, Remaining 6 |
| A browser performance benchmark with a 3,000-row model catalog. | docs/engine-models-onboarding-2026-10-05.md, Verification |
| Decide the model card metrics (B08: reading speed, answer speed and task quality) and the profile names (B13: keep Agentic / Balanced / Performance, or rename) with real participants. The shipped choices were provisional. | docs/persona-review-2026-10-05/decision-report.md, Unresolved choices |
| Platform checks never run: a normal-browser export download, launch from the installed manifest, favicon fallbacks, OS appearance changes, forced-colors mode, a deliberate image failure, native icon packaging. | docs/design-improvements-2026-10-05.md, Remaining boundaries 2 |
| A safety cue when the engine settings leave their defaults. | docs/plans/03-engine-tuning.md |
| "Copy as cURL" in the Serving request drawer. | docs/plans/05-serving.md |
| Settings: grouping and per-section resets. | docs/plans/08-settings.md |
| First run: a confirmation about local data. | docs/plans/09-first-run.md |
| Command palette: a Recents group. | docs/plans/11-command-palette.md |
| Time expectations on long operations. | docs/plans/14-states.md |
| An accessibility "fatigue budget". | docs/plans/16-accessibility.md |
## Check
| Item | Source |
| --- | --- |
| A screen-reader pass, forced colors, contrast in every interactive state, the reading-size and focus matrix. 200% browser zoom was checked for Engine, Models and setup only. | engine-models-onboarding (Remaining 5); design-improvements (UI-18); persona implementation-verification |
| Model fit from measured free headroom and the native engine's real footprint (the recommendation uses memory class). | engine-models-onboarding (Remaining 4); design-improvements (MODEL-03) |
| Benchmark evidence: engine build and driver identity, and rules for when a measurement stops being valid. | engine-models-onboarding, Remaining 3 |
| Curated agent and tool candidates, the Balanced ordering and comparable throughput data. | engine-models-onboarding, Remaining 2 |
| Download resume and browser fault injection on the native path. | engine-models-onboarding, Verification and Recovery |
| Real subagent orchestration (the demo engine had none). | annotations-followup-2026-10-09.md |
| PDF input to the model (pictures work). | annotations-followup-2026-10-09.md |
| Scheduled tasks that use file, shell, web, email and calendar tools. | user-feedback-2026-10-06.md |
| LAN sharing as an explicit opt-in, with a test. | user-feedback-2026-10-06.md; dsh-recon.md |
| A Windows and Linux save picker for the Monitor image export. | ui-followup-2026-10-06.md |
| A live Cloudflare tunnel run (only a connector fixture was used). | ui-followup-2026-10-06.md |
| Live microphone dictation (tested with simulated events only). | annotations-19-2026-10-09.md |
| One coordinated arrow-key navigation system for menus (optional). | design-improvements, UI-11-12 |
| Session-log export delivery in a normal browser. | design-improvements, CHAT-08 |
| Windows and Linux runtime lifecycle, native tool and image grants, privacy and logging, native sensors. | design-improvements, Remaining boundaries 1 |
| A human credential workflow test for Serving; native cancellation; assistive-technology announcements for invalid fields. | design-improvements, SERVE-05 to 07, UI-01-04 |
| Plans: first-run scan substance, a labelled theme toggle, gauge animation, skeletons, operation-failed empties, a motion duration budget. | docs/plans/09, 10, 12, 14, 15 |
| Shared items with no recorded closure: the loaded model shown in four places, context described in different words, destructive actions styled like navigation, value fills that jump. | docs/plans/INDEX-CHECKLIST.md, "Present on multiple screens" |
## Not captured individually
- The 16 screen plans list about 190 numbered items, of which the index says about a quarter were executed. The rest stay in
[history/apps/host/docs/plans](../../../history/apps/host/docs/plans/README.md); read the plan for a screen before changing that screen.
- The audit-fix campaign left 80 of its 177 findings as "optional, not scheduled" (all low priority); 91 were implemented and 6 closed by removal.
The finding ids and the task each belongs to are in [history/apps/host/audit-fixes/FINDING-STATUS.md](../../../history/apps/host/audit-fixes/FINDING-STATUS.md).
By task: T11 chat 18, T13 serving 13, T09 model page 8, T15 skills and tasks 7, T10 engine page 6, T16 settings and setup 5, T18 preview panel 4,
T01, T03, T12 and T19 three each, T05 and T17 two each, T04, T07 and T14 one each.
- Declined by the owner, so not backlog: Tuning's nine flat fields in two groups, the Settings section order, renaming "More" to Settings, and repeating About's config paths.
+47
View File
@@ -0,0 +1,47 @@
# App developer docs
Current:
- [interaction-guidelines.md](interaction-guidelines.md): motion, materials, navigation and contrast rules. `scripts/audit-ui.mjs` and the style sheets cite its sections.
- [design-system-notes.md](design-system-notes.md): how the app uses the kit styles in `src/styles/kit/`.
- [BACKLOG.md](BACKLOG.md): UI requirements from the October 2026 campaign that are still open or unchecked.
- [../README.md](../README.md): start here (build, preview, tests).
## How the host is organised
This replaces the 14 October 2026 native-engine handover (kept in [history](../../../history/apps/host/docs/native-engine-handover.md); it was written against engine 0.1.38).
| Module | Responsibility |
| --- | --- |
| `server/native-control.mjs` | The bridge to Strata Control and the installed engine: catalog, status, install jobs, saved job state, per-instance config copies, process start and stop, benchmark, calibration and updates. |
| `server/native-settings.mjs` | Allowed per-model settings, their bounds, MCP configuration validation and credential masking. |
| `server/native-gateway.mjs` | Forwards Chat Completions, Anthropic and Responses requests to the selected instance. |
| `server/chat-images.mjs` | Validation of attached pictures. |
| `src/lib/native.ts` | Shared native state and polling. |
| `src/components/NativeModels.tsx` | Model setup, device assignment, version management and load. |
| `src/components/NativeSettings.tsx`, `NativeMcpEditor.tsx` | Per-model parameters, JSON and tool-server configuration. |
| `src/components/NativeMonitor.tsx` | Rates per instance, per-instance detail and host measurements. |
Contracts that still hold:
- Each loaded instance runs `serve/server.py` on its own private loopback port with a random API key, from a config copy in the app's data folder.
The one public URL shown in Serving forwards to it; the public model id is an alias in that config, and unknown or ambiguous ids fail before streaming.
- A source checkout finds a Strata installation through `STRATA_NATIVE_REPO` (it must contain `setup.py`, `tools/strata_mcp.py` and `serve/server.py`) and a Python through
`STRATA_PYTHON` or `STRATA_NATIVE_PYTHON` (`server/python-resolver.mjs`). The installed app uses the Control that ships beside it instead.
- Saving settings validates first, keeps unrelated keys and non-Strata environment values, writes a `.bak` and replaces the config atomically. New settings apply on the next load.
## Moved to history
Pointer: [history/README.md](../../../history/README.md). The files, unchanged:
| Was | Now |
| --- | --- |
| `audit-fixes/` (next to this folder) | [history/apps/host/audit-fixes](../../../history/apps/host/audit-fixes/README.md) |
| `README-historical.md` | [history/apps/host/docs/README-historical.md](../../../history/apps/host/docs/README-historical.md) |
| `native-engine-handover.md` | [history/apps/host/docs/native-engine-handover.md](../../../history/apps/host/docs/native-engine-handover.md) |
| `annotations-19-2026-10-09.md`, `annotations-32-2026-10-09.md`, `annotations-followup-2026-10-09.md` | [history/apps/host/docs/](../../../history/apps/host/docs) |
| `chat-shell-2026-10-06.md`, `ui-followup-2026-10-06.md` | [history/apps/host/docs/](../../../history/apps/host/docs) |
| `design-improvements-2026-10-05.md`, `dsh-recon.md`, `engine-models-onboarding-2026-10-05.md` | [history/apps/host/docs/](../../../history/apps/host/docs) |
| `user-feedback-2026-10-05.md`, `user-feedback-2026-10-06.md` | [history/apps/host/docs/](../../../history/apps/host/docs) |
| `persona-review-2026-10-05/` | [history/apps/host/docs/persona-review-2026-10-05](../../../history/apps/host/docs/persona-review-2026-10-05/decision-report.md) |
| `plans/` | [history/apps/host/docs/plans](../../../history/apps/host/docs/plans/README.md) |
+2 -6
View File
@@ -7,17 +7,13 @@ tints composited over `--st-surface` first.
## 1. How the app consumes the kit
`src/styles/kit/{tokens,components}.css` are synced verbatim from `../ui` by
`npm run sync:kit` (the only rewrite is making the `@font-face` URLs absolute).
`src/styles/kit/{tokens,components}.css` are the checked-in kit styles and the source of truth
(there is no generator; the kit was first imported from a separate design workspace that is not in this repository).
`src/styles/app.css` sits on top and contains, in order: extensions to the kit's
classes, app shell layout, utilities, app-only components, and the first-run
takeover. There are no gradients in it and no literal colours — every value is
either a kit token or a layout constant.
```
npm run sync:kit # re-import tokens/components/fonts/icons from ../ui
```
Every component the kit defines is in use:
| Kit class | Where the app uses it |
+1 -1
View File
@@ -6,7 +6,7 @@ measure` / `npm run motion` (runtime), so a violation fails a command rather tha
a code review.
Companion documents: [`design-system-notes.md`](design-system-notes.md) for how
the app maps onto the kit, [`dsh-recon.md`](dsh-recon.md) for the product this
the app maps onto the kit, [`dsh-recon.md`](../../../history/apps/host/docs/dsh-recon.md) (archived) for the product this
one is modelled on.
---
@@ -0,0 +1,5 @@
# Moved
The 5 October 2026 persona review (eight personas, debates, ballot, decision report) is kept in
[history/apps/host/docs/persona-review-2026-10-05](../../../../history/apps/host/docs/persona-review-2026-10-05/decision-report.md).
Open requirements from it are in [../BACKLOG.md](../BACKLOG.md).
+3 -66
View File
@@ -1,67 +1,4 @@
# Strata · per-screen improvement plans
# Moved
One plan per page and per scenario, written against the two rulebooks the app is
built on — [interaction-guidelines.md](../interaction-guidelines.md) (Strata's own
rules) and Apple's Liquid Glass HIG — and against what the code actually does
today, not what it is meant to do.
Every plan follows the same shape: **what it is today** (grounded in the file and
in measured numbers), **where it falls short**, **the plan**, **motion**, and
**how the result is verified**. Claims about the present are checkable with the
scripts in `scripts/`; claims about the future are written so they can be checked
the same way once built.
## Screens
| # | Page | Route | Plan |
|---|---|---|---|
| 01 | Chat | `#/chat` | [01-chat.md](01-chat.md) |
| 02 | Models | `#/models` | [02-models.md](02-models.md) |
| 03 | Engine tuning | `#/tuning` | [03-engine-tuning.md](03-engine-tuning.md) |
| 04 | Monitor | `#/monitor` | [04-monitor.md](04-monitor.md) |
| 05 | Serving | `#/serving` | [05-serving.md](05-serving.md) |
| 06 | Workspace | `#/workspace` | [06-workspace.md](06-workspace.md) |
| 07 | Skills | `#/skills` | [07-skills.md](07-skills.md) |
| 08 | Settings | `#/settings` | [08-settings.md](08-settings.md) |
| 09 | First run | modal | [09-first-run.md](09-first-run.md) |
## Scenarios
| # | Scenario | Plan |
|---|---|---|
| 10 | Sidebar and navigation | [10-sidebar.md](10-sidebar.md) |
| 11 | Command palette | [11-command-palette.md](11-command-palette.md) |
| 12 | Context rail | [12-context-rail.md](12-context-rail.md) |
| 13 | Overlays — drawers, menus, selects, popovers | [13-overlays.md](13-overlays.md) |
| 14 | Empty, loading and error states | [14-states.md](14-states.md) |
| 15 | Motion and fluidity | [15-motion.md](15-motion.md) |
| 16 | Accessibility and long sessions | [16-accessibility.md](16-accessibility.md) |
## The shared vocabulary these plans assume
Fixed by the design system, so plans do not re-litigate them:
- **Light is the reference theme.** Every change is designed light-first and
verified in dark, never the reverse.
- **Capsules are for controls; the 8/14/20/24 scale is for surfaces.** Audit-enforced.
- **Glass is only for the floating control layer.** Glass never samples glass;
`npm run luminance` fails the build if a material ends up nested in another.
- **One motion scale** — 100/160/240/320/460 ms, one standard curve, springs only
for direct manipulation. No layout property is transitioned directly; grid
tracks and opacity carry the movement.
- **Contrast is measured on rendered pairings**, compositing alpha, in every
interactive state.
- **Reading measure is capped** at 68 characters in the conversation, verified at
10, 16 and 22 px.
## Verification harness these plans refer to
```
npm run check typecheck + audit + contrast + measure + luminance
npm run audit off-scale values, materials, elevation, capsules, icons, native selects
npm run contrast rendered pairings, composited, per state, both themes
npm run measure characters per line at every content size
npm run luminance dark-mode brightness budget + material layering
npm run motion interruptibility, frame-sampled contact sheets
npm run flow <name> scripted screen flows; `tour` and `tour-dark` cover every route
```
The 18 screen plans and their checklist (October 2026) are kept in [history/apps/host/docs/plans](../../../../history/apps/host/docs/plans/README.md).
Open requirements from them are in [../BACKLOG.md](../BACKLOG.md).
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "strata",
"version": "0.2.11",
"version": "0.2.13",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "strata",
"version": "0.2.11",
"version": "0.2.13",
"dependencies": {
"html-to-image": "^1.11.13",
"katex": "^0.19.0",
+2 -3
View File
@@ -1,7 +1,7 @@
{
"name": "strata",
"private": true,
"version": "0.2.11",
"version": "0.2.13",
"type": "module",
"description": "Strata \u2014 local AI, tuned to one model. Web shell + engine host.",
"scripts": {
@@ -12,8 +12,7 @@
"serve": "node server/index.mjs --dist dist",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
"sync:kit": "node scripts/sync-design-kit.mjs",
"shot": "node scripts/shoot.mjs",
"shot": "node scripts/shoot.mjs",
"flow": "node scripts/flow.mjs",
"audit": "node scripts/audit-ui.mjs",
"contrast": "node scripts/contrast.mjs",
+3 -20
View File
@@ -17,7 +17,6 @@ import { fileURLToPath } from 'node:url';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const STYLE_DIR = join(ROOT, 'src/styles');
const KIT_SRC = join(ROOT, '..', 'ui');
const KIT_COPY = join(ROOT, 'src/styles/kit');
/** The app's own stylesheets, in cascade order: the last one wins. */
@@ -264,24 +263,8 @@ const allAppCss = APP_SHEETS.map((sheet) => sheet.css).join('\n');
/* ------------------------------------------------- kit sync + motion scale -- */
for (const file of ['tokens.css', 'components.css']) {
const a = join(KIT_SRC, file);
const b = join(KIT_COPY, file);
// Some checkouts of this branch do not carry the design-kit source (apps/ui),
// only the synced copy. There is nothing to compare against then, so the check
// reports that instead of failing every run with a condition no one can fix.
if (!existsSync(a)) {
notes.push(`kit copy: ${file} has no source in this checkout — drift check skipped`);
continue;
}
if (!existsSync(b)) {
fail('kit', 0, `${file} missing from src/styles/kit — run npm run sync:kit`);
continue;
}
// The sync rewrites @font-face URLs to absolute; everything else must match.
const norm = (t) => t.replace(/url\((["']?)\/fonts\//g, 'url($1fonts/').trim();
if (norm(readFileSync(a, 'utf8')) !== norm(readFileSync(b, 'utf8'))) {
fail('kit', 0, `${file} has drifted from ../ui — run npm run sync:kit`);
}
// The checked-in files in src/styles/kit are the source of the kit styles; there is no generator.
if (!existsSync(join(KIT_COPY, file))) fail('kit', 0, `${file} is missing from src/styles/kit (restore it from git)`);
}
// Which selectors have been given the capsule radius. Checked across the whole
@@ -328,7 +311,7 @@ for (const file of [...readdirSync(join(ROOT, 'src'), { recursive: true })].filt
for (const m of readFileSync(full, 'utf8').matchAll(/<(?:Icon|IconBtn)\s[^>]*?\bname="([a-z-]+)"/g)) referenced.add(m[1]);
}
for (const name of referenced) {
if (!symbols.has(name)) fail('src', 0, `icon "${name}" is referenced but not in the sprite — run npm run sync:kit`);
if (!symbols.has(name)) fail('src', 0, `icon "${name}" is referenced but not in the sprite: add a <symbol id="i-NAME"> to public/icons/sprite.svg`);
}
/* ---------------------------------------------------------- undefined classes -- */
+18
View File
@@ -202,6 +202,24 @@ add("Drafting: off (the cards were short of memory)",
"Previsão: desligada (faltou memória nas placas)",
"초안 예측: 꺼짐(카드 메모리가 부족했습니다)",
"Anticipation : désactivée (les cartes manquaient de mémoire)")
add("On your Mac, DFlash2 was faster than MTP on this model in our tests.",
"在您的 Mac 上,我们的测试中 DFlash2 在此模型上比 MTP 更快。",
"お使いの Mac では、私たちのテストでこのモデルにおいて DFlash2 は MTP より速くなりました。",
"Auf Ihrem Mac war DFlash2 bei diesem Modell in unseren Tests schneller als MTP.",
"En su Mac, en nuestras pruebas DFlash2 fue más rápido que MTP con este modelo.",
"На вашем Mac в наших тестах DFlash2 с этой моделью был быстрее MTP.",
"No seu Mac, nos nossos testes o DFlash2 foi mais rápido que o MTP com este modelo.",
"고객님의 Mac에서 저희 테스트 결과 이 모델에서는 DFlash2가 MTP보다 빨랐습니다.",
"Sur votre Mac, DFlash2 a été plus rapide que MTP avec ce modèle lors de nos tests.")
add("DFlash2 is off by default on this graphics card for now; choose it in Drafting to use it.",
"目前在这张显卡上 DFlash2 默认关闭;如需使用,请在“预测草稿”中选择它。",
"現在、このグラフィックスカードでは DFlash2 は既定でオフです。使うには「先読み」で選んでください。",
"DFlash2 ist auf dieser Grafikkarte vorerst standardmäßig aus; wählen Sie es unter „Vorausraten“ aus, um es zu nutzen.",
"DFlash2 está desactivado de forma predeterminada en esta tarjeta gráfica por ahora; elíjalo en Predicción para usarlo.",
"На этой видеокарте DFlash2 пока выключен по умолчанию; чтобы использовать его, выберите его в разделе «Предсказание».",
"O DFlash2 está desligado por predefinição nesta placa de vídeo por agora; escolha-o em Previsão para o usar.",
"현재 이 그래픽 카드에서는 DFlash2가 기본적으로 꺼져 있습니다. 사용하려면 ‘초안 예측’에서 선택하세요.",
"DFlash2 est désactivé par défaut sur cette carte graphique pour le moment ; choisissez-le dans Anticipation pour l'utiliser.")
here = Path(__file__).resolve().parent.parent / "src" / "locales"
NBSP = " "
-87
View File
@@ -1,87 +0,0 @@
/**
* Sync the Strata UI kit (../ui) into the app's public/ folder.
*
* node scripts/sync-design-kit.mjs
*
* Copies fonts, and rebuilds a single clean icon sprite at public/icons/sprite.svg.
* The kit's own SVG files carry C2PA manifest metadata (~12 KB each, base64);
* we strip <metadata> so the shipped sprite stays small.
*
* Also mirrors tokens.css / components.css into src/styles/kit/ so the app has a
* versioned copy that can be diffed against the kit.
*/
import { readFileSync, writeFileSync, mkdirSync, readdirSync, copyFileSync, existsSync } from 'node:fs';
import { join, dirname, basename } from 'node:path';
import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
const root = join(here, '..');
const kit = join(root, '..', 'ui');
const pub = join(root, 'public');
const styles = join(root, 'src', 'styles', 'kit');
if (!existsSync(kit)) {
console.error(`Design kit not found at ${kit}`);
process.exit(1);
}
mkdirSync(join(pub, 'fonts'), { recursive: true });
mkdirSync(join(pub, 'icons'), { recursive: true });
mkdirSync(styles, { recursive: true });
// ---- fonts -----------------------------------------------------------------
let fonts = 0;
for (const f of readdirSync(join(kit, 'fonts'))) {
if (!f.endsWith('.woff2')) continue;
copyFileSync(join(kit, 'fonts', f), join(pub, 'fonts', f));
fonts++;
}
// ---- icon sprite -----------------------------------------------------------
const strip = (svg) =>
svg
.replace(/<metadata>[\s\S]*?<\/metadata>/g, '')
.replace(/<svg[^>]*>/, '')
.replace(/<\/svg>\s*$/, '')
.replace(/\s+xmlns(:\w+)?="[^"]*"/g, '')
.trim();
const iconDir = join(kit, 'icons');
const sources = new Map(readdirSync(iconDir)
.filter((f) => f.endsWith('.svg') && f !== 'sprite.svg')
.map((f) => [basename(f, '.svg'), join(iconDir, f)]));
const appIconDir = join(root, 'src', 'assets', 'icons');
if (existsSync(appIconDir)) {
for (const file of readdirSync(appIconDir).filter(f => f.endsWith('.svg'))) {
sources.set(basename(file, '.svg'), join(appIconDir, file));
}
}
const names = [...sources.keys()].sort();
const symbols = names.map((name) => {
const raw = readFileSync(sources.get(name), 'utf8');
const body = strip(raw);
const viewBox = /viewBox="([^"]+)"/.exec(raw)?.[1] ?? '0 0 24 24';
// The root <svg> carries fill="none"; without it every stroked path inherits
// the default black fill and the icons render as solid blobs.
const rootFill = /<svg[^>]*\sfill="([^"]+)"/.exec(raw)?.[1] ?? 'none';
return ` <symbol id="i-${name}" viewBox="${viewBox}" fill="${rootFill}">\n ${body}\n </symbol>`;
});
const sprite = `<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">\n${symbols.join('\n')}\n</svg>\n`;
writeFileSync(join(pub, 'icons', 'sprite.svg'), sprite);
// ---- styles ----------------------------------------------------------------
// The kit references fonts relatively (fonts/x.woff2). In the app the fonts are
// served from public/fonts, so rewrite the URLs to be absolute — otherwise the
// browser resolves them against the CSS file and 404s in both dev and build.
for (const f of ['tokens.css', 'components.css']) {
const css = readFileSync(join(kit, f), 'utf8').replace(/url\((["']?)fonts\//g, 'url($1/fonts/');
writeFileSync(join(styles, f), css);
}
const kb = (n) => `${(n / 1024).toFixed(1)} KB`;
console.log(`synced ${fonts} fonts -> public/fonts/`);
console.log(`sprite: ${names.length} icons -> public/icons/sprite.svg (${kb(sprite.length)})`);
console.log(`styles: tokens.css, components.css -> src/styles/kit/`);
console.log(`icons: ${names.join(', ')}`);
+3 -1
View File
@@ -11,6 +11,8 @@ 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});
/* A test that never finishes fails after STRATA_TEST_TIMEOUT_MS (default 10 minutes) instead of holding the whole run for ever; extra arguments (a reporter, --test-name-pattern) go to node --test. */
const timeout=Number(process.env.STRATA_TEST_TIMEOUT_MS)||600000;
const child=spawn(process.execPath,['--test',`--test-timeout=${timeout}`,...process.argv.slice(2),...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);});
+1 -1
View File
@@ -6,7 +6,7 @@
* per saved API connection, so that each chat can pick its own model. This module writes the same two
* Cordis entries (`agent-default-model`, `llm-pi-ai`) for the current set.
*
* Rules carried over from the integration notes (docs/work/IMPLEMENTATION_STATUS.md section 1.8):
* Rules carried over from the integration notes (history/docs/work/IMPLEMENTATION_STATUS.md section 1.8):
* - `llm-pi-ai` config is `{ providers: { "<route>": {...} } }`; a route key at the top level is
* silently ignored.
* - the output budget must fit the whole request, so `maxTokens` is derived from the real context.
+1 -1
View File
@@ -22,7 +22,7 @@ export function refusedFolder(dir) {
const win = (process.env.SystemRoot ?? 'C:\\Windows').toLowerCase();
const q = p.toLowerCase();
if (q === win || q.startsWith(win + sep) || /^[a-z]:\\(program files( \(x86\))?|programdata)(\\|$)/.test(q)) return 'That is a system folder. Choose one of your own folders.';
} else if (/^\/(bin|boot|dev|etc|lib|lib64|proc|root|run|sbin|sys|usr|var)(\/|$)/.test(p)) return 'That is a system folder. Choose one of your own folders.';
} else if (/^\/(bin|boot|dev|etc|lib|lib64|proc|root|run|sbin|sys|usr|var)(\/|$)/.test(p) && !/^\/run\/media\/[^/]+\/./.test(p)) return 'That is a system folder. Choose one of your own folders.';
return null;
}
+5 -5
View File
@@ -288,18 +288,18 @@ export function createControl({ control = findControl(), env = process.env, run
throw Object.assign(failure(result, 'The partial copies could not be removed.'), { code: result.json?.error?.code ?? 'leave_failed', details: result.json?.error?.details ?? '' });
},
/**
* The drafter for a model's run config and the drafting choice (`strata drafting plan`): { needed, gb, line, note, drafter, split, mtp_needed, mtp_gb }. Control decides
* The drafter for a model's run config and the drafting choice (`strata drafting plan`): { needed, tuned_needed, gb, drafter_gb, tuned, file, line, note, drafter, split, mtp_needed, mtp_gb }. `model` (with `family`) names a model that has no run config yet. Control decides
* (the table of strata_control/drafting.py and the all-cards split engine's rule, strata_control/split_engine.py: the same answer as the Terminal's); the app never reads
* it. Nothing is downloaded. `noSplit` asks as if the split engine will not take the start (only after the draft layer for the split could not be downloaded).
*/
async draftingPlan({ configPath = null, mode = null, family = null, dataDir = null, noSplit = false, unloading = false } = {}) {
const result = await call(['drafting', 'plan', ...(configPath ? ['--config', configPath] : []), ...(mode ? ['--mode', mode] : []), ...(family ? ['--family', family] : []), ...(dataDir ? ['--data-dir', dataDir] : []), ...(noSplit ? ['--no-split'] : []), ...(unloading ? ['--engines-unloading'] : []), '--output', 'json'], { timeoutMs: 30_000 });
async draftingPlan({ configPath = null, mode = null, family = null, model = null, dataDir = null, noSplit = false, unloading = false } = {}) {
const result = await call(['drafting', 'plan', ...(configPath ? ['--config', configPath] : []), ...(mode ? ['--mode', mode] : []), ...(family ? ['--family', family] : []), ...(model && !configPath ? ['--model', model] : []), ...(dataDir ? ['--data-dir', dataDir] : []), ...(noSplit ? ['--no-split'] : []), ...(unloading ? ['--engines-unloading'] : []), '--output', 'json'], { timeoutMs: 30_000 });
if (result.json?.ok) return result.json;
throw failure(result, 'The drafter could not be checked.');
},
/** Fetch and pack the drafter when the choice would use it and it is not here (`strata drafting fetch --if-needed`); `onEvent` gets {step, done, total}. */
async draftingFetch({ configPath, mode = null, noSplit = false, unloading = false, onEvent = null, signal = null } = {}) {
const args = ['drafting', 'fetch', '--config', configPath, ...(mode ? ['--mode', mode] : []), ...(noSplit ? ['--no-split'] : []), ...(unloading ? ['--engines-unloading'] : []), '--if-needed', '--output', 'jsonl'];
async draftingFetch({ configPath, mode = null, noSplit = false, unloading = false, throttle = false, onEvent = null, signal = null } = {}) {
const args = ['drafting', 'fetch', '--config', configPath, ...(mode ? ['--mode', mode] : []), ...(noSplit ? ['--no-split'] : []), ...(unloading ? ['--engines-unloading'] : []), '--if-needed', ...(throttle ? ['--throttle'] : []), '--output', 'jsonl'];
const result = await call(args, { timeoutMs: 3 * 3600_000, acquire: true, onLine: onEvent ?? (() => {}), signal });
if (result.json?.ok) return result.json;
throw Object.assign(new ControlError(result.json?.error?.message ?? (result.stderr.split(/\r?\n/).filter(Boolean).pop() || 'The DFlash2 drafter could not be downloaded.'), { exitCode: result.code, output: result.json }), { code: 'drafting_failed' });
+1 -1
View File
@@ -1098,7 +1098,7 @@ async function api(req, res, path, method, url) {
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);
/* 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==='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,hints:await native.settingsHints?.(nativeSettings[1])});}
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);
if(nativeSettingsFile&&method==='GET')return json(res,200,native.settingsFile(nativeSettingsFile[1]));
+30 -5
View File
@@ -7,7 +7,7 @@ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, realpa
import { join, resolve, dirname, basename, sep, relative, isAbsolute } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createServer } from 'node:net';
import { NATIVE_FIELDS, APP_OWNED_FIELDS, withoutDraftLayer, resolveDrafter, validateNativeSettings, validateMcpServers, publicMcpServers, publicNativeEnv, restoreNativeEnv, speedSwitchRows } from './native-settings.mjs';
import { draftingHint, NATIVE_FIELDS, APP_OWNED_FIELDS, withoutDraftLayer, resolveDrafter, validateNativeSettings, validateMcpServers, publicMcpServers, publicNativeEnv, restoreNativeEnv, speedSwitchRows } from './native-settings.mjs';
import { resourceSettings, saferStart, reserveChoice } from './native-resources.mjs';
import { USER_KEY, capture as captureUserArgs, merge as mergeUserArgs, setFlag as setUserFlag, stamped as stampedUserArgs } from './native-user-args.mjs';
import { resolvePython, venvPython } from './python-resolver.mjs';
@@ -164,11 +164,13 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
/* 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 checkedSettingsConfig(text){const check=checkAndParseRunConfig(text);if(!check.ok)throw new Error(`The settings file for this model has a mistake, so the model was not started. ${describeIssues(check.issues,'error').join(' ')}`);return check.config;}
function settings(model){const {entry,config}=record(model);const values=Object.fromEntries(NATIVE_FIELDS.map(f=>[f.id,config[f.id]??null]));if(typeof values.aliases==='string')values.aliases=values.aliases.split(',').map(v=>v.trim()).filter(Boolean);values.sampling=config.sampling??{};values.args=config.args??[];values.env=publicNativeEnv(config.env??{});return {model,values,mcp:publicMcpServers(config.mcp_servers??config.mcpServers??{}),restartNeeded:true,capabilities:{dsExperts:ownEngine(config)&&dsExpertsSupported(config)},speedSwitches:speedSwitchRows(config.env??{},{deepseek:ownEngine(config),arch:config.arch??null,backend:config.backend??entry?.backend??snapshot.installed?.engine?.backend??null,gpus:snapshot.hardware?.gpus??[]})};}
/* The Drafting hint of this model's settings: the size of the drafter DFlash2 would fetch for it (Control's plan with the choice DFlash2); the generic hint when Control does not say. */
async function settingsHints(model){try{if(!packaged||typeof control.draftingPlan!=='function')return {};const {path}=recordFile(model),d=await control.draftingPlan({configPath:path,mode:'dflash2'});return Number(d?.drafter_gb)>0?{drafting:draftingHint(d.drafter_gb)}:{};}catch{return {};}}
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). */
const choice=args.drafting??(dflash2Served===true?'auto':undefined);if(packaged&&typeof choice==='string'&&typeof control.draftingPlan==='function'){try{const size=snapshot.catalog?.families?.find(f=>f.family===args.family)?.sizes.find(s=>s.model===args.model),here=snapshot.installed?.models?.find(m=>m.model===size?.id);const d=await control.draftingPlan({mode:choice,family:args.family??null,dataDir:here?.runDataFolder??snapshot.installed?.data_folder??null,...(unloadsRunning(args)?{unloading:true}:{})});if(d.needed||d.mtp_needed)return {...result,drafting:{needed:true,drafter:d.needed?'dflash2':'mtp',gb:d.needed?d.gb:d.mtp_gb,line:d.line}};}catch{/* the model plan still stands */}}
const choice=args.drafting??(dflash2Served===true?'auto':undefined);if(packaged&&typeof choice==='string'&&typeof control.draftingPlan==='function'){try{const size=snapshot.catalog?.families?.find(f=>f.family===args.family)?.sizes.find(s=>s.model===args.model),here=snapshot.installed?.models?.find(m=>m.model===size?.id);const d=await control.draftingPlan({mode:choice,family:args.family??null,model:args.model??null,dataDir:here?.runDataFolder??snapshot.installed?.data_folder??null,...(unloadsRunning(args)?{unloading:true}:{})});if(d.needed||d.mtp_needed)return {...result,drafting:{needed:true,drafter:d.needed?'dflash2':'mtp',gb:d.needed?d.gb:d.mtp_gb,line:d.line,...(d.needed&&d.tuned===true?{tuned:true}:{})}};}catch{/* the model plan still stands */}}
return result;}
/* The folder the person picked in the first-run chooser can be the one that holds their GGUF files (or a folder inside it). It is theirs, not a Strata data folder: packs are never made in it, and a Strata folder around it (<Strata>/models/<tag>) is used instead (#3507, #3424). */
function ownFilesDataFolder(candidate,args){if(!candidate||args.data_dir!==undefined||!args.gguf_dir)return candidate;const same=(a,b)=>{const x=resolve(a),y=resolve(b);return process.platform==='win32'?x.toLowerCase()===y.toLowerCase():x===y;};const inside=(a,b)=>{const rel=relative(resolve(b),resolve(a));return rel===''||(!!rel&&!rel.startsWith('..')&&!isAbsolute(rel));};if(!same(candidate,args.gguf_dir)&&!inside(candidate,args.gguf_dir))return candidate;const models=dirname(resolve(args.gguf_dir)),top=dirname(models);return basename(models).toLowerCase()==='models'&&/strata/i.test(basename(top))?top:undefined;}
@@ -326,12 +328,31 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
async function draftPlan(args,configFile,{noSplit=false}={}){
if(!packaged||typeof control.draftingPlan!=='function')return null;
const asked=args.values?.drafting;
/* The plan is for the cards this Load uses: the screen's own card choice goes over the ones the saved settings name (the all-cards split engine needs two or more). */
try{return await withPlanFile(args,configFile,planFile=>control.draftingPlan({configPath:planFile,...(asked===undefined?{}:{mode:asked===null?'auto':asked}),...(noSplit?{noSplit:true}:{}),...(unloadsRunning(args)?{unloading:true}:{})}));}catch{return null;}
}
/* The plan is for the cards this Load uses: the screen's own card choice goes over the ones the saved settings name (the all-cards split engine needs two or more). `use` gets the file to hand to Control; a temporary one is removed after. */
async function withPlanFile(args,configFile,use){
let planFile=configFile,temporary=null;
if(Array.isArray(args.gpus)&&args.gpus.length&&args.gpus.every(Number.isInteger)){try{const work=join(directory,'native-instances');mkdirSync(work,{recursive:true});temporary=join(work,'drafting-plan-'+randomUUID()+'.json');writeFileSync(temporary,JSON.stringify({...readJson(configFile),gpu:args.gpus.length===1?args.gpus[0]:args.gpus}),{mode:0o600});planFile=temporary;}catch{temporary=null;planFile=configFile;}}
try{return await control.draftingPlan({configPath:planFile,...(asked===undefined?{}:{mode:asked===null?'auto':asked}),...(noSplit?{noSplit:true}:{}),...(unloadsRunning(args)?{unloading:true}:{})});}catch{return null;}
try{return await use(planFile);}
finally{if(temporary){try{rmSync(temporary,{force:true});}catch{/* best effort */}}}
}
/* Auto and its drafter (the tuned one where the set has one, else the standard DFlash2 drafter: whatever Control's Auto plan says is `needed`), on every path that starts a model: a plain load, a restart of the stopped models and a tuning run fetch it as the Models screen's own Load does (Control: pinned, checked, marked, licence and notice), but they were not asked for it, so
the 12 hour rule of the Terminal's launch applies (--throttle: a feed that had nothing a short while ago is not asked again, a feed that does not answer is not an error). A choice the person named or saved (MTP, DFlash2, off) wins: this never runs for it. */
function autoChoice(value){return value==null||String(value).toLowerCase()==='auto';}
async function fetchAutoDrafter(job,args,configFile,drafts,{noSplit=false,after}={}){
if(!packaged||typeof control.draftingFetch!=='function'||drafts?.needed!==true||!autoChoice(readJson(configFile).drafting))return false;
job.engineAbort??=new AbortController();
try{
const got=await withPlanFile(args,configFile,planFile=>control.draftingFetch({configPath:planFile,mode:'auto',throttle:true,...(noSplit?{noSplit:true}:{}),...(unloadsRunning(args)?{unloading:true}:{}),signal:job.engineAbort.signal,onEvent:event=>{if(event.event==='progress'&&job.state==='running'){job.summary=event.step;job.details=event.total>1&&event.done<event.total?`${(event.done/1e9).toFixed(2)} of ${(event.total/1e9).toFixed(2)} GB`:undefined;emit();}}}));
if(job.state==='running'&&after){job.summary=after;job.details=undefined;emit();}
return got?.pack!=null&&got?.skipped!==true;
}catch(error){if(job.state==='running'){job.drafterNote=error.message;if(after){job.summary=after;job.details=undefined;}emit();}return false;}
}
/* The same for a path that has no Load of its own around it (a restart of stopped models, a tuning run): plan the saved settings file, then fetch. */
async function autoDrafterFor(job,model,after){
try{const {path}=recordFile(model),args={model,multiple:true},drafts=await draftPlan(args,path);return await fetchAutoDrafter(job,args,path,drafts,{after});}catch{return false;}
}
/* The choice changed to the MTP and its layer is not here yet: it is fetched once, by the installer (finished steps are skipped), in this Load. Only the Models screen's own Load carries the choice; a restart or a tuning run never fetches. */
async function installMtpLayer(job,args,config){
const fam=snapshot.catalog?.families?.find(f=>f.sizes.some(s=>s.id===args.model)),size=fam?.sizes.find(s=>s.id===args.model);
@@ -434,6 +455,8 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
if(drafts?.mtp_needed===true&&(args.values?.drafting!==undefined||drafts.split===true&&(config.drafting==null||String(config.drafting).toLowerCase()==='auto'))){/* the Models screen's own Load carries the choice; a plain Auto load fetches the layer too when Control says the all-cards split engine will take the start (it runs the MTP and nothing else): the same as the Terminal */await installMtpLayer(job,args,config);if(job.state!=='running')return;await refresh(true);config=checkedSettingsConfig(readFileSync(recordFile(args.model).path,'utf8'));drafts=await draftPlan(args,configFile);
/* The draft layer for the split could not be had (no internet, disk): the load goes on as the 0.2.10 app did, the normal layer split with the plan DFlash2's again (the Models screen's own Load fetches that drafter below). One plain line says so. */
if(drafts?.split===true&&drafts.mtp_needed===true){noSplit=true;job.drafterNote=job.drafterNote??'The MTP drafter could not be downloaded.';job.summary='Loading the model…';emit();drafts=await draftPlan(args,configFile,{noSplit:true});}}
/* A plain load (the Models screen's own Load carries its choice and fetches below): Auto on a set that has the tuned drafter fetches it now, so the start uses it. Not on this computer and not reachable: the MTP or the stock drafter runs as before. */
if(args.values?.drafting===undefined&&drafts?.needed===true){const got=await fetchAutoDrafter(job,args,configFile,drafts,{noSplit,after:'Loading the model…'});if(job.state!=='running')return;if(got)drafts=await draftPlan(args,configFile,{noSplit});}
const {cfg,resources,visionPreferences,parallelChoice,parallelNote:chosenNote,dsNote}=buildConfig(args,config,drafts?.choice==='dflash2'&&drafts.needed===true&&args.values?.drafting===undefined?'mtp':drafts?.choice??null);/* (a drafter that is not here and will not be fetched by this load: the MTP runs, and the request slots stay) */
let parallelNote=chosenNote;
await prepareDrafter(job,args,cfg,configFile,drafts,{noSplit});if(job.state!=='running')return;
@@ -607,6 +630,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
for(const old of offer.instances){
const instance={...old,status:'loading',phase:'loading',error:null,metrics:null,loadedAt:new Date().toISOString()};
instances.set(instance.id,instance);if(!defaultId||!instances.has(defaultId))defaultId=instance.id;job.summary=`Starting ${instance.label}. Loading can take a few minutes.`;emit();
await autoDrafterFor(job,instance.model,`Starting ${instance.label}. Loading can take a few minutes.`);
let wait=960;try{const {config,entry}=record(instance.model);wait=startWaitFor(config,entry,instance.model);}catch{/* the usual wait */}
try{
await control.start(instance.profile,{waitSeconds:wait});
@@ -661,6 +685,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
const {path}=tuneConfig(args);
if(externalRunning().length)throw new Error('Stop the native server running outside this UI before tuning.');
if(busy(requests().filter(r=>instances.has(r.instanceId))))throw new Error('Finish requests before tuning.');
await autoDrafterFor(job,args.model,'Finding the fastest settings for your PC…');if(job.state!=='running')return;
for(const id of [...instances.keys()])await unload(id);
if(job.state!=='running')return;
job.summary='Finding the fastest settings for your PC…';emit();
@@ -767,7 +792,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
// Remove nonserializable process references from public jobs.
const publicState=()=>{const value=state();value.jobs=value.jobs.map(({process,engineAbort,rawResult,...job})=>job);return value;};
async function localPreview(path){if(!packaged)throw new Error('Checking local model files is part of the Strata app.');if(typeof path!=='string'||!path.trim()||[...path].some(c=>c.charCodeAt(0)<32))throw new Error('Choose a folder.');return control.importPlan(path.trim());}
return {state:publicState,tuneStatus,tuneEffective,tuneOff,tunePresets:{list:()=>autotuner.list(),action:(kind,id,extra)=>autotuner.action(kind,id,extra),undoFor:async args=>autotuner.undoFor(tuneConfig(args).path)},refresh,localPreview,configure,setDownloadDirectory,planModelsFolder,startModelsFolder,interruptedMove,finishInterruptedMove,leaveInterruptedMove,plan,settings,save,settingsFile,openSettingsFile,operation,dismissRestart,cancel,unload,resolveInstance,poll,logs:()=>logs.slice(),benchmarkReport:()=>readJson(join(directory,'native-benchmark.json'),null),setDefault(id){defaultId=resolveInstance(id).id;emit();return publicState();},hasLive:()=>[...instances.values()].some(i=>i.status==='ready'),readyInstances:()=>[...instances.values()].filter(i=>i.status==='ready'),defaultInstanceId:()=>defaultId,close};
return {state:publicState,settingsHints,tuneStatus,tuneEffective,tuneOff,tunePresets:{list:()=>autotuner.list(),action:(kind,id,extra)=>autotuner.action(kind,id,extra),undoFor:async args=>autotuner.undoFor(tuneConfig(args).path)},refresh,localPreview,configure,setDownloadDirectory,planModelsFolder,startModelsFolder,interruptedMove,finishInterruptedMove,leaveInterruptedMove,plan,settings,save,settingsFile,openSettingsFile,operation,dismissRestart,cancel,unload,resolveInstance,poll,logs:()=>logs.slice(),benchmarkReport:()=>readJson(join(directory,'native-benchmark.json'),null),setDefault(id){defaultId=resolveInstance(id).id;emit();return publicState();},hasLive:()=>[...instances.values()].some(i=>i.status==='ready'),readyInstances:()=>[...instances.values()].filter(i=>i.status==='ready'),defaultInstanceId:()=>defaultId,close};
}
/* A DeepSeek config on AMD written by setup 0.2.8 or earlier names ONE card and marks it "asked", though setup picked it itself (strata_control/gpu_pick.py): it must not stop the app's recommended cards from applying. A card setup named for itself is the first of the usable cards, largest memory first; another card number was the person's. Roles and a "gpus_by" marker are always honoured. */
export function setupPickedOneCard(config,hardware){
+1 -1
View File
@@ -1,7 +1,7 @@
/** Preserve native protocol events and tool payloads; the gateway owns identity and cancellation. */
import { once } from 'node:events';
export async function nativeGateway({req,res,path,body,control,serving,fetcher=fetch}){
let instance;try{instance=control.resolveInstance(body.model);}catch(e){res.writeHead(400,{'content-type':'application/json'});res.end(JSON.stringify({error:{message:e.message,type:'invalid_request_error',param:'model',code:null}}));return;}
let instance;try{instance=control.resolveInstance(body.model);}catch(e){/* #5312: an Anthropic client sends a model name of its own (Claude Desktop: claude-5-5-sonnet) and cannot change it: with exactly one model loaded the route answers with it */if(path==='/v1/messages'&&body.model){try{instance=control.resolveInstance(undefined);}catch{}}if(!instance){res.writeHead(400,{'content-type':'application/json'});res.end(JSON.stringify({error:{message:e.message,type:'invalid_request_error',param:'model',code:null}}));return;}}
const abort=new AbortController(),started=Date.now();let sawDone=false;/* #4690: a client that hangs up after `data: [DONE]` (the OpenAI SDK does) has the whole answer: that is not a cancel. */res.on('close',()=>{if(!res.writableEnded&&!sawDone)abort.abort();});
const entry=serving.open({client:req.headers['user-agent']?.slice(0,100)??'API client',endpoint:path.slice(3),model:instance.apiModelId,instanceId:instance.id,gpuIds:instance.gpus.map(String),cancel:()=>abort.abort()});
let tokensIn=null,tokensOut=null,tps=null,promptTps=null;
+8 -19
View File
@@ -1,5 +1,8 @@
import { validMcpName, MCP_NAME_HELP } from './mcp-names.mjs';
import { SPEED_SWITCH_TABLE, DRAFTING_FAMILIES as GENERATED_DRAFTING_FAMILIES, DFLASH2_MODEL_PATTERN } from './policy-generated.mjs';
/** Editable native keys. Unknown config keys and program paths stay intact. */
/* The Drafting hint, word for word, with the size of the drafter this model would really fetch (the tuned one is 0.34 GB, 0.52 on an Arc card; the stock one 1.2). */
export function draftingHint(gb=1.2){const size=Number.isFinite(Number(gb))&&Number(gb)>0?Number(gb):1.2;return `Which drafter guesses words ahead to make answers faster, one at a time. Auto (recommended) is DFlash2 for Qwen3.8-Flash-Next where the engine has it, the MTP otherwise; on two or more identical graphics cards that the all-cards split engine takes, it is that engine with the MTP draft layer. DFlash2 downloads a ${size} GB drafter once, the MTP its draft layer once. With Auto, several requests at once use the MTP. If you choose DFlash2 yourself, it runs with the number of requests you set. If DFlash2 cannot run, the MTP is used alone and Details says why. Off turns the drafter off.`;}
export const NATIVE_FIELDS=[
{id:'reasoning_budget_tokens',label:'Thinking token budget',group:'Responses',type:'number',min:0,max:1048576,hint:'Leave blank for the model default.'},
{id:'fit_max_tokens',label:'Fit output to remaining context',group:'Responses',type:'boolean',hint:'Shortens the requested output cap when the prompt leaves less room.'},
@@ -9,7 +12,7 @@ export const NATIVE_FIELDS=[
{id:'idle_unload_s',label:'Unload after idle seconds',group:'Serving',type:'number',min:0,max:86400,hint:'Zero keeps the model loaded.'},
{id:'lazy_load',label:'Load on first request',group:'Serving',type:'boolean'},
{id:'mtp',notForModel:'deepseek',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',notForModel:'deepseek',label:'Drafting',group:'Serving',type:'enum',choices:['auto','mtp','dflash2','off'],hint:'Which drafter guesses words ahead to make answers faster, one at a time. Auto (recommended) is DFlash2 for Qwen3.8-Flash-Next where the engine has it, the MTP otherwise; on two or more identical graphics cards that the all-cards split engine takes, it is that engine with the MTP draft layer. DFlash2 downloads a 1.2 GB drafter once, the MTP its draft layer once. With Auto, several requests at once use the MTP. If you choose DFlash2 yourself, it runs with the number of requests you set. If DFlash2 cannot run, the MTP is used alone and Details says why. Off turns the drafter off.'},
{id:'drafting',notForModel:'deepseek',label:'Drafting',group:'Serving',type:'enum',choices:['auto','mtp','dflash2','off'],hint:draftingHint(1.2)},
{id:'batchMtp',notForModel:'deepseek',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.'},
@@ -36,22 +39,8 @@ export const ENV_FLAG=/^(?:STRATA|DS4)_[A-Z0-9_]+$/;
* turns it on, and the STRATA_ROUTE_PRIOR=0 that 0.2.7's Off wrote is the default state now. The other two are on by default. The names are the engine switch contract (phaseA-tests/notes-027-plan.md); strata_control/speed_switches.py says the
* same for the Terminal (tests/terminal/test_speed_switches.py compares the two).
*/
export const SPEED_SWITCHES=[
{id:'faster_long_prompts',label:'Faster long prompts',group:'Speed',
hint:'Reads very long prompts faster by approximating part of the work. Off by default: answers about long documents can miss or mix up details.',
default:'off',env:{STRATA_KVA:'1'}},
{id:'faster_12gb_nvidia',label:'Faster answers on 12 GB NVIDIA cards',group:'Speed',
hint:'Prefers the parts of the model already on the graphics card, which makes answers faster on 12 GB cards. Off by default: in long agent sessions it can repeat itself and give weaker results. Turn on if you mostly chat and want the speed.',
default:'off',env:{STRATA_ROUTE_PRIOR:'1.5'}},
{id:'faster_radeon_9000',label:'Faster answers on Radeon RX 9000 / AI PRO R9700',group:'Speed',
hint:'Shares the work between the graphics card and the processor in a way that makes answers faster on these cards. Turn off to go back to the previous setting, which is slower.',
default:'on',env:{STRATA_PCIE_FRAC_DEFAULT:'old'}},
{id:'deepseek_speedups',label:'DeepSeek speed-ups',group:'Speed',
hint:'Reads long prompts faster and loads the parts of the model it needs faster. Turn off if DeepSeek stops working or its answers look wrong; it then runs as in version 0.2.6.',
default:'on',env:{DS4_DEFAULTS:'026',DS4_FAST_ATTN:'0',DS4_ATTN_SLAB:'0',DS4_COPY_PIN:'0',DS4_COPY_HUGETLB:'0',DS4_STORE_SPARE:'0',DS4_STORE_PUT:'0'},
/* an earlier test build wrote only these six for the same state: they read as the switch being set, and go with it */
legacy:{DS4_FAST_ATTN:'0',DS4_ATTN_SLAB:'0',DS4_COPY_PIN:'0',DS4_COPY_HUGETLB:'0',DS4_STORE_SPARE:'0',DS4_STORE_PUT:'0'}},
];
/* The table (ids, words, defaults, environment entries) is generated from strata_control/speed_switches.py into policy-generated.mjs; this file only adds the group the screen lists them under and the rules for where a switch applies (below), which tests/speed-switches.test.mjs and tests/terminal/test_speed_switches.py run against the same cases. */
export const SPEED_SWITCHES=SPEED_SWITCH_TABLE.map(s=>({id:s.id,label:s.label,group:'Speed',hint:s.hint,default:s.default,env:s.env,...(s.legacy?{legacy:s.legacy}:{})}));
const GFX12=/^gfx120[01]$/i, RADEON_9000=/\b(?:RX\s*9\d{3}|AI\s*PRO\s*R97\d{2}|R97\d{2})\b/i;
/**
* Which switches make sense for this model on this PC: only those are shown. `backend` is the engine's (cuda, hip, sycl, metal, cpu). `arch` is the "arch"
@@ -88,8 +77,8 @@ export function speedSwitchRows(env,context){return SPEED_SWITCHES.filter(s=>spe
/** Settings the app itself applies when it starts a model (they are not read from the engine's source, so the engine's text never says it supports them). */
export const APP_OWNED_FIELDS=['mtp','batchMtp','drafting','ds_experts'];
/** The families that have a Drafting choice and the model names the DFlash2 drafter was made for (strata_control/drafting.py SUPPORTED_FAMILIES, _SUPPORTED_NAME): Qwen3.8-Flash-Next, Gyro and Unsloth's quantizations of the same weights; not Swift, Coder or NVFP4. */
export const DRAFTING_FAMILIES=['qwen','gyro','unsloth'];
const DFLASH2_MODEL=/^qwen3\.8-flash-next(-gyro-[a-z0-9._-]+|-unsloth-[a-z0-9._-]+|-(q|iq|tq)\d[a-z0-9_.]*)?$/i;
export const DRAFTING_FAMILIES=GENERATED_DRAFTING_FAMILIES;
const DFLASH2_MODEL=new RegExp(DFLASH2_MODEL_PATTERN,'i');
/** The drafter a choice means (one per run; strata_control/drafting.py choice_of): Auto is DFlash2 where the engine has it (`served`, Control's word) and the model is one it was made for, else the MTP; off and mtp are what they say. `model` is a run config's model name or a family. */
/** `split` (Control's plan: the all-cards split engine will take this start) makes Auto the MTP, the draft layer that engine runs; a DFlash2 the person chose by name stays. */
export function resolveDrafter(choice,model,served,split){
+13
View File
@@ -0,0 +1,13 @@
// GENERATED by `python -m strata_control.app_policy` from strata_control/speed_switches.py and strata_control/drafting.py. Do not edit it by hand:
// change the Python and regenerate; tests/terminal/test_app_policy.py and apps/host/tests/policy-generated.test.mjs fail when this file is out of date.
/** The speed switches of the app's Expert settings (strata_control/speed_switches.py app_switches()): id, words, default, the environment entries outside the default. */
export const SPEED_SWITCH_TABLE = [
{"id": "faster_long_prompts", "label": "Faster long prompts", "hint": "Reads very long prompts faster by approximating part of the work. Off by default: answers about long documents can miss or mix up details.", "default": "off", "env": {"STRATA_KVA": "1"}},
{"id": "faster_12gb_nvidia", "label": "Faster answers on 12 GB NVIDIA cards", "hint": "Prefers the parts of the model already on the graphics card, which makes answers faster on 12 GB cards. Off by default: in long agent sessions it can repeat itself and give weaker results. Turn on if you mostly chat and want the speed.", "default": "off", "env": {"STRATA_ROUTE_PRIOR": "1.5"}},
{"id": "faster_radeon_9000", "label": "Faster answers on Radeon RX 9000 / AI PRO R9700", "hint": "Shares the work between the graphics card and the processor in a way that makes answers faster on these cards. Turn off to go back to the previous setting, which is slower.", "default": "on", "env": {"STRATA_PCIE_FRAC_DEFAULT": "old"}},
{"id": "deepseek_speedups", "label": "DeepSeek speed-ups", "hint": "Reads long prompts faster and loads the parts of the model it needs faster. Turn off if DeepSeek stops working or its answers look wrong; it then runs as in version 0.2.6.", "default": "on", "env": {"DS4_DEFAULTS": "026", "DS4_FAST_ATTN": "0", "DS4_ATTN_SLAB": "0", "DS4_COPY_PIN": "0", "DS4_COPY_HUGETLB": "0", "DS4_STORE_SPARE": "0", "DS4_STORE_PUT": "0"}, "legacy": {"DS4_FAST_ATTN": "0", "DS4_ATTN_SLAB": "0", "DS4_COPY_PIN": "0", "DS4_COPY_HUGETLB": "0", "DS4_STORE_SPARE": "0", "DS4_STORE_PUT": "0"}},
];
/** The families that have a Drafting choice (strata_control/drafting.py SUPPORTED_FAMILIES). */
export const DRAFTING_FAMILIES = ["qwen", "gyro", "unsloth"];
/** The model names the DFlash2 drafter was made for (strata_control/drafting.py _SUPPORTED_NAME), a case-insensitive pattern. */
export const DFLASH2_MODEL_PATTERN = "^qwen3\\.8-flash-next(-gyro-[a-z0-9._-]+|-unsloth-[a-z0-9._-]+|-(q|iq|tq)\\d[a-z0-9_.]*)?$";
+3 -1
View File
@@ -28,7 +28,9 @@ export function endTree(child, signal = 'SIGTERM') {
if (child.pid) {
try { process.kill(-child.pid, signal); return; } catch { /* no such group: it did not lead one, or it is gone */ }
}
child.kill(signal);
// A child that never started has no pid. On macOS, kill() on it right after a failed spawn signals the caller's own process group (pid 0), so
// it is left alone: there is nothing to end.
if (child.pid) child.kill(signal);
} catch { /* already gone */ }
}
+21 -13
View File
@@ -1,7 +1,7 @@
import { tr } from '../lib/i18n';
/** App shell: routes, setup takeover, palette, toasts. */
import { useNative } from '../lib/native';
import { Component, useEffect, useRef, useState, type ReactNode } from 'react';
import { Component, Suspense, useDeferredValue, useEffect, useRef, useState, type ReactNode } from 'react';
import { useSurfaceFocus } from '../lib/focus';
import { closeSettings, useLocation } from '../lib/router';
import { bootOnce, bootstrap, setNavOpen, setFocus, useStore, } from '../lib/store';
@@ -13,14 +13,16 @@ import { MOD, handleAppShortcut, runCommand } from '../lib/commands';
import { useMedia } from '../lib/responsive';
import { Btn, Dialog, IconBtn, Toasts } from '../components/ui';
import { Brand } from '../components/Brand';
import { SetupScreen } from '../screens/Setup';
import { ChatScreen } from '../screens/Chat';
import { ModelRunScreen } from '../screens/ModelRun';
import { ServingScreen } from '../screens/Serving';
import { MonitorScreen } from '../screens/Monitor';
import { TasksScreen } from '../screens/Tasks';
import { SkillsScreen } from '../screens/Skills';
import { SettingsScreen } from '../screens/Settings';
import { lazyScreen, useLoadedModule } from '../lib/lazy-screen';
// The start screen is Chat. Every other screen loads the first time it is opened (7.1), so the first start reads and runs less code.
const loadSetup = () => import('../screens/Setup');
const loadSettings = () => import('../screens/Settings');
const ModelRunScreen = lazyScreen(() => import('../screens/ModelRun'), (m) => m.ModelRunScreen);
const ServingScreen = lazyScreen(() => import('../screens/Serving'), (m) => m.ServingScreen);
const MonitorScreen = lazyScreen(() => import('../screens/Monitor'), (m) => m.MonitorScreen);
const TasksScreen = lazyScreen(() => import('../screens/Tasks'), (m) => m.TasksScreen);
const SkillsScreen = lazyScreen(() => import('../screens/Skills'), (m) => m.SkillsScreen);
function AppShell() {
useStore(s => s.language);
const location = useLocation();
@@ -49,7 +51,13 @@ function AppShell() {
// demanding five steps before anything appears.
const setupOpen = location.route === 'setup' || (!!setup && !setup.complete && !setup.dismissed) || firstRunOpen;
// Hook runs here, after setupOpen exists — see src/lib/focus.ts.
useSurfaceFocus(setupOpen, setupRef);
// The dialogs open once their code has loaded, so they appear and take focus exactly as before.
const setupCode = useLoadedModule(loadSetup, setupOpen);
const setupShown = setupOpen && !!setupCode;
useSurfaceFocus(setupShown, setupRef);
const settingsWanted = location.query.get('settings') === '1';
const settingsCode = useLoadedModule(loadSettings, settingsWanted);
const routeShown = useDeferredValue(location.route);
return (<div className="app" data-rail={compact ? !navOpen : rail} data-layout={mobile ? 'mobile' : compact ? 'compact' : 'desktop'} data-nav-open={navOpen || undefined} data-focus={focus && location.route === 'chat' || undefined}>
<Sidebar containerRef={navRef}/>
{tr(navOpen && compact ? <button className="nav-scrim" tabIndex={-1} aria-hidden="true" aria-label={tr("Close navigation")} onClick={() => setNavOpen(false)}/> : null)}
@@ -62,16 +70,16 @@ function AppShell() {
</div>
<ConnectionBar />
{tr(focus && location.route === 'chat' ? <FocusBar /> : null)}
{tr(!ready ? <BootSkeleton /> : <RouteView route={location.route}/>)}
{tr(!ready ? <BootSkeleton /> : <Suspense fallback={<BootSkeleton />}><RouteView route={routeShown}/></Suspense>)}
</main>
<CommandPalette open={paletteOpen}/>
<Dialog open={location.query.get('settings') === '1'} title={tr("Settings")} className="settings-dialog" onClose={closeSettings}><SettingsScreen /></Dialog>
<Dialog open={settingsWanted && !!settingsCode} title={tr("Settings")} className="settings-dialog" onClose={closeSettings}>{settingsCode ? <settingsCode.SettingsScreen /> : null}</Dialog>
<ShortcutsDialog />
<FolderPathDialog />
{tr(setupOpen ? (<div className="setupmodal" role="dialog" aria-modal="true" aria-label={tr("Set up Strata")}>
{tr(setupShown ? (<div className="setupmodal" role="dialog" aria-modal="true" aria-label={tr("Set up Strata")}>
<div className="scrim" data-open="true" aria-hidden="true"/>
<div className="setupmodal__surface" ref={setupRef}>
<SetupScreen />
{setupCode ? <setupCode.SetupScreen /> : null}
</div>
</div>) : null)}
<Toasts />
+1 -1
View File
@@ -7,7 +7,7 @@ Original SVGs supplied by Niko in `modelandweights.zip`, 10 October 2026.
The originals retain their metadata and vector geometry. The app sprite strips
metadata and uses their 24 x 24 viewBox, currentColor, 1.5px strokes, and 12% fills.
`scripts/sync-design-kit.mjs` includes these files when rebuilding the kit sprite.
They are already in the checked-in sprite, `public/icons/sprite.svg`, which is the source: a new icon is added there as a `<symbol id="i-NAME">`.
Also supplied by Niko on 10 October 2026:
+15 -6
View File
@@ -10,6 +10,7 @@ import { Icon } from './ui';
*/
export function FamilySwitch({ families, selected, onPick }: { families: CatalogModel[]; selected?: string; onPick: (family: CatalogModel) => void }) {
const shown = families.filter(f => f.sizes.length);
const missing = families.filter(f => !f.sizes.length && f.available === false); // a model of the update feed this engine does not run: said, never offered
const trigger = useRef<HTMLButtonElement>(null);
const [open, setOpen] = useState(false);
if (!shown.length) return null;
@@ -19,12 +20,20 @@ export function FamilySwitch({ families, selected, onPick }: { families: Catalog
<Icon name="models" size={20}/><strong>{tr(current.title)}</strong><Icon name="chevron" size={16} className="family-trigger__chev"/>
</button>
{tr(open ? <FloatingMenu anchor={trigger.current} title={tr("Model family")} width={320} onClose={() => setOpen(false)}>
<div className="family-menu">
{shown.map(f => <button key={f.family} type="button" role="menuitemradio" aria-checked={f.family === current.family} className="family-menu__item" onClick={() => { setOpen(false); if (f.family !== current.family) onPick(f); }}>
<Icon name="models" size={18}/><span><strong>{tr(f.title)}</strong>{f.experimental ? <small>{tr("Experimental")}</small> : null}</span>
{f.family === current.family ? <Icon name="check" size={16} className="family-menu__check"/> : null}
</button>)}
</div>
<FamilyMenuList shown={shown} missing={missing} current={current} onPick={f => { setOpen(false); if (f.family !== current.family) onPick(f); }}/>
</FloatingMenu> : null)}
</>;
}
/** The rows of the family menu: the families to pick, then (as notes, not choices) the models of the update feed this engine cannot use, each with the plain reason. */
export function FamilyMenuList({ shown, missing, current, onPick }: { shown: CatalogModel[]; missing: CatalogModel[]; current: CatalogModel; onPick: (family: CatalogModel) => void }) {
return (
<div className="family-menu">
{shown.map(f => <button key={f.family} type="button" role="menuitemradio" aria-checked={f.family === current.family} className="family-menu__item" onClick={() => onPick(f)}>
<Icon name="models" size={18}/><span><strong>{tr(f.title)}</strong>{f.experimental ? <small>{tr("Experimental")}</small> : null}{f.available === false && f.unavailable_reason ? <small>{f.unavailable_reason}</small> : null}</span>
{f.family === current.family ? <Icon name="check" size={16} className="family-menu__check"/> : null}
</button>)}
{missing.map(f => <div key={f.family} className="family-menu__item" role="note" aria-disabled="true"><Icon name="models" size={18}/><span><strong>{f.title}</strong><small>{f.unavailable_reason}</small></span></div>)}
</div>
);
}
+2 -2
View File
@@ -129,7 +129,7 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
const cardsGpus: number[] = roleCards.length > 1 ? roleOrder(effectiveRoles, roleCards) : [];
const chosenCards = cardsGpus;
const fitHardware = { ram_gb: pcRam ? Math.max(0, pcRam - keptRam) : pcRam, vram_gb: Math.max(0, (roleCards.length > 1 ? vramOn(effectiveRoles, roleCards) : pcVram) - keptVramOn(chosenCards)) };
// How the chosen version sits on this computer (the same rule as the version list): the page says a word only for "SSD streaming" and "Doesn't fit".
// How the chosen version sits on this computer (the same rule as the version list): the page says a word only for "may run slower".
const fitState = selectedSize ? versionFit(selectedSize, fitHardware, { context, kv }) : 'unknown';
// A layer split written for another number of cards no longer fits: back to automatic.
useEffect(() => { if (cardsGpus.length <= 1 && split !== 'auto') setSplit('auto'); }, [cardsGpus.length]);
@@ -251,7 +251,7 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
<div className="native-choice__head"><span className="native-choice__label">{tr("Version")}</span><span className="native-choice__tools"><NativePresets values={values} onApply={v => { const f = families.find(f => f.family === v.family || f.sizes.some(s => s.sourceFamily === v.family)); if (!f)
return; presetPending.current = true; setFamily(f.family); setSize(f.sizes.find(s => s.model === v.model && (s.sourceFamily === v.family || f.family === v.family))?.id ?? ''); setContext(v.context ?? 32768); setVision(v.vision ?? 'no'); setKv(v.kv ?? 'int8'); setLowRam(v.low_ram ?? 'auto'); }}/><Btn icon="models" size="sm" variant="ghost" onClick={() => setManage(true)}>{tr("Manage models")}</Btn></span></div>
<div className="native-version"><VersionPicker pick={native?.catalog?.recommendation} versions={selectedFamily?.sizes ?? []} details={expert} selectedId={selectedSize.id} hardware={fitHardware} cache={{ context, kv, drafter: fitDrafter }} downloaded={readyIds} onSelect={id => { const f = families.find(f => f.sizes.some(s => s.id === id)); if (f) { setFamily(f.family); setSize(id); setError(''); } }} onOwnGguf={() => (onImport ?? onOtherModels)()}/></div>
{tr(fitState === 'ssd' ? <p className="small muted native-spill-note"><Icon name="disk" size={16}/>{tr("SSD streaming · will run slower")}</p> : fitState === 'no' ? <p className="small muted native-spill-note"><Icon name="warning" size={16}/>{tr("Doesn't fit")}</p> : null)}
{tr(fitState === 'ssd' ? <p className="small muted native-spill-note"><Icon name="disk" size={16}/>{tr("may run slower")}</p> : null)}
{tr(selectedSize?.multi_file && deepseekContextNote(context) ? <p className="small muted native-spill-note"><Icon name="info" size={16}/>{tr(deepseekContextNote(context))}</p> : null)}
<div className="native-choice__row">
<div className="native-choice__col"><span className="native-choice__label">{tr("Conversation context")}</span><Select ariaLabel={tr("Conversation context")} value={String(context)} options={contextChoices(contextList, context).map(n => ({ value: String(n), label: n % 1024 === 0 ? `${n / 1024}K` : `${n.toLocaleString()} tokens` }))} onChange={n => { setContextPicked(true); setContext(Number(n)); }}/></div>
+4 -1
View File
@@ -28,6 +28,7 @@ export function NativeSettings({ model }: {
}) {
const native = useNative();
const [revision,setRevision]=useState(0);
const [hints, setHints] = useState<Record<string, string>>({}); // a field's hint worded for this model (the Drafting size)
const [switches, setSwitches] = useState<SpeedSwitch[]>([]); // the speed switches that apply to this model on this PC
const [dsExperts, setDsExperts] = useState(false); // the installed DeepSeek engine knows --experts (0.1.50 or newer)
const [tuned, setTuned] = useState<{ preset: { name: string } | null; changed: TunedChange[] } | null>(null);
@@ -44,6 +45,7 @@ export function NativeSettings({ model }: {
values: Record<string, any>;
mcp: Record<string, any>;
tuned?: { preset: { name: string } | null; changed: TunedChange[] } | null;
hints?: Record<string, string>;
capabilities?: { dsExperts?: boolean };
speedSwitches?: SpeedSwitch[];
}>(`/api/native/models/${model}/settings`).then(input => {
@@ -52,6 +54,7 @@ export function NativeSettings({ model }: {
setDsExperts(!!input.capabilities?.dsExperts);
setSwitches(input.speedSwitches ?? []);
setTuned(input.tuned ?? null);
setHints(input.hints ?? {});
const value = nativeSettingsResponse(input);
setValues(value.values);
setText(JSON.stringify(value.values, null, 2));
@@ -120,7 +123,7 @@ export function NativeSettings({ model }: {
const rows = (native?.fields ?? []).filter(f => (!f.forModel || (model.toLowerCase().includes(f.forModel) && (f.id !== 'ds_experts' || dsExperts))) && (!f.notForModel || !model.toLowerCase().includes(f.notForModel)) && (f.id !== 'drafting' || draftingOffered) && `${tr(f.label)} ${f.id} ${tr(f.group)}`.toLowerCase().includes(query.toLowerCase()));
const switchRows = switches.filter(sw => `${tr(sw.label)} ${sw.id} ${tr('Speed')}`.toLowerCase().includes(query.toLowerCase()));
return <details className="native-settings"><summary>{tr("Native engine settings")}</summary>{tr(tuned?.changed?.length ? <div className="native-tuned"><p className="small"><strong>{tr("Your tuning '{0}' sets", [tuned.preset?.name ?? ''])}</strong></p><ul>{tuned.changed.map(c => { const d = describeChange(c); return <li key={c.flag}><span>{d.name}</span><b>{d.now || tr('off')}</b>{d.was !== null ? <small className="muted">{tr("was {0}", [d.was])}</small> : null}</li>; })}</ul></div> : null)}<p className="small muted">{tr(!original ? 'Reading saved settings… ' : '')}{tr("Settings for this model. Blank values use Strata\u2019s defaults.")}</p><div className="editor-tabs" role="tablist" aria-label={tr("Native settings editor")}>{tr(['fields', 'json', 'tools'].map(t => <button key={t} role="tab" aria-selected={tab === t} onClick={() => chooseTab(t)}>{tr(t === 'fields' ? 'Parameters' : t === 'json' ? 'JSON' : 'MCP tools')}</button>))}</div>
{tr(tab === 'fields' ? <><input className="st-input" type="search" aria-label={tr("Search native parameters")} placeholder={tr("Search parameters…")} value={query} onChange={e => setQuery(e.target.value)}/>{tr(['Responses', 'GPU & memory', 'Serving', 'Image reading', 'Tools', 'Recovery'].map(group => rows.some(f => f.group === group) ? <section className="native-settings-group" key={group}><h3 className="h4">{tr(group)}</h3>{tr(rows.filter(f => f.group === group).map(f => <label className="native-setting-row" key={f.id}><span><strong>{tr(f.label)}</strong>{tr(f.supported === false ? <small>{tr("Requires a newer Strata installation.")}</small> : f.hint ? <small>{tr(f.hint)}</small> : null)}</span><NativeFieldInput field={f} model={model} value={values[f.id]} onChange={v => change(f.id, v)}/></label>))}</section> : null))}{tr(switchRows.length ? <section className="native-settings-group"><h3 className="h4">{tr("Speed")}</h3>{tr(switchRows.map(sw => <label className="native-setting-row" key={sw.id}><span><strong>{tr(sw.label)}</strong><small>{tr(sw.hint)}</small></span><Select ariaLabel={tr(sw.label)} value={switchIsOn(sw, values.env) ? 'on' : 'off'} options={sw.default === 'on' ? [{ value: 'on', label: 'On (recommended)' }, { value: 'off', label: 'Off' }] : [{ value: 'off', label: 'Off (recommended)' }, { value: 'on', label: 'On' }]} onChange={v => changeSwitch(sw, v === 'on')}/></label>))}</section> : null)}<section className="native-settings-group"><h3 className="h4">{tr("Sampling")}</h3>{tr([['temperature', 'Temperature'], ['top_p', 'Top-p'], ['top_k', 'Top-k'], ['min_p', 'Min-p'], ['repetition_penalty', 'Repetition penalty'], ['presence_penalty', 'Presence penalty'], ['frequency_penalty', 'Frequency penalty'], ['penalty_last_n', 'Penalty window'], ['seed', 'Seed']].map(([id, label]) => <label className="native-setting-row" key={id}><span><strong>{tr(label)}</strong></span><DecimalInput ariaLabel={tr("Native {0}", [label])} value={values.sampling?.[id] ?? null} onChange={n => { const sampling = { ...values.sampling }; if (n === null) delete sampling[id]; else sampling[id] = n; change('sampling', sampling); }}/></label>))}</section></> : tab === 'json' ? <><p className="small muted">{tr("Engine arguments and STRATA environment flags are editable here. Program paths and network access remain managed by the host.")}</p><textarea className="st-input native-json" aria-label={tr("Native configuration JSON")} spellCheck={false} value={text} onChange={e => { setText(e.target.value); setNotice(''); }}/></> : <><p className="small muted">{tr("Add local programs or HTTP MCP servers. Connected tools can read or change the resources you give them access to. Enable tools in chat when you want the model to use them.")}</p><NativeMcpEditor text={mcp} onChange={value => { setMcp(value); setNotice(''); }}/><details><summary className="small">{tr("Configuration example")}</summary><pre className="native-example">{'{\n "files": {\n "command": "npx",\n "args": ["-y", "@modelcontextprotocol/server-filesystem", "/your/folder"],\n "disabled": false\n }\n}'}</pre><p className="xs muted">{tr("Saved credentials are masked. Keep the mask to preserve an existing value; type a new value to replace it.")}</p></details></>)}
{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(hints[f.id] ?? f.hint)}</small> : null)}</span><NativeFieldInput field={f} model={model} value={values[f.id]} onChange={v => change(f.id, v)}/></label>))}</section> : null))}{tr(switchRows.length ? <section className="native-settings-group"><h3 className="h4">{tr("Speed")}</h3>{tr(switchRows.map(sw => <label className="native-setting-row" key={sw.id}><span><strong>{tr(sw.label)}</strong><small>{tr(sw.hint)}</small></span><Select ariaLabel={tr(sw.label)} value={switchIsOn(sw, values.env) ? 'on' : 'off'} options={sw.default === 'on' ? [{ value: 'on', label: 'On (recommended)' }, { value: 'off', label: 'Off' }] : [{ value: 'off', label: 'Off (recommended)' }, { value: 'on', label: 'On' }]} onChange={v => changeSwitch(sw, v === 'on')}/></label>))}</section> : null)}<section className="native-settings-group"><h3 className="h4">{tr("Sampling")}</h3>{tr([['temperature', 'Temperature'], ['top_p', 'Top-p'], ['top_k', 'Top-k'], ['min_p', 'Min-p'], ['repetition_penalty', 'Repetition penalty'], ['presence_penalty', 'Presence penalty'], ['frequency_penalty', 'Frequency penalty'], ['penalty_last_n', 'Penalty window'], ['seed', 'Seed']].map(([id, label]) => <label className="native-setting-row" key={id}><span><strong>{tr(label)}</strong></span><DecimalInput ariaLabel={tr("Native {0}", [label])} value={values.sampling?.[id] ?? null} onChange={n => { const sampling = { ...values.sampling }; if (n === null) delete sampling[id]; else sampling[id] = n; change('sampling', sampling); }}/></label>))}</section></> : tab === 'json' ? <><p className="small muted">{tr("Engine arguments and STRATA environment flags are editable here. Program paths and network access remain managed by the host.")}</p><textarea className="st-input native-json" aria-label={tr("Native configuration JSON")} spellCheck={false} value={text} onChange={e => { setText(e.target.value); setNotice(''); }}/></> : <><p className="small muted">{tr("Add local programs or HTTP MCP servers. Connected tools can read or change the resources you give them access to. Enable tools in chat when you want the model to use them.")}</p><NativeMcpEditor text={mcp} onChange={value => { setMcp(value); setNotice(''); }}/><details><summary className="small">{tr("Configuration example")}</summary><pre className="native-example">{'{\n "files": {\n "command": "npx",\n "args": ["-y", "@modelcontextprotocol/server-filesystem", "/your/folder"],\n "disabled": false\n }\n}'}</pre><p className="xs muted">{tr("Saved credentials are masked. Keep the mask to preserve an existing value; type a new value to replace it.")}</p></details></>)}
<div className="native-settings-actions"><Btn disabled={pending || !original || !dirty} variant="primary" onClick={() => void save()}>{tr(pending ? 'Saving…' : 'Save settings')}</Btn><Btn disabled={pending} onClick={() => { const saved = JSON.parse(original || '{"values":{},"mcp":{}}'); setValues(saved.values); setText(JSON.stringify(saved.values, null, 2)); setMcp(JSON.stringify(saved.mcp, null, 2)); setError(''); }}>{tr("Restore saved")}</Btn></div>{tr(notice ? <p role="status" className="small">{tr(notice)}</p> : null)}{tr(error ? <ErrorNotice message={error} onRetry={() => setRevision(value => value + 1)}/> : null)}
</details>;
}
+1 -1
View File
@@ -21,7 +21,7 @@ function FreeMeter({ label, total, free }: { label: string; total: number; free:
* "This computer": how much graphics memory and RAM is free right now. While a model is loaded that is all the card shows: the hardware line, the model's
* name, "Loaded", what it takes and the SSD line are gone (owner, 11-01: the page already says what is loaded); only while the loaded model really reads
* from the SSD, how fast it reads right now. Before anything is loaded the card says what the chosen version would take. That a version does not fit in
* memory is said once, under the version picker ("SSD streaming · will run slower"), not again here. (The n-gram table, which always sits on the SSD, is not counted.)
* memory is said once, under the version picker ("may run slower"), not again here. (The n-gram table, which always sits on the SSD, is not counted.)
*/
export function PcFitCard({ gpuName, ramGb, vramGb, fit, inUse = null, diskGb = null, live = null, loaded = [], selectedLoaded = false }: { gpuName?: string | null; ramGb: number | null; vramGb: number; fit: Fit | null; inUse?: { ramGb: number; vramGb: number } | null; diskGb?: number | null; live?: PcLive | null; loaded?: PcLoaded[]; selectedLoaded?: boolean }) {
const showFit = !!fit?.known && !selectedLoaded;
+1 -1
View File
@@ -88,7 +88,7 @@ export function SettingsUpdates() {
</>;
}
/** "Strata 0.2.11 · Engine 0.1.57": the app's own version first, the engine's beside it (About). */
/** "Strata 0.2.13 · Engine 0.1.59": 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);
+15 -3
View File
@@ -1,7 +1,19 @@
import { useMemo } from 'react';
import { codeLanguage, highlightedLines } from '../lib/syntax';
import { useEffect, useMemo, useState } from 'react';
import { codeLanguage } from '../lib/code-language';
import type { CodeToken } from '../lib/syntax';
/** The Prism grammars are a separate file (src/lib/syntax.ts), fetched after the first screen is up (App.tsx asks for it a few seconds in) or by the first code block, whichever comes first. Until it is here a block shows its text uncoloured. */
type Highlighter = typeof import('../lib/syntax');
let loaded: Highlighter | null = null;
let loading: Promise<Highlighter | null> | null = null;
export function loadHighlighter(): Promise<Highlighter | null> {
loading ??= import('../lib/syntax').then(module => (loaded = module), () => { loading = null; return null; });
return loading;
}
const plainLines = (text: string): CodeToken[][] => text.split('\n').map(part => (part ? [{ text: part, classes: [] }] : []));
export function SyntaxCode({text,language='',filename,lineNumbers=false,className=''}:{text:string;language?:string;filename?:string;lineNumbers?:boolean;className?:string}){
const resolved=filename?codeLanguage(filename,true):codeLanguage(language);
const lines=useMemo(()=>highlightedLines(text,resolved),[text,resolved]);
const [highlighter,setHighlighter]=useState<Highlighter|null>(loaded);
useEffect(()=>{if(highlighter)return;let live=true;void loadHighlighter().then(module=>{if(live&&module)setHighlighter(module);});return()=>{live=false;};},[highlighter]);
const lines=useMemo(()=>highlighter?highlighter.highlightedLines(text,resolved):plainLines(text),[highlighter,text,resolved]);
return <pre className={`syntax-code ${lineNumbers?'code-preview':''} ${className}`} data-language={resolved}><code>{lines.map((line,i)=><span className="syntax-line" key={i}>{lineNumbers?<span className="syntax-gutter" aria-hidden="true">{i+1}</span>:null}<span className="syntax-text">{line.map((part,j)=>part.classes.length?<span className={part.classes.map(c=>`syntax-${c}`).join(' ')} key={j}>{part.text}</span>:part.text)}{!lineNumbers&&i<lines.length-1?'\n':''}</span></span>)}</code></pre>;
}
+8 -9
View File
@@ -26,9 +26,9 @@ const whole = (n: number) => `${Math.round(n)} GB`;
/**
* Where the versions of the chosen base model are listed, each exactly once. At the top: the versions we recommend (the pick for this PC first; no "Recommended" tag since 11-02, the order says it) and the versions already downloaded (tagged "Downloaded"). Below, folded under "More
* versions": all the others. A row is the name, small tags, a word only where one is needed ("SSD streaming · will run slower" for a version that does
* not fully fit in memory, "Doesn't fit" where it cannot run at all; a version that fits says nothing: see `versionFit`) and the size. The maker, the
* memory it needs and the SSD figure are behind "Show details" (open from the start in Expert mode). The other base models are not in this list: they
* versions": all the others. A row is the name, small tags, a word only where one is needed ("may run slower" for a version that does
* not fit in memory; a version that fits says nothing: see `versionFit`) and the size. The maker and the
* memory it needs are behind "Show details" (open from the start in Expert mode). The other base models are not in this list: they
* appear when picked at the top of the page.
*/
export function VersionPicker({ versions, selectedId, hardware, onSelect, onOwnGguf, downloaded, showRecommended = true, moreLabel, pick, cache, details: detailsOpen = false }: {
@@ -67,21 +67,20 @@ export function VersionPicker({ versions, selectedId, hardware, onSelect, onOwnG
const foldedOpen = moreOpen || !top.length || more.some(v => v.id === selectedId);
const need = (v: CatalogVersion, technical: boolean) => {
const fit = fitOn(v, hardware, true, cache);
if (technical) return v.multi_file && v.disk_gb ? tr("{0} RAM + VRAM, plus about {1} GB of disk", [gb(fit.memoryGb), Math.round(v.disk_gb)]) : tr("{0} RAM + VRAM", [gb(fit.memoryGb)]);
return v.multi_file && v.disk_gb ? tr("Needs {0} of memory and about {1} GB of disk", [whole(fit.memoryGb), Math.round(v.disk_gb)]) : tr("Needs {0} of memory", [whole(fit.memoryGb)]);
const size = typeof v.fit_gb === 'number' && Number.isFinite(v.fit_gb) ? v.fit_gb : fitOn(v, hardware, true, cache).memoryGb; // the files without the lookup table: the figure the label uses
if (technical) return v.multi_file && v.disk_gb ? tr("{0} RAM + VRAM, plus about {1} GB of disk", [gb(size), Math.round(v.disk_gb)]) : tr("{0} RAM + VRAM", [gb(size)]);
return v.multi_file && v.disk_gb ? tr("Needs {0} of memory and about {1} GB of disk", [whole(size), Math.round(v.disk_gb)]) : tr("Needs {0} of memory", [whole(size)]);
};
const row = (v: CatalogVersion) => {
const fit = fitOn(v, hardware, true, cache);
const on = v.id === selectedId, have = !!downloaded?.has(v.id);
const state = versionFit(v, hardware, cache);
const technical = [MAKERS[v.sourceFamily], need(v, true), state === 'ssd' ? tr("Uses SSD {0} GB", [Math.round(fit.spillGb)]) : ''].filter(Boolean).join(' · ');
const technical = [MAKERS[v.sourceFamily], need(v, true)].filter(Boolean).join(' · ');
const note = versionNote(v.model);
return <button key={v.id} role="menuitemradio" aria-checked={on} className={cx('version-row', on && 'is-on')} onClick={() => { onSelect(v.id); setOpen(false); }}>
<span className="version-row__check">{tr(on ? <Icon name="check" size={14}/> : null)}</span>
<span className="version-row__body">
<span className="version-row__title"><strong>{versionName(v)}</strong>{tr(have ? <span className="version-row__tags"><span className="version-tag version-tag--have">{tr("Downloaded")}</span></span> : null)}</span>
{tr(state === 'ssd' || state === 'no' ? <span className="version-row__fit" data-fit={state}>{tr(state === 'ssd' ? 'SSD streaming · will run slower' : "Doesn't fit")}</span> : null)}
{tr(state === 'ssd' ? <span className="version-row__fit" data-fit={state}>{tr('may run slower')}</span> : null)}
{tr(details ? <span className="version-row__tech">{technical}{tr(note ? <><br/>{note}</> : null)}</span> : null)}
</span>
<span className="version-row__size">{have ? gb(v.download_gb) : tr("{0} download", [gb(v.download_gb)])}</span>
+17
View File
@@ -0,0 +1,17 @@
/* All the interface strings of one language (de): the base file and the 13 add-on files, merged in the order the app has always used (a later file wins).
Loaded the first time the language is used (see loadCatalog in i18n.ts); English stays in the main file because the English text is the key. */
import base from '../locales/de.json';
import tune from '../locales/tune-de.json';
import tools from '../locales/tools-de.json';
import fix from '../locales/fix-de.json';
import gh from '../locales/gh-de.json';
import ui from '../locales/ui-de.json';
import engine from '../locales/engine-de.json';
import pick from '../locales/pick-de.json';
import df2 from '../locales/df2-de.json';
import upd from '../locales/upd-de.json';
import r3 from '../locales/r3-de.json';
import pic from '../locales/pic-de.json';
import gui from '../locales/gui-de.json';
import sav from '../locales/sav-de.json';
export default {...base,...tune,...tools,...fix,...gh,...ui,...engine,...pick,...df2,...upd,...r3,...pic,...gui,...sav} as Record<string,string>;
+17
View File
@@ -0,0 +1,17 @@
/* All the interface strings of one language (es): the base file and the 13 add-on files, merged in the order the app has always used (a later file wins).
Loaded the first time the language is used (see loadCatalog in i18n.ts); English stays in the main file because the English text is the key. */
import base from '../locales/es.json';
import tune from '../locales/tune-es.json';
import tools from '../locales/tools-es.json';
import fix from '../locales/fix-es.json';
import gh from '../locales/gh-es.json';
import ui from '../locales/ui-es.json';
import engine from '../locales/engine-es.json';
import pick from '../locales/pick-es.json';
import df2 from '../locales/df2-es.json';
import upd from '../locales/upd-es.json';
import r3 from '../locales/r3-es.json';
import pic from '../locales/pic-es.json';
import gui from '../locales/gui-es.json';
import sav from '../locales/sav-es.json';
export default {...base,...tune,...tools,...fix,...gh,...ui,...engine,...pick,...df2,...upd,...r3,...pic,...gui,...sav} as Record<string,string>;
+17
View File
@@ -0,0 +1,17 @@
/* All the interface strings of one language (fr): the base file and the 13 add-on files, merged in the order the app has always used (a later file wins).
Loaded the first time the language is used (see loadCatalog in i18n.ts); English stays in the main file because the English text is the key. */
import base from '../locales/fr.json';
import tune from '../locales/tune-fr.json';
import tools from '../locales/tools-fr.json';
import fix from '../locales/fix-fr.json';
import gh from '../locales/gh-fr.json';
import ui from '../locales/ui-fr.json';
import engine from '../locales/engine-fr.json';
import pick from '../locales/pick-fr.json';
import df2 from '../locales/df2-fr.json';
import upd from '../locales/upd-fr.json';
import r3 from '../locales/r3-fr.json';
import pic from '../locales/pic-fr.json';
import gui from '../locales/gui-fr.json';
import sav from '../locales/sav-fr.json';
export default {...base,...tune,...tools,...fix,...gh,...ui,...engine,...pick,...df2,...upd,...r3,...pic,...gui,...sav} as Record<string,string>;
+17
View File
@@ -0,0 +1,17 @@
/* All the interface strings of one language (ja): the base file and the 13 add-on files, merged in the order the app has always used (a later file wins).
Loaded the first time the language is used (see loadCatalog in i18n.ts); English stays in the main file because the English text is the key. */
import base from '../locales/ja.json';
import tune from '../locales/tune-ja.json';
import tools from '../locales/tools-ja.json';
import fix from '../locales/fix-ja.json';
import gh from '../locales/gh-ja.json';
import ui from '../locales/ui-ja.json';
import engine from '../locales/engine-ja.json';
import pick from '../locales/pick-ja.json';
import df2 from '../locales/df2-ja.json';
import upd from '../locales/upd-ja.json';
import r3 from '../locales/r3-ja.json';
import pic from '../locales/pic-ja.json';
import gui from '../locales/gui-ja.json';
import sav from '../locales/sav-ja.json';
export default {...base,...tune,...tools,...fix,...gh,...ui,...engine,...pick,...df2,...upd,...r3,...pic,...gui,...sav} as Record<string,string>;
+17
View File
@@ -0,0 +1,17 @@
/* All the interface strings of one language (ko): the base file and the 13 add-on files, merged in the order the app has always used (a later file wins).
Loaded the first time the language is used (see loadCatalog in i18n.ts); English stays in the main file because the English text is the key. */
import base from '../locales/ko.json';
import tune from '../locales/tune-ko.json';
import tools from '../locales/tools-ko.json';
import fix from '../locales/fix-ko.json';
import gh from '../locales/gh-ko.json';
import ui from '../locales/ui-ko.json';
import engine from '../locales/engine-ko.json';
import pick from '../locales/pick-ko.json';
import df2 from '../locales/df2-ko.json';
import upd from '../locales/upd-ko.json';
import r3 from '../locales/r3-ko.json';
import pic from '../locales/pic-ko.json';
import gui from '../locales/gui-ko.json';
import sav from '../locales/sav-ko.json';
export default {...base,...tune,...tools,...fix,...gh,...ui,...engine,...pick,...df2,...upd,...r3,...pic,...gui,...sav} as Record<string,string>;
+17
View File
@@ -0,0 +1,17 @@
/* All the interface strings of one language (pt): the base file and the 13 add-on files, merged in the order the app has always used (a later file wins).
Loaded the first time the language is used (see loadCatalog in i18n.ts); English stays in the main file because the English text is the key. */
import base from '../locales/pt.json';
import tune from '../locales/tune-pt.json';
import tools from '../locales/tools-pt.json';
import fix from '../locales/fix-pt.json';
import gh from '../locales/gh-pt.json';
import ui from '../locales/ui-pt.json';
import engine from '../locales/engine-pt.json';
import pick from '../locales/pick-pt.json';
import df2 from '../locales/df2-pt.json';
import upd from '../locales/upd-pt.json';
import r3 from '../locales/r3-pt.json';
import pic from '../locales/pic-pt.json';
import gui from '../locales/gui-pt.json';
import sav from '../locales/sav-pt.json';
export default {...base,...tune,...tools,...fix,...gh,...ui,...engine,...pick,...df2,...upd,...r3,...pic,...gui,...sav} as Record<string,string>;
+17
View File
@@ -0,0 +1,17 @@
/* All the interface strings of one language (ru): the base file and the 13 add-on files, merged in the order the app has always used (a later file wins).
Loaded the first time the language is used (see loadCatalog in i18n.ts); English stays in the main file because the English text is the key. */
import base from '../locales/ru.json';
import tune from '../locales/tune-ru.json';
import tools from '../locales/tools-ru.json';
import fix from '../locales/fix-ru.json';
import gh from '../locales/gh-ru.json';
import ui from '../locales/ui-ru.json';
import engine from '../locales/engine-ru.json';
import pick from '../locales/pick-ru.json';
import df2 from '../locales/df2-ru.json';
import upd from '../locales/upd-ru.json';
import r3 from '../locales/r3-ru.json';
import pic from '../locales/pic-ru.json';
import gui from '../locales/gui-ru.json';
import sav from '../locales/sav-ru.json';
export default {...base,...tune,...tools,...fix,...gh,...ui,...engine,...pick,...df2,...upd,...r3,...pic,...gui,...sav} as Record<string,string>;
+17
View File
@@ -0,0 +1,17 @@
/* All the interface strings of one language (zh): the base file and the 13 add-on files, merged in the order the app has always used (a later file wins).
Loaded the first time the language is used (see loadCatalog in i18n.ts); English stays in the main file because the English text is the key. */
import base from '../locales/zh.json';
import tune from '../locales/tune-zh.json';
import tools from '../locales/tools-zh.json';
import fix from '../locales/fix-zh.json';
import gh from '../locales/gh-zh.json';
import ui from '../locales/ui-zh.json';
import engine from '../locales/engine-zh.json';
import pick from '../locales/pick-zh.json';
import df2 from '../locales/df2-zh.json';
import upd from '../locales/upd-zh.json';
import r3 from '../locales/r3-zh.json';
import pic from '../locales/pic-zh.json';
import gui from '../locales/gui-zh.json';
import sav from '../locales/sav-zh.json';
export default {...base,...tune,...tools,...fix,...gh,...ui,...engine,...pick,...df2,...upd,...r3,...pic,...gui,...sav} as Record<string,string>;
+6
View File
@@ -0,0 +1,6 @@
/** The language name of a code fence or a file name. Kept apart from the highlighter (syntax.ts, which carries the Prism grammars) so a code block can name its language before the grammars have loaded. */
const aliases:Record<string,string>={ts:'typescript',tsx:'tsx',mts:'typescript',cts:'typescript',js:'javascript',jsx:'jsx',mjs:'javascript',cjs:'javascript',py:'python',pyw:'python',md:'markdown',mdx:'markdown',markdown:'markdown',html:'markup',htm:'markup',xml:'markup',svg:'markup',json:'json',css:'css',sh:'bash',shell:'bash',bash:'bash',zsh:'bash',yml:'yaml',yaml:'yaml',rs:'rust',go:'go',java:'java',c:'c',h:'c',cc:'cpp',cpp:'cpp',cxx:'cpp',hpp:'cpp',cs:'csharp',csharp:'csharp',sql:'sql',typescript:'typescript',javascript:'javascript',python:'python',rust:'rust'};
export function codeLanguage(value:string,filename=false){
const input=filename?(value.split(/[?#]/)[0].split('.').at(-1)??''):value.trim().split(/\s/)[0].toLowerCase();
return aliases[input.toLowerCase()]??'plain';
}
+18 -115
View File
@@ -1,138 +1,26 @@
import baseEnglish from '../locales/en.json';
import tune_en from '../locales/tune-en.json';
import tune_zh from '../locales/tune-zh.json';
import tune_ja from '../locales/tune-ja.json';
import tune_de from '../locales/tune-de.json';
import tune_es from '../locales/tune-es.json';
import tune_ru from '../locales/tune-ru.json';
import tune_pt from '../locales/tune-pt.json';
import tune_ko from '../locales/tune-ko.json';
import tune_fr from '../locales/tune-fr.json';
import tools_en from '../locales/tools-en.json';
import tools_zh from '../locales/tools-zh.json';
import tools_ja from '../locales/tools-ja.json';
import tools_de from '../locales/tools-de.json';
import tools_es from '../locales/tools-es.json';
import tools_ru from '../locales/tools-ru.json';
import tools_pt from '../locales/tools-pt.json';
import tools_ko from '../locales/tools-ko.json';
import tools_fr from '../locales/tools-fr.json';
import fix_en from '../locales/fix-en.json';
import fix_zh from '../locales/fix-zh.json';
import fix_ja from '../locales/fix-ja.json';
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 fix_fr from '../locales/fix-fr.json';
/* The strings of the 0.2.7 GitHub items (gh-<lang>.json, scripts/make-gh-locales.py). */
import gh_en from '../locales/gh-en.json';
import gh_zh from '../locales/gh-zh.json';
import gh_ja from '../locales/gh-ja.json';
import gh_de from '../locales/gh-de.json';
import gh_es from '../locales/gh-es.json';
import gh_ru from '../locales/gh-ru.json';
import gh_pt from '../locales/gh-pt.json';
import gh_ko from '../locales/gh-ko.json';
import gh_fr from '../locales/gh-fr.json';
/* The strings of the 0.2.10 interface changes (ui-<lang>.json, scripts/make-ui-locales.py). */
import ui_en from '../locales/ui-en.json';
import ui_zh from '../locales/ui-zh.json';
import ui_ja from '../locales/ui-ja.json';
import ui_de from '../locales/ui-de.json';
import ui_es from '../locales/ui-es.json';
import ui_ru from '../locales/ui-ru.json';
import ui_pt from '../locales/ui-pt.json';
import ui_ko from '../locales/ui-ko.json';
import ui_fr from '../locales/ui-fr.json';
/* The strings of Settings > Engine (engine-<lang>.json, scripts/make-engine-locales.py). */
import engine_en from '../locales/engine-en.json';
import engine_zh from '../locales/engine-zh.json';
import engine_ja from '../locales/engine-ja.json';
import engine_de from '../locales/engine-de.json';
import engine_es from '../locales/engine-es.json';
import engine_ru from '../locales/engine-ru.json';
import engine_pt from '../locales/engine-pt.json';
import engine_ko from '../locales/engine-ko.json';
import engine_fr from '../locales/engine-fr.json';
/* The strings of the model version list (pick-<lang>.json, scripts/make-pick-locales.py). */
import pick_en from '../locales/pick-en.json';
import pick_zh from '../locales/pick-zh.json';
import pick_ja from '../locales/pick-ja.json';
import pick_de from '../locales/pick-de.json';
import pick_es from '../locales/pick-es.json';
import pick_ru from '../locales/pick-ru.json';
import pick_pt from '../locales/pick-pt.json';
import pick_ko from '../locales/pick-ko.json';
import pick_fr from '../locales/pick-fr.json';
/* The strings of the Drafting choice and the SSD line (df2-<lang>.json, scripts/make-df2-locales.py). */
import df2_en from '../locales/df2-en.json';
import df2_zh from '../locales/df2-zh.json';
import df2_ja from '../locales/df2-ja.json';
import df2_de from '../locales/df2-de.json';
import df2_es from '../locales/df2-es.json';
import df2_ru from '../locales/df2-ru.json';
import df2_pt from '../locales/df2-pt.json';
import df2_ko from '../locales/df2-ko.json';
import df2_fr from '../locales/df2-fr.json';
/* The strings of the engine update beside a loaded model (upd-<lang>.json, scripts/make-update-locales.py). */
import upd_en from '../locales/upd-en.json';
import upd_zh from '../locales/upd-zh.json';
import upd_ja from '../locales/upd-ja.json';
import upd_de from '../locales/upd-de.json';
import upd_es from '../locales/upd-es.json';
import upd_ru from '../locales/upd-ru.json';
import upd_pt from '../locales/upd-pt.json';
import upd_ko from '../locales/upd-ko.json';
import upd_fr from '../locales/upd-fr.json';
import r3_en from '../locales/r3-en.json';
import r3_zh from '../locales/r3-zh.json';
import r3_ja from '../locales/r3-ja.json';
import r3_de from '../locales/r3-de.json';
import r3_es from '../locales/r3-es.json';
import r3_ru from '../locales/r3-ru.json';
import r3_pt from '../locales/r3-pt.json';
import r3_ko from '../locales/r3-ko.json';
import r3_fr from '../locales/r3-fr.json';
/* The picture files Load fetches by itself (pic-<lang>.json; the host words these, strata_control/pictures.py's sentences are the same). */
import pic_en from '../locales/pic-en.json';
import pic_zh from '../locales/pic-zh.json';
import pic_ja from '../locales/pic-ja.json';
import pic_de from '../locales/pic-de.json';
import pic_es from '../locales/pic-es.json';
import pic_ru from '../locales/pic-ru.json';
import pic_pt from '../locales/pic-pt.json';
import pic_ko from '../locales/pic-ko.json';
import pic_fr from '../locales/pic-fr.json';
/* The GUI feedback round 2 strings (gui-<lang>.json, scripts/make-gui-locales.py). */
import gui_en from '../locales/gui-en.json';
import gui_zh from '../locales/gui-zh.json';
import gui_ja from '../locales/gui-ja.json';
import gui_de from '../locales/gui-de.json';
import gui_es from '../locales/gui-es.json';
import gui_ru from '../locales/gui-ru.json';
import gui_pt from '../locales/gui-pt.json';
import gui_ko from '../locales/gui-ko.json';
import gui_fr from '../locales/gui-fr.json';
/* The notice for chats the host could not write (sav-<lang>.json, scripts/make-chatsave-locales.py). */
import sav_en from '../locales/sav-en.json';
import sav_zh from '../locales/sav-zh.json';
import sav_ja from '../locales/sav-ja.json';
import sav_de from '../locales/sav-de.json';
import sav_es from '../locales/sav-es.json';
import sav_ru from '../locales/sav-ru.json';
import sav_pt from '../locales/sav-pt.json';
import sav_ko from '../locales/sav-ko.json';
import sav_fr from '../locales/sav-fr.json';
import base_zh from '../locales/zh.json';
import base_ja from '../locales/ja.json';
import base_de from '../locales/de.json';
import base_es from '../locales/es.json';
import base_ru from '../locales/ru.json';
import base_pt from '../locales/pt.json';
import base_ko from '../locales/ko.json';
import base_fr from '../locales/fr.json';
export const LANGUAGES=[{value:'en',label:'English'},{value:'zh',label:'中文(简体)'},{value:'ja',label:'日本語'},{value:'de',label:'Deutsch'},{value:'fr',label:'Français'},{value:'es',label:'Español'},{value:'ru',label:'Русский'},{value:'pt',label:'Português'},{value:'ko',label:'한국어'}] as const;
export type Language=typeof LANGUAGES[number]['value'];
@@ -140,8 +28,20 @@ export type Language=typeof LANGUAGES[number]['value'];
/* The tool-server, picture and agent-result strings of the tools and images fixes live in tools-<lang>.json for the same reason. */
/* The strings that had no key in the files above (#3479, #3825) and the 0.2.4.3 fixes live in fix-<lang>.json (scripts/make-fix-locales.py). */
const english:Record<string,string>={...baseEnglish,...tune_en,...tools_en,...fix_en,...gh_en,...ui_en,...engine_en,...pick_en,...df2_en,...upd_en,...r3_en,...pic_en,...gui_en,...sav_en};
const [zh,ja,de,es,ru,pt,ko,fr]=[{...base_zh,...tune_zh,...tools_zh,...fix_zh,...gh_zh,...ui_zh,...engine_zh,...pick_zh,...df2_zh,...upd_zh,...r3_zh,...pic_zh,...gui_zh,...sav_zh},{...base_ja,...tune_ja,...tools_ja,...fix_ja,...gh_ja,...ui_ja,...engine_ja,...pick_ja,...df2_ja,...upd_ja,...r3_ja,...pic_ja,...gui_ja,...sav_ja},{...base_de,...tune_de,...tools_de,...fix_de,...gh_de,...ui_de,...engine_de,...pick_de,...df2_de,...upd_de,...r3_de,...pic_de,...gui_de,...sav_de},{...base_es,...tune_es,...tools_es,...fix_es,...gh_es,...ui_es,...engine_es,...pick_es,...df2_es,...upd_es,...r3_es,...pic_es,...gui_es,...sav_es},{...base_ru,...tune_ru,...tools_ru,...fix_ru,...gh_ru,...ui_ru,...engine_ru,...pick_ru,...df2_ru,...upd_ru,...r3_ru,...pic_ru,...gui_ru,...sav_ru},{...base_pt,...tune_pt,...tools_pt,...fix_pt,...gh_pt,...ui_pt,...engine_pt,...pick_pt,...df2_pt,...upd_pt,...r3_pt,...pic_pt,...gui_pt,...sav_pt},{...base_ko,...tune_ko,...tools_ko,...fix_ko,...gh_ko,...ui_ko,...engine_ko,...pick_ko,...df2_ko,...upd_ko,...r3_ko,...pic_ko,...gui_ko,...sav_ko},{...base_fr,...tune_fr,...tools_fr,...fix_fr,...gh_fr,...ui_fr,...engine_fr,...pick_fr,...df2_fr,...upd_fr,...r3_fr,...pic_fr,...gui_fr,...sav_fr}] as Record<string,string>[];
const catalogs:Record<Language,Record<string,string>>={en:english,zh,ja,de,fr,es,ru,pt,ko};
/* English is the key of every string, so it stays in the main file. The other eight languages are one file each, fetched the first time a language is used
(the saved language before the first screen is drawn, a language picked later when it is picked): most people read one of the nine. */
type Catalog=Record<string,string>;
const loaders:Record<Exclude<Language,'en'>,()=>Promise<{default:Catalog}>>={zh:()=>import('./catalog-zh'),ja:()=>import('./catalog-ja'),de:()=>import('./catalog-de'),fr:()=>import('./catalog-fr'),es:()=>import('./catalog-es'),ru:()=>import('./catalog-ru'),pt:()=>import('./catalog-pt'),ko:()=>import('./catalog-ko')};
const catalogs:Partial<Record<Language,Catalog>>={en:english};
const loading=new Map<Language,Promise<void>>();
export const catalogLoaded=(value:Language)=>!!catalogs[value];
/** Fetch the strings of a language (once). Resolves when they are in; a failed fetch also resolves, the text then stays English and the next call tries again. */
export function loadCatalog(value:Language):Promise<void>{
if(catalogs[value])return Promise.resolve();
let request=loading.get(value);
if(!request){request=loaders[value as Exclude<Language,'en'>]().then(module=>{catalogs[value]=module.default;},()=>undefined).finally(()=>{loading.delete(value);});loading.set(value,request);}
return request;
}
const entities:Record<string,string>={amp:'&',lt:'<',gt:'>',quot:'"',apos:"'",nbsp:'\u00a0'};
const decode=(text:string)=>text.replace(/&(#x[\da-f]+|#\d+|amp|lt|gt|quot|apos|nbsp);/gi,(whole,key:string)=>key[0]==='#'?String.fromCodePoint(key[1].toLowerCase()==='x'?parseInt(key.slice(2),16):parseInt(key.slice(1),10)):entities[key]??whole);
const ids=new Map(Object.entries(english).map(([id,value])=>[decode(value),id]));
@@ -149,7 +49,10 @@ let language:Language='en';
export const validLanguage=(value:unknown):value is Language=>LANGUAGES.some(item=>item.value===value);
export const currentLanguage=()=>language;
export const localeTag=()=>language==='zh'?'zh-CN':language;
export function setTranslationLanguage(value:Language){language=value;}
/** Switch the language `tr` answers in. The strings of a language that is not loaded yet come in a moment later (English until then): wait on the result before drawing, or use `loadCatalog` first. */
export function setTranslationLanguage(value:Language):Promise<void>{language=value;return loadCatalog(value);}
/** Resolves when the current language's strings are in. */
export const translationsReady=()=>loadCatalog(language);
const escape=(value:string)=>value.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
const patterns=Object.entries(english).filter(([,value])=>/\{\d+\}/.test(value)).map(([id,value])=>{
const slots:number[]=[];let expression='',at=0;
+30
View File
@@ -0,0 +1,30 @@
/**
* Screens that are not needed to show the start screen load the first time they are used.
*
* `lazyScreen(load, pick)` is React.lazy over a dynamic import, for the routed screens.
* `useLoadedModule(load, wanted)` is for surfaces that stay mounted and animate open (the Settings and set-up
* dialogs): it returns the module once its code is here (null until then), and the caller opens the surface at
* that point, so it appears exactly as before (same first frame, same focus target) instead of opening empty and
* filling in. If a chunk cannot be fetched the error is thrown into render, and reaches the app's boundary as for
* any other screen that fails to show.
*/
import { lazy, useEffect, useState, type ComponentType } from 'react';
export function lazyScreen<M, P extends object>(load: () => Promise<M>, pick: (module: M) => ComponentType<P>) {
return lazy(() => load().then((module) => ({ default: pick(module) })));
}
const loaded = new WeakMap<object, unknown>();
export function useLoadedModule<M>(load: () => Promise<M>, wanted: boolean): M | null {
const [module, setModule] = useState<M | null>(() => (loaded.get(load) as M | undefined) ?? null);
const [failure, setFailure] = useState<unknown>(null);
useEffect(() => {
if (!wanted || module) return;
let live = true;
load().then((value) => { loaded.set(load, value); if (live) setModule(value); }, (error) => { if (live) setFailure(error ?? new Error('chunk failed')); });
return () => { live = false; };
}, [wanted, module, load]);
if (failure) throw failure;
return module;
}
+1
View File
@@ -19,6 +19,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.feed){groups.push({family:family.family,title:family.title,about:family.about??'',experimental:false,images:family.images,available:family.available,unavailable_reason:family.unavailable_reason,feed:true,sizes:family.sizes.map(size=>({...size,sourceFamily:family.family,edition:'',images:family.images}))});continue;}
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:sizes.some(size=>size.images!==false),sizes:sizes.map(size=>({...size,sourceFamily:family.family,edition:'',images:size.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;
+10 -17
View File
@@ -6,7 +6,7 @@
* about 10 GB left for the system; whatever is left is read from the SSD. The same rules setup uses to choose its low-RAM mode
* (setup.py: low_ram_gpu_gb, LOW_RAM_HEADROOM_GB), so the page and the engine agree.
*/
export interface FitInput { ram_needed_gb?: number; experts_gb?: number | null; memory_gb?: number; mem_total?: boolean }
export interface FitInput { ram_needed_gb?: number; experts_gb?: number | null; memory_gb?: number; mem_total?: boolean; /** the weight files without the lookup table, when the catalog states them (DeepSeek: `memory_gb` is the CPU's share) */ fit_gb?: number | null }
export interface FitHardware { ram_gb?: number | null; vram_gb?: number | null }
export interface Fit {
/** RAM + VRAM the version needs (the n-gram table not counted). */
@@ -127,26 +127,19 @@ export function versionGroups<V extends { id: string; download_gb: number }>(ver
}
/**
* What a version is on this computer. The list writes a word only for the two cases that need one: "SSD streaming · will run slower" (part of the
* version is read from the SSD) and "Doesn't fit". A version that fits in memory says nothing.
* What a version is on this computer (owner's one rule, 2026-11-08): its files, WITHOUT the n-gram / PLE / Engram lookup table's file (the download size minus
* that file: `fit_gb`, else `memory_gb`), against the graphics memory plus the RAM, nothing taken off either side. They fit: 'fits', and the list says nothing.
* They do not: 'ssd', and the list says "may run slower" (part is read from the SSD while it answers). Nothing here refuses anything: no card, or a Mac with
* little memory, gets the same two words. (Not enough free disk is a different question; the download says it.)
*
* The figures are the app's own (`fitOn`: graphics memory, then RAM, then the SSD), the same ones the This computer card uses. The catalog's
* `on_this_pc` is only a RAM comparison for the plain families (it ignores the graphics card and the SSD): it said "does not fit" for NVFP4 on a
* 12 GB card with 63 GB of RAM, which runs with the rest read from the SSD. So the engine's "does not fit" is believed in exactly two cases:
* - the version needs a graphics card and this computer has none that counts (DeepSeek-V4.1-Flash: "does not fit: it needs one or more graphics cards");
* - this computer has less memory than anything has ever been run with (MIN_RUN_GB) and the engine says it does not fit.
* Everything else that does not fit in memory is "ssd": it runs, slower. (Not enough free disk is a different question; the download says it.)
* `fitOn` (above) still places the experts for the This computer card; `on_this_pc` no longer decides anything here.
*/
export type VersionFit = 'fits' | 'ssd' | 'no' | 'unknown';
export const MIN_RUN_GB = 16;
export type VersionFit = 'fits' | 'ssd' | 'unknown';
export function versionFit(v: FitInput & { on_this_pc?: string | null }, hw: FitHardware | null | undefined, cache?: CacheChoice | null): VersionFit {
const words = String(v.on_this_pc ?? '').trim().toLowerCase();
const engineNo = words.startsWith('does not fit');
if (/needs? (one or more|a) graphics cards?/.test(words)) return 'no';
const fit = fitOn(v, hw, true, cache);
if (!fit.known) return engineNo ? 'no' : 'unknown';
if (engineNo && Number(hw?.ram_gb) < MIN_RUN_GB) return 'no';
return fit.spillGb >= 1 ? 'ssd' : 'fits';
if (!fit.known) return 'unknown';
const size = typeof v.fit_gb === 'number' && Number.isFinite(v.fit_gb) ? v.fit_gb : fit.memoryGb;
return size <= Number(hw?.ram_gb) + (Number(hw?.vram_gb) || 0) ? 'fits' : 'ssd';
}
/** A card as the installer's hardware report lists it. */
+2 -2
View File
@@ -3,8 +3,8 @@ 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;/** DeepSeek: the engine gives 5 experts here on RDNA4 cards (one card, or several holding half of the experts). */rdna4_faster?:boolean|null;gpu_options?:GpuChoice|null;images?:boolean;imagesCapable?:boolean;/** DeepSeek-V4.1-Flash and the like: several big files and an engine of their own. */multi_file?:boolean;/** DeepSeek: the free disk the set needs (its download plus the pack and room to work), from the set itself. */disk_gb?:number}
export interface NativeFamily {family:string;title:string;about:string;experimental:boolean;images:boolean;sizes:NativeSize[]}
export interface 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;/** The files without the lookup table (the download minus the n-gram / PLE / Engram file): what the fit label and the "Needs" figure use. */fit_gb?:number|null;installed:boolean;/** DeepSeek: the engine gives 5 experts here on RDNA4 cards (one card, or several holding half of the experts). */rdna4_faster?:boolean|null;gpu_options?:GpuChoice|null;images?:boolean;imagesCapable?:boolean;/** DeepSeek-V4.1-Flash and the like: several big files and an engine of their own. */multi_file?:boolean;/** DeepSeek: the free disk the set needs (its download plus the pack and room to work), from the set itself. */disk_gb?:number}
export interface NativeFamily {family:string;title:string;about:string;experimental:boolean;images:boolean;sizes:NativeSize[];/** A model of the signed update feed (shown after the built-in ones); `available:false` = this engine does not run it, `unavailable_reason` says so in plain words. */feed?:boolean;available?:boolean;unavailable_reason?:string}
export interface NativeField {id:string;label:string;group:string;/** Not shown for a model whose id contains this (DeepSeek has no MTP). */notForModel?: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;/** An engine update beside a loaded model: which step it is on and the models it concerns (the app words these itself). */step?:'getting-ready'|'stopping'|'stopped';models?:string[]}
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;/** One short line when the model was loaded without picture reading although it was on (no internet for the picture files, no room on the disk). */picturesNote?:string;metrics:Record<string,any>|null}
+12 -5
View File
@@ -30,7 +30,7 @@ import { postWithFolderFallback } from './folder-pick';
import { createSessionWriter, exportSessions, readBrowserSessions, HOST_FLAG_KEY, PENDING_KEY, type SaveResult, type StorageLike } from './session-persist';
import { createHttpTransport, hydrateChats, keepLastMomentCopy, WINDOW_ID } from './chat-sync';
import { copyText } from './clipboard';
import { setTranslationLanguage, tr, validLanguage, type Language } from './i18n';
import { catalogLoaded, loadCatalog, setTranslationLanguage, tr, validLanguage, type Language } from './i18n';
/**
* Strata app store.
*
@@ -280,8 +280,8 @@ function loadPrefs(): Prefs {
}
const prefs = loadPrefs();
// Use the saved language for the very first render, before host bootstrap.
setTranslationLanguage(prefs.language);
// Use the saved language for the very first render, before host bootstrap (main.tsx waits for its strings before it draws).
void setTranslationLanguage(prefs.language);
/** The last approval question a notification was shown for, so one question is announced once. */
let lastApprovalNotice = '';
@@ -494,7 +494,7 @@ export function dismissToast(id: string) {
*/
function applyPrefs() {
const root = document.documentElement;
setTranslationLanguage(state.language);
void setTranslationLanguage(state.language);
root.lang = state.language === 'zh' ? 'zh-CN' : state.language;
if(state.fontFamily)root.style.setProperty('--st-font',`${JSON.stringify(state.fontFamily)},system-ui,-apple-system,"Segoe UI",sans-serif`);
else root.style.removeProperty('--st-font');
@@ -560,7 +560,14 @@ export function toggleRail() {
set({ rail: !state.rail });
savePrefs();
}
export function setLanguage(language:Language){if(!validLanguage(language))return;setTranslationLanguage(language);set({language});savePrefs();rememberShellLanguage(language);}
/** A language whose strings are already here switches at once; the first use of another one waits for its file (a moment), then switches everything together. The last pick wins. */
let languageRequest=0;
export function setLanguage(language:Language){
if(!validLanguage(language))return;
const request=++languageRequest;
const apply=()=>{if(request!==languageRequest)return;setTranslationLanguage(language);set({language});savePrefs();rememberShellLanguage(language);};
if(catalogLoaded(language))apply();else void loadCatalog(language).then(apply);
}
let shellAnnounced = false;
/** The desktop shell's tray menu follows the app's language; it reads it from a small file the host keeps (no effect in a browser). */
function rememberShellLanguage(language:Language){api.post('/api/shell/prefs',{language}).catch(()=>undefined);}
+2 -5
View File
@@ -16,11 +16,8 @@ import 'prismjs/components/prism-cpp';
import 'prismjs/components/prism-csharp';
import 'prismjs/components/prism-sql';
Prism.manual = true;
const aliases:Record<string,string>={ts:'typescript',tsx:'tsx',mts:'typescript',cts:'typescript',js:'javascript',jsx:'jsx',mjs:'javascript',cjs:'javascript',py:'python',pyw:'python',md:'markdown',mdx:'markdown',markdown:'markdown',html:'markup',htm:'markup',xml:'markup',svg:'markup',json:'json',css:'css',sh:'bash',shell:'bash',bash:'bash',zsh:'bash',yml:'yaml',yaml:'yaml',rs:'rust',go:'go',java:'java',c:'c',h:'c',cc:'cpp',cpp:'cpp',cxx:'cpp',hpp:'cpp',cs:'csharp',csharp:'csharp',sql:'sql',typescript:'typescript',javascript:'javascript',python:'python',rust:'rust'};
export function codeLanguage(value:string,filename=false){
const input=filename?(value.split(/[?#]/)[0].split('.').at(-1)??''):value.trim().split(/\s/)[0].toLowerCase();
return aliases[input.toLowerCase()]??'plain';
}
import { codeLanguage } from './code-language';
export { codeLanguage };
export interface CodeToken {text:string;classes:string[]}
export function highlightedLines(text:string,language:string):CodeToken[][]{
const lines:CodeToken[][]=[[]];
+3 -1
View File
@@ -18,5 +18,7 @@
"df17": "Vorausraten: MTP (DFlash2 ist nicht gestartet)",
"df18": "Automatisch nutzt die Alle-Karten-Split-Engine mit der MTP-Entwurfsschicht: etwa {0} GB, einmalig herunterzuladen.",
"df19": "Die Alle-Karten-Split-Engine konnte nicht starten, deshalb läuft die normale Schichtaufteilung.",
"df20": "Vorausraten: aus (den Karten fehlte Speicher)"
"df20": "Vorausraten: aus (den Karten fehlte Speicher)",
"df21": "Auf Ihrem Mac war DFlash2 bei diesem Modell in unseren Tests schneller als MTP.",
"df22": "DFlash2 ist auf dieser Grafikkarte vorerst standardmäßig aus; wählen Sie es unter „Vorausraten“ aus, um es zu nutzen."
}
+3 -1
View File
@@ -18,5 +18,7 @@
"df17": "Drafting: MTP (DFlash2 didn't start)",
"df18": "Auto uses the all-cards split engine with the MTP draft layer: about {0} GB to download, once.",
"df19": "The all-cards split engine could not start, so the normal layer split runs.",
"df20": "Drafting: off (the cards were short of memory)"
"df20": "Drafting: off (the cards were short of memory)",
"df21": "On your Mac, DFlash2 was faster than MTP on this model in our tests.",
"df22": "DFlash2 is off by default on this graphics card for now; choose it in Drafting to use it."
}
+3 -1
View File
@@ -18,5 +18,7 @@
"df17": "Predicción: MTP (DFlash2 no arrancó)",
"df18": "Automático usa el motor de reparto entre todas las tarjetas con la capa de borrador MTP: unos {0} GB, una sola vez.",
"df19": "El motor de reparto entre todas las tarjetas no pudo arrancar, así que se usa el reparto normal por capas.",
"df20": "Predicción: desactivada (a las tarjetas les faltaba memoria)"
"df20": "Predicción: desactivada (a las tarjetas les faltaba memoria)",
"df21": "En su Mac, en nuestras pruebas DFlash2 fue más rápido que MTP con este modelo.",
"df22": "DFlash2 está desactivado de forma predeterminada en esta tarjeta gráfica por ahora; elíjalo en Predicción para usarlo."
}
+3 -1
View File
@@ -18,5 +18,7 @@
"df17": "Anticipation : MTP (DFlash2 n’a pas démarré)",
"df18": "Auto utilise le moteur réparti sur toutes les cartes avec la couche de brouillon MTP : environ {0} Go à télécharger, une seule fois.",
"df19": "Le moteur réparti sur toutes les cartes n’a pas pu démarrer, la répartition normale par couches est donc utilisée.",
"df20": "Anticipation : désactivée (les cartes manquaient de mémoire)"
"df20": "Anticipation : désactivée (les cartes manquaient de mémoire)",
"df21": "Sur votre Mac, DFlash2 a été plus rapide que MTP avec ce modèle lors de nos tests.",
"df22": "DFlash2 est désactivé par défaut sur cette carte graphique pour le moment ; choisissez-le dans Anticipation pour l’utiliser."
}
+3 -1
View File
@@ -18,5 +18,7 @@
"df17": "先読み:MTP(DFlash2 は起動しませんでした)",
"df18": "自動では、全カードのスプリットエンジンと MTP ドラフト層を使います。約 {0} GB を一度だけダウンロードします。",
"df19": "全カードのスプリットエンジンを起動できなかったため、通常のレイヤー分割で動作しています。",
"df20": "先読み:オフ(グラフィックスカードのメモリが不足していました)"
"df20": "先読み:オフ(グラフィックスカードのメモリが不足していました)",
"df21": "お使いの Mac では、私たちのテストでこのモデルにおいて DFlash2 は MTP より速くなりました。",
"df22": "現在、このグラフィックスカードでは DFlash2 は既定でオフです。使うには「先読み」で選んでください。"
}
+3 -1
View File
@@ -18,5 +18,7 @@
"df17": "초안 예측: MTP(DFlash2가 시작되지 않았습니다)",
"df18": "자동은 모든 카드를 쓰는 분할 엔진과 MTP 초안 층을 사용합니다(약 {0}GB, 한 번만 내려받음).",
"df19": "모든 카드를 쓰는 분할 엔진을 시작하지 못해 일반 레이어 분할로 실행합니다.",
"df20": "초안 예측: 꺼짐(카드 메모리가 부족했습니다)"
"df20": "초안 예측: 꺼짐(카드 메모리가 부족했습니다)",
"df21": "고객님의 Mac에서 저희 테스트 결과 이 모델에서는 DFlash2가 MTP보다 빨랐습니다.",
"df22": "현재 이 그래픽 카드에서는 DFlash2가 기본적으로 꺼져 있습니다. 사용하려면 ‘초안 예측’에서 선택하세요."
}
+3 -1
View File
@@ -18,5 +18,7 @@
"df17": "Previsão: MTP (o DFlash2 não iniciou)",
"df18": "O automático usa o motor de divisão entre todas as placas com a camada de rascunho MTP: cerca de {0} GB, uma vez.",
"df19": "O motor de divisão entre todas as placas não conseguiu iniciar, então a divisão normal por camadas está em uso.",
"df20": "Previsão: desligada (faltou memória nas placas)"
"df20": "Previsão: desligada (faltou memória nas placas)",
"df21": "No seu Mac, nos nossos testes o DFlash2 foi mais rápido que o MTP com este modelo.",
"df22": "O DFlash2 está desligado por predefinição nesta placa de vídeo por agora; escolha-o em Previsão para o usar."
}
+3 -1
View File
@@ -18,5 +18,7 @@
"df17": "Предсказание: MTP (DFlash2 не запустился)",
"df18": "Авто использует движок с разделением по всем картам и черновой слой MTP: около {0} ГБ, скачивается один раз.",
"df19": "Движок с разделением по всем картам не запустился, поэтому работает обычное послойное разделение.",
"df20": "Предсказание: выключено (на картах не хватило памяти)"
"df20": "Предсказание: выключено (на картах не хватило памяти)",
"df21": "На вашем Mac в наших тестах DFlash2 с этой моделью был быстрее MTP.",
"df22": "На этой видеокарте DFlash2 пока выключен по умолчанию; чтобы использовать его, выберите его в разделе «Предсказание»."
}
+3 -1
View File
@@ -18,5 +18,7 @@
"df17": "预测草稿:MTP(DFlash2 未能启动)",
"df18": "自动模式使用全部显卡的拆分引擎和 MTP 草稿层:约 {0} GB,只下载一次。",
"df19": "全显卡拆分引擎未能启动,因此改用常规的分层拆分。",
"df20": "预测草稿:关闭(显卡内存不足)"
"df20": "预测草稿:关闭(显卡内存不足)",
"df21": "在您的 Mac 上,我们的测试中 DFlash2 在此模型上比 MTP 更快。",
"df22": "目前在这张显卡上 DFlash2 默认关闭;如需使用,请在“预测草稿”中选择它。"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"r3_01": "Braucht ein Engine-Update",
"r3_02": "SSD-Streaming · läuft langsamer",
"r3_02": "kann langsamer laufen",
"r3_03": "So viel Kontext belegt etwa {0} GB Grafikspeicher und lässt auf dieser Karte wenig Platz für das Modell. Strata versucht es; startet es nicht, wähle einen kürzeren Kontext.",
"r3_04": "In Arbeit · {0}",
"r3_05": "{0} Chats in Arbeit",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"r3_01": "Needs an engine update",
"r3_02": "SSD streaming · will run slower",
"r3_02": "may run slower",
"r3_03": "This much context takes about {0} GB of graphics memory and leaves little room for the model on this card. Strata will try it; if it does not start, choose a shorter context.",
"r3_04": "Working · {0}",
"r3_05": "{0} chats working",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"r3_01": "Necesita actualizar el motor",
"r3_02": "Streaming desde el SSD · irá más lento",
"r3_02": "puede ir más lento",
"r3_03": "Este contexto ocupa unos {0} GB de memoria gráfica y deja poco espacio para el modelo en esta tarjeta. Strata lo intentará; si no arranca, elige un contexto más corto.",
"r3_04": "Trabajando · {0}",
"r3_05": "{0} chats trabajando",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"r3_01": "Nécessite une mise à jour du moteur",
"r3_02": "Streaming depuis le SSD · plus lent",
"r3_02": "peut être plus lent",
"r3_03": "Ce contexte occupe environ {0} Go de mémoire graphique et laisse peu de place au modèle sur cette carte. Strata va essayer ; s’il ne démarre pas, choisissez un contexte plus court.",
"r3_04": "En cours de travail · {0}",
"r3_05": "{0} conversations en cours",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"r3_01": "エンジンの更新が必要です",
"r3_02": "SSD ストリーミング · 動作は遅くなります",
"r3_02": "動作が遅くなる可能性があります",
"r3_03": "このコンテキストはグラフィックスメモリーを約 {0} GB 使い、このカードではモデルに残る余裕がわずかです。Strata は試しますが、起動しない場合はより短いコンテキストを選んでください。",
"r3_04": "作業中 · {0}",
"r3_05": "{0} 件のチャットが作業中",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"r3_01": "엔진 업데이트가 필요합니다",
"r3_02": "SSD 스트리밍 · 더 느리게 실행됩니다",
"r3_02": "더 느리게 실행될 수 있습니다",
"r3_03": "이 정도의 컨텍스트는 그래픽 메모리를 약 {0} GB 사용하며 이 카드에서는 모델이 쓸 공간이 거의 남지 않습니다. Strata가 시도하지만 시작되지 않으면 더 짧은 컨텍스트를 선택하세요.",
"r3_04": "작업 중 · {0}",
"r3_05": "대화 {0}개 작업 중",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"r3_01": "Precisa de uma atualização do motor",
"r3_02": "Streaming do SSD · vai rodar mais devagar",
"r3_02": "pode rodar mais devagar",
"r3_03": "Esse contexto ocupa cerca de {0} GB de memória gráfica e deixa pouco espaço para o modelo nesta placa. O Strata vai tentar; se não iniciar, escolha um contexto menor.",
"r3_04": "Trabalhando · {0}",
"r3_05": "{0} chats trabalhando",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"r3_01": "Нужно обновить движок",
"r3_02": "Потоковая подгрузка с SSD · будет работать медленнее",
"r3_02": "может работать медленнее",
"r3_03": "Такой контекст занимает около {0} ГБ видеопамяти и оставляет модели мало места на этой карте. Strata попробует запустить; если не получится, выберите контекст короче.",
"r3_04": "В работе · {0}",
"r3_05": "В работе чатов: {0}",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"r3_01": "需要更新引擎",
"r3_02": "SSD 流式读取 · 运行会更慢",
"r3_02": "运行可能会更慢",
"r3_03": "这么长的上下文大约占用 {0} GB 显存,留给模型的空间很少。Strata 会尝试运行;如果无法启动,请选择更短的上下文。",
"r3_04": "进行中 · {0}",
"r3_05": "{0} 个对话进行中",
+17 -5
View File
@@ -6,6 +6,8 @@ import './styles/app.css';
import { App } from './app/App';
import { installDropGuard } from './components/DropZone';
import { startRecovery } from './lib/recovery';
import { translationsReady } from './lib/i18n';
import { loadHighlighter } from './components/SyntaxCode';
const el = document.getElementById('root');
if (!el) throw new Error('#root missing');
@@ -13,11 +15,21 @@ if (!el) throw new Error('#root missing');
installDropGuard();
startRecovery();
createRoot(el).render(
<StrictMode>
<App />
</StrictMode>,
);
// The saved language's strings are fetched while the rest of the code loads; the first screen is drawn once they are in (English needs no wait).
// Code colouring (the Prism grammars, 130 kB) is not needed to show the first screen: it is fetched a few seconds later, when the browser is idle,
// so the first code block of the session is already coloured when it appears. A block that appears sooner fetches it itself.
window.setTimeout(() => {
const idle: (run: () => void) => void = 'requestIdleCallback' in window ? (run) => window.requestIdleCallback(run, { timeout: 4000 }) : (run) => window.setTimeout(run, 0);
idle(() => { void loadHighlighter(); void import('./components/TimeSkipPanel'); void import('./components/TimeSkipWorking'); });
}, 3000);
void translationsReady().then(() => {
createRoot(el).render(
<StrictMode>
<App />
</StrictMode>,
);
});
import './styles/annotation-fixes.css';
import './styles/monitor.css';
+7 -5
View File
@@ -2,9 +2,8 @@ import {localeTag} from '../lib/i18n';
import { tr } from '../lib/i18n';
import { UsageDashboard } from '../components/UsageDashboard';
import { FloatingMenu } from '../components/FloatingMenu';
import { TimeSkipPanel } from '../components/TimeSkipPanel';
import { lazyScreen } from '../lib/lazy-screen';
import { timeSkipPrompt, timeSkipTabId, type TimeSkipRun } from '../lib/time-skip';
import { TimeSkipWorking } from '../components/TimeSkipWorking';
import { ChatToolbar } from '../components/ChatToolbar';
import { ChatTelemetry } from '../components/TelemetryPopover';
import { ArtifactCard } from '../components/ArtifactCard';
@@ -40,7 +39,7 @@ import { openExternal } from '../lib/open-external';
import { busyPlan } from '../lib/while-busy';
import { composeQuoted, parseQuoted, type Quote } from '../lib/quotes';
/** Chat — talk to the one loaded model. */
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Markdown } from '../components/Markdown';
import { Btn, Icon, IconBtn, ErrorNotice, type IconName } from '../components/ui';
import { composerCap, scrollBehavior } from '../lib/responsive';
@@ -52,6 +51,9 @@ import { getSlashCommands } from '../lib/commands';
import { useLocation } from '../lib/router';
import { composerSends } from '../lib/shortcut-bindings';
import type { ChatMessage, Session, ToolCall } from '../lib/types';
// The Time-skip panel and its progress card are only drawn in a chat that has a Time-skip run: their code loads then (and a few seconds after start, see main.tsx).
const TimeSkipPanel = lazyScreen(() => import('../components/TimeSkipPanel'), (m) => m.TimeSkipPanel);
const TimeSkipWorking = lazyScreen(() => import('../components/TimeSkipWorking'), (m) => m.TimeSkipWorking);
export function ChatScreen() {
const location = useLocation();
const sessions = useStore((s) => s.sessions);
@@ -535,7 +537,7 @@ export function ChatScreen() {
<Icon name="chevron" size={18}/></button>) : null)}</div>
<Composer draft={draft} setDraft={updateDraft} loaded={!!loaded} streaming={streaming} onSubmit={submit} onKey={onComposerKey} session={session} onPreview={openPreview} textareaRef={textarea}/>
</DropZone>
{tr(!setupVisible && selectedRun && session ? <TimeSkipPanel key={selectedRun.id} run={selectedRun} sessionId={session.id} inline={inlinePreview} expanded={expanded} onCompare={setExpanded} onPreview={openPreview} onClose={() => { setSelectedPreview(null); setExpanded(false); }}/> : null)}
{tr(!setupVisible && selectedRun && session ? <Suspense fallback={null}><TimeSkipPanel key={selectedRun.id} run={selectedRun} sessionId={session.id} inline={inlinePreview} expanded={expanded} onCompare={setExpanded} onPreview={openPreview} onClose={() => { setSelectedPreview(null); setExpanded(false); }}/></Suspense> : null)}
{tr(!setupVisible && planShown ? <PlanPanel chatId={session?.id}/> : null)}
<ArtifactPanel items={previews} onCloseTab={closeTab} selected={setupVisible || planShown ? null : selectedPreview} onSelect={selectPreview} onClose={() => setSelectedPreview(null)} inline={inlinePreview}/>
{tr(resizeHandle)}
@@ -781,7 +783,7 @@ function MessageView({ message: m, isLast, onEditPrompt, onPreview, onClosePrevi
{tr(m.deliverables?.length && !m.streaming ? <div className="deliverables">{m.deliverables.length >= COMPACT_FILES_FROM ? <FilesSummary files={m.deliverables} answer={m.content} sessionId={activeSession ?? ''} onPreview={onPreview}/> : <>{m.deliverables.map(f => <FileCard key={f.path} file={f} sessionId={activeSession ?? ''} onPreview={onPreview}/>)}<EditedFiles files={m.deliverables} sessionId={activeSession ?? ''} onPreview={onPreview}/></>}</div> : null)}
{tr(m.streaming && isLast && !m.timeSkipId ? <StatusLine sessionId={activeSession} startedAt={Date.parse(m.at) || Date.now()} tokens={m.tokens}/> : null)}
{tr(m.timeSkipId && timeSkipReal && !timeSkipDone && timeSkipRun ? <TimeSkipWorking run={timeSkipRun} sessionId={activeSession ?? ''}/> : m.timeSkipId && (!timeSkipReal || (timeSkipRun?.job?.files?.length ?? 0) > 0) ? <button className="timeskip-chat-card" onClick={() => onOpenTimeSkip(m.timeSkipId!)}><Icon name="branch" size={20}/><span><strong>{tr("Time-skip")}</strong><span>{tr(timeSkipDone ? 'Result ready · compare versions' : 'Revising · follow progress')}</span></span><Icon name="chevron" size={16} className="timeskip-card-arrow"/></button> : null)}
{tr(m.timeSkipId && timeSkipReal && !timeSkipDone && timeSkipRun ? <Suspense fallback={null}><TimeSkipWorking run={timeSkipRun} sessionId={activeSession ?? ''}/></Suspense> : m.timeSkipId && (!timeSkipReal || (timeSkipRun?.job?.files?.length ?? 0) > 0) ? <button className="timeskip-chat-card" onClick={() => onOpenTimeSkip(m.timeSkipId!)}><Icon name="branch" size={20}/><span><strong>{tr("Time-skip")}</strong><span>{tr(timeSkipDone ? 'Result ready · compare versions' : 'Revising · follow progress')}</span></span><Icon name="chevron" size={16} className="timeskip-card-arrow"/></button> : null)}
{tr(m.media?.length ? <ChatMedia items={m.media} onPreview={onPreview}/> : null)}
{tr(m.artifacts?.length ? <div className="artifact-cards">{tr(m.artifacts.map(a => <ArtifactCard key={a.id} artifact={a} onPreview={onPreview}/>))}</div> : null)}
+10 -2
View File
@@ -1,8 +1,11 @@
// The chat's own folder and opening its files by path.
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, symlinkSync, existsSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, symlinkSync, existsSync, readFileSync, realpathSync } from 'node:fs';
import { tmpdir as osTmpdir } from 'node:os';
// the canonical scratch root: on a Mac the temp folder is /var/folders/..., which the folder policy refuses as a system folder and which
// is /private/var/... once resolved; the tests make their folders under the resolved name (the policy itself is unchanged)
const tmpdir = () => realpathSync(osTmpdir());
import { join } from 'node:path';
import { createChatFolders, openFile, openCommand, resolveChatFile, refusedFolder } from '../server/chat-files.mjs';
import { startScriptedWorker } from './helpers/scripted-worker.mjs';
@@ -32,6 +35,11 @@ test('a chat folder is remembered per chat, must exist, and is never a system fo
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('a drive the desktop mounted (/run/media/<user>/<drive>) is a fine chat folder; the rest of /run is still system', { skip: process.platform === 'win32' && 'POSIX paths' }, () => {
for (const p of ['/run/media/ann/Backup', '/run/media/ann/Backup/projects/pagoda', '/media/ann/Backup/x', '/mnt/data/x']) assert.equal(refusedFolder(p), null, p);
for (const p of ['/run', '/run/media', '/run/media/ann', '/run/user/1000/x', '/run/systemd', '/usr/share', '/etc', '/var/lib/x']) assert.match(refusedFolder(p), /system folder/, p);
});
test('files open by path from inside the folder only; the browser gets a file:// address', () => {
const dir = mkdtempSync(join(tmpdir(), 'strata-cf2-'));
try {
+1 -1
View File
@@ -35,7 +35,7 @@ test('the notice exists in all nine languages, with the placeholder kept', () =>
test('the catalog finds the notice: the German page shows it in German, with the number filled in', async () => {
const i18n = await bundle('../src/lib/i18n.ts');
i18n.setTranslationLanguage('de');
await i18n.setTranslationLanguage('de');
assert.equal(i18n.tr(TITLE), 'Einige Chats konnten nicht gespeichert werden');
assert.equal(i18n.tr(BUTTON), 'Nicht gespeicherte Chats kopieren');
assert.match(i18n.tr(BODY, [3]), /^Der Chat-Speicher von Strata ist voll, daher gibt es die neuesten Änderungen an 3 Chat\(s\) nur in diesem Fenster/);
+1 -1
View File
@@ -68,7 +68,7 @@ test('STRATA_CLOUDFLARED picks exactly one file; STRATA_CLOUDFLARED_SEARCH=own k
assert.deepEqual(own.map(c => c.source), ['strata', 'component']);
});
test('a real executable on PATH is found and run (a user-local copy, never a system install)', { skip: process.platform === 'win32' }, async () => {
test('a real executable on PATH is found and run (a user-local copy, never a system install)', { skip: process.platform === 'win32' && 'needs a POSIX executable file to run as cloudflared (not on Windows)' }, async () => {
const dir = await mkdtemp(join(tmpdir(), 'strata-cf-path-'));
try {
const bin = join(dir, 'bin'); await mkdir(bin);
+2 -1
View File
@@ -313,7 +313,8 @@ test('packaged: an app update that ends the app is recorded as finished by the n
});
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
// <install>/strata-desktop(.exe) as seen from <install>/host; native-control.mjs looks for the name of the platform it runs on
const exe = new URL(process.platform === 'win32' ? '../../strata-desktop.exe' : '../../strata-desktop', import.meta.url);
const f = fixture({ withApp: true });
try {
const { fileURLToPath } = await import('node:url');
+16 -4
View File
@@ -22,10 +22,22 @@ test('corrupt or unverified downloads cannot become executable', () => {
assert.throws(()=>verifyConnector(bytes,null),/could not be verified/);
});
test('failed concurrent setup shares one download, cleans up and can be retried', async () => {
/** Waits until the fake download service has been asked; fails (instead of spinning for ever) when it never is. */
async function untilAsked(isAsked){
const deadline=Date.now()+10000;
while(!isAsked()){
if(Date.now()>deadline)throw new Error('the installer never asked the download service (it found a connector, or stopped before the request)');
await new Promise(resolve=>setTimeout(resolve,5));
}
}
// The installer first looks for a connector that is already on the computer. The look is pinned to Strata's own empty folder
// (search:false, an empty environment): a cloudflared installed on the machine that runs the test (PATH, /usr/local/bin, ~/.local/bin)
// would otherwise be used, nothing would be downloaded, and the test would wait for ever for a request that never comes.
test('failed concurrent setup shares one download, cleans up and can be retried', {timeout:30000}, async () => {
const directory=await mkdtemp(join(tmpdir(),'strata-connector-test-'));
let requests=0, release;
const installer=createConnectorInstaller({directory,platform:'linux',arch:'x64',fetchImpl:async()=>{
const installer=createConnectorInstaller({directory,platform:'linux',arch:'x64',env:{},home:directory,search:false,fetchImpl:async()=>{
requests++;
await new Promise(resolve=>{release=resolve;});
return {ok:false};
@@ -33,7 +45,7 @@ test('failed concurrent setup shares one download, cleans up and can be retried'
try {
const first=installer.ensure(),second=installer.ensure();
assert.equal(first,second);
while(!release)await new Promise(resolve=>setImmediate(resolve));
await untilAsked(()=>release);
release();
const results=await Promise.allSettled([first,second]);
assert.ok(results.every(result=>result.status==='rejected'));
@@ -41,7 +53,7 @@ test('failed concurrent setup shares one download, cleans up and can be retried'
assert.deepEqual(await readdir(directory),[]);
release=null;
const retry=installer.ensure();
while(!release)await new Promise(resolve=>setImmediate(resolve));
await untilAsked(()=>release);
release();
await assert.rejects(retry,/download service/);
assert.equal(requests,2);
File diff suppressed because it is too large. Load diff
+5 -4
View File
@@ -9,11 +9,12 @@ import { join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const read = rel => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf8').replace(/\r\n/g, '\n');
const stdin = { contents: "import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { PcFitCard } from './src/components/PcFitCard'; export { setTranslationLanguage } from './src/lib/i18n'; export const render = props => renderToStaticMarkup(createElement(PcFitCard, props));", resolveDir: fileURLToPath(new URL('..', import.meta.url)), loader: 'ts' };
const stdin = { contents: "import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { PcFitCard } from './src/components/PcFitCard'; export { setTranslationLanguage, loadCatalog } from './src/lib/i18n'; export const render = props => renderToStaticMarkup(createElement(PcFitCard, props));", resolveDir: fileURLToPath(new URL('..', import.meta.url)), loader: 'ts' };
const built = await build({ stdin, bundle: true, write: false, format: 'esm', platform: 'node', loader: { '.json': 'json', '.tsx': 'tsx' }, jsx: 'automatic', logLevel: 'silent', banner: { js: "import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);" } });
const dir = mkdtempSync(join(tmpdir(), 'strata-card-test-'));
writeFileSync(join(dir, 'card.mjs'), built.outputFiles[0].text); // a file, not a data: URL: the bundled react-dom/server needs require()
const { render, setTranslationLanguage } = await import(pathToFileURL(join(dir, 'card.mjs')).href);
const { render, setTranslationLanguage, loadCatalog } = await import(pathToFileURL(join(dir, 'card.mjs')).href);
for (const l of ['zh', 'ja', 'de', 'es', 'ru', 'pt', 'ko', 'fr']) await loadCatalog(l); // the non-English catalogs load on first use (src/lib/i18n.ts loadCatalog); load them before the first tr()
rmSync(dir, { recursive: true, force: true });
const fit = (spillGb, known = true) => ({ memoryGb: 250, vramUseGb: 10, ramUseGb: 240, spillGb, known });
const card = props => render({ gpuName: 'RTX 5070', ramGb: 64, vramGb: 12, ...props });
@@ -67,7 +68,7 @@ test('the Models screen passes the set\'s disk to the card, and the version line
const screen = read('../src/components/NativeModels.tsx');
assert.match(screen, /<PcComputer [\s\S]*?diskGb=\{selectedSize\?\.multi_file \? selectedSize\.disk_gb \?\? selectedSize\.download_gb : null\}/);
const picker = read('../src/components/VersionPicker.tsx');
assert.match(picker, /v\.multi_file && v\.disk_gb \? tr\("\{0\} RAM \+ VRAM, plus about \{1\} GB of disk", \[gb\(fit\.memoryGb\), Math\.round\(v\.disk_gb\)\]\)/, 'Details: the disk of the set for DeepSeek');
assert.match(picker, /v\.multi_file && v\.disk_gb \? tr\("\{0\} RAM \+ VRAM, plus about \{1\} GB of disk", \[gb\(size\), Math\.round\(v\.disk_gb\)\]\)/, 'Details: the disk of the set for DeepSeek');
assert.match(picker, /v\.multi_file && v\.disk_gb \? tr\("Needs \{0\} of memory and about \{1\} GB of disk"/, 'plain: the disk for DeepSeek only');
assert.match(picker, /: tr\("Needs \{0\} of memory", \[whole\(fit\.memoryGb\)\]\)/, 'every other model keeps its line');
assert.match(picker, /: tr\("Needs \{0\} of memory", \[whole\(size\)\]\)/, 'every other model keeps its line');
});
+260
View File
@@ -0,0 +1,260 @@
/**
* 0.2.13: the app fetches the tuned DFlash2 drafter on every path where Auto resolves to it (a plain load, a restart of the stopped models, a tuning run), like the Models screen
* own Load does, with the 12 hour rule (--throttle); a drafter the person named or saved wins; the plan and the Settings hint carry the size of the drafter that is really fetched.
* The decision is Control (strata drafting plan / fetch; tests/terminal/test_df2ux_0213.py); this file drives the host with a fake Control.
*/
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, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { tmpdir } from 'node:os';
import { createNativeControl } from '../server/native-control.mjs';
import { createControl } from '../server/control-runtime.mjs';
import { resolveDrafter as hostResolve } from '../server/native-settings.mjs';
const SPLIT_PLAN = { ok: true, split: true, choice: 'mtp', drafter: 'mtp', needed: false, gb: 0, line: 'Drafting: MTP (the all-cards split engine uses the MTP draft layer, 5.2 GB)', serves: true };
const DFLASH2_PLAN = { ok: true, split: false, choice: 'dflash2', drafter: 'dflash2', needed: true, gb: 1.2, mtp_needed: false, mtp_gb: 0, line: 'Drafting: DFlash2', serves: true };
/**
* A packaged install on two 48 GB cards. `plan(options, state)` answers `drafting plan` (state.installs says how many layer installs ran), `split(options, files)` answers `split-engine apply`
* (it may write options.outPath), `installFails` makes the installer's MCP tool fail, `startFails` fails the first N engine starts.
*/
function fixture({ fetchResult = { ok: true, event: 'result', pack: 'C:/d/dflash2/x.pack', skipped: false }, plan, split = null, installFails = false, startFails = 0, metrics = {}, config = {} } = {}) {
const root = mkdtempSync(join(tmpdir(), 'strata-splitapp-test-'));
const install = join(root, 'install'), data = join(root, 'host-data'), home = join(root, 'home'), modelsDir = join(home, 'models');
for (const d of [install, data, modelsDir]) mkdirSync(d, { recursive: true });
const earlier = process.env.STRATA_INSTALL_DIR;
process.env.STRATA_INSTALL_DIR = install;
const dataFolder = join(root, 'Strata-data'), modelFolder = join(dataFolder, 'models', 'UD-Q4_K_XL');
const gguf = join(modelFolder, 'Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00002.gguf');
mkdirSync(modelFolder, { recursive: true });
writeFileSync(gguf, 'GGUF'); writeFileSync(gguf.replace('00001', '00002'), 'GGUF');
const pack = join(dataFolder, 'packs', 'ud_q4_k_xl'), mtp = join(dataFolder, 'mtp', 'rt');
mkdirSync(join(pack, 'tokenizer'), { recursive: true });
writeFileSync(join(pack, 'native_experts.txt'), 'x');
const configPath = join(install, 'strata-ud_q4_k_xl.json');
const args = ['--pack', pack, '--native', gguf, '--ple-gguf', gguf.replace('00001', '00002'), '--mtp', mtp, '--max-context', '131072', '--kv', 'int8'];
writeFileSync(configPath, JSON.stringify({ exe: 'component:strata-engine', tokenizer: join(pack, 'tokenizer'), args, model_name: 'qwen3.8-flash-next-ud_q4_k_xl', port: 8080, gpu: [0, 1], ...config }));
writeFileSync(process.env.STRATA_SETTINGS_FILE, JSON.stringify({ data_dir: join(root, 'host-owned', 'Strata-data'), data_dir_at: 1700000000 }));
const state = { installs: 0, planCalls: [], fetchCalls: [], splitCalls: [], saved: [], starts: 0, rpc: [] };
const control = {
control: { dir: join(root, 'control'), python: join(root, 'control', 'python', 'python') }, version: '0.2.10',
async installedModels() {
return { install_dir: install, models: [{ id: 'ud_q4_k_xl', name: 'qwen3.8-flash-next-ud_q4_k_xl', source_config: configPath, context_tokens: 131072, backend: 'cuda', component_status: 'present', missing: [], issue_codes: [] }] };
},
async engine() { return { usable: true, backend: 'cuda', components_dir: join(root, 'c'), engine: { version: '0.1.56', platform: 'linux-x86_64', backend: 'cuda', exe: join(root, 'engine', 'strata'), root: join(root, 'engine') } }; },
async draftingPlan(options) { state.planCalls.push(options); return typeof plan === 'function' ? plan(options, state) : plan; },
async draftingFetch(options) { state.fetchCalls.push(options); if (fetchResult instanceof Error) throw fetchResult; return fetchResult; },
async splitEngine(options) {
state.splitCalls.push({ ...options, input: JSON.parse(readFileSync(options.configPath, 'utf8')) });
if (!split) return { ok: true, split: false, line: '', warn: false, reasons: [], written: null };
return split(options, state);
},
async saveProfile(name, path) { state.saved.push(JSON.parse(readFileSync(path, 'utf8'))); return {}; },
async start() { state.starts += 1; if (state.starts <= startFails) throw new Error('The engine stopped: out of memory'); return { ok: true, state: 'ready' }; },
async stop() { return { ok: true }; },
};
const catalog = { families: [{ family: 'qwen', title: 'Qwen3.8-Flash-Next', about: 'x', images: false, experimental: false, sizes: [
{ id: 'ud_q4_k_xl', model: 'UD-Q4_K_XL', download_gb: 22, ram_needed_gb: 60, about: 'x', on_this_pc: 'fits', installed: true, images: false, experimental: false }] }],
contexts: [32768, 131072], recommendation: { family: 'qwen', model: 'UD-Q4_K_XL', context: 131072 }, context_note: '' };
const hardware = { ram_gb: 256, cpu: { name: 'cpu', cores: 32 }, engine_vendor: 'nvidia', gpus: [0, 1].map(index => ({ index, name: 'NVIDIA RTX A6000', vendor: 'nvidia', vram_gb: 48, arch: 86, usable: true })) };
const spawnProcess = (command, argv) => {
if (!String(argv[0]).endsWith('strata_mcp.py')) throw new Error('no process but the installer may be started in packaged mode: ' + command + ' ' + argv.join(' '));
const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.exitCode = null;
child.kill = () => { child.exitCode = 0; queueMicrotask(() => child.emit('exit', 0)); return true; };
child.stdin = { write(line) {
const message = JSON.parse(line); if (!message.id) return;
if (message.method === 'tools/call') state.rpc.push(message.params);
let result;
if (message.method === 'initialize') result = { protocolVersion: '2024-11-05' };
else if (message.method === 'tools/list') result = { tools: ['strata_models', 'strata_status', 'strata_install', 'strata_remove'].map(name => ({ name })) };
else if (message.params.name === 'strata_models') result = { structuredContent: catalog };
else if (message.params.name === 'strata_status') result = { structuredContent: { hardware, recommendation: catalog.recommendation, install: null, running: [], summary: 'x' } };
else if (Object.keys(message.params.arguments ?? {}).length === 0) result = { structuredContent: { summary: 'Install finished', install: { running: false, exit_code: 0 } } };
else if (message.params.arguments?.confirm) {
if (installFails) result = { isError: true, structuredContent: { error: 'The download did not finish.' } };
else { state.installs += 1; mkdirSync(mtp, { recursive: true }); writeFileSync(join(mtp, 'dense.bin'), 'x'); result = { structuredContent: { summary: 'Install started', plan: {}, install: { running: true, step: '1/7' } } }; }
} else result = { structuredContent: { summary: 'Plan', plan: { title: 'Qwen3.8-Flash-Next UD-Q4_K_XL', download_gb: 5.2, disk_short: null } } };
queueMicrotask(() => child.stdout.emit('data', Buffer.from(JSON.stringify({ jsonrpc: '2.0', id: message.id, result }) + String.fromCharCode(10))));
} };
return child;
};
const fetcher = async url => String(url).endsWith('/metrics') ? Response.json({ engine: metrics }) : Response.json({ status: 'ok' });
const native = createNativeControl({ control, directory: data, spawnProcess, fetcher, requests: () => [], modelsDirectory: () => modelsDir, hardwareProbe: () => [0, 1].map(index => ({ index, vram_gb: 48 })) });
return { native, state, root, gguf, modelFolder, mtp, configPath, args,
cleanup: () => { if (earlier === undefined) delete process.env.STRATA_INSTALL_DIR; else process.env.STRATA_INSTALL_DIR = earlier; rmSync(root, { recursive: true, force: true }); } };
}
const finish = async (native, id) => { for (let i = 0; i < 1500; 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'); };
const load = (f, args = {}) => f.native.operation('load', { model: 'ud_q4_k_xl', ...args });
/** The split engine's answer: the layer config with the split's engine and a lane count (Control's split_config, in miniature), written where the host asked. */
const splitAnswer = (options) => {
const cfg = JSON.parse(readFileSync(options.configPath, 'utf8'));
const lanes = Math.min(8, cfg.parallel ?? 6);
writeFileSync(options.outPath, JSON.stringify({ ...cfg, exe: '/e/strata-split-2-sm86', args: ['--native', cfg.args[cfg.args.indexOf('--native') + 1], '--batch', String(lanes)], drafting: 'mtp', split_fallback: { exe: cfg.exe, args: cfg.args, parallel: cfg.parallel }, split_engine_note: 'Using the all-cards split engine on 2 cards.' }));
return { ok: true, split: true, line: 'Using the all-cards split engine on 2 cards.', warn: false, reasons: [], cards: 2, written: options.outPath };
};
/** Before the layer is fetched Control says split + mtp_needed; after it, split with the layer here. */
const splitPlan = (_options, state) => state.installs === 0 ? { ...SPLIT_PLAN, mtp_needed: true, mtp_gb: 5.2 } : { ...SPLIT_PLAN, mtp_needed: false, mtp_gb: 0 };
const TUNED_PLAN = { ok: true, split: false, choice: 'dflash2', drafter: 'dflash2', needed: true, tuned_needed: true, tuned: true, gb: 0.34, drafter_gb: 0.34, mtp_needed: false, mtp_gb: 0, line: 'Drafting: DFlash2', serves: true };
const READY_PLAN = { ...TUNED_PLAN, needed: false, tuned_needed: false, gb: 0 };
const planAfterFetch = (_o, state) => (state.fetchCalls.length ? READY_PLAN : TUNED_PLAN);
test('a plain load fetches the tuned drafter with the 12 hour rule, then plans again', async () => {
const f = fixture({ plan: planAfterFetch });
try {
await f.native.refresh(true);
const job = await finish(f.native, load(f).id);
assert.equal(job.state, 'done', job.error);
assert.equal(f.state.fetchCalls.length, 1);
assert.equal(f.state.fetchCalls[0].mode, 'auto');
assert.equal(f.state.fetchCalls[0].throttle, true, 'nobody asked for it: the Terminal 12 hour rule');
assert.ok(f.state.planCalls.length >= 2, 'planned again after the fetch');
} finally { await f.native.close(); f.cleanup(); }
});
/* CLEAN-1A (owner 11-07): the plain Load follows Auto everywhere. Where Auto's drafter is the standard DFlash2 pack (Intel Arc, UD-Q4_K_XL on NVIDIA 72 GB / 12 GB and the Spark), Control says `needed` without `tuned_needed`; the app fetches it just the same, asking Control. */
const STOCK_PLAN = { ...TUNED_PLAN, tuned_needed: false, tuned: false, gb: 1.2, drafter_gb: 1.2 };
const stockAfterFetch = (_o, state) => (state.fetchCalls.length ? { ...STOCK_PLAN, needed: false, gb: 0 } : STOCK_PLAN);
test('a plain load fetches the standard DFlash2 drafter too when that is what Auto runs', async () => {
const f = fixture({ plan: stockAfterFetch });
try {
await f.native.refresh(true);
const job = await finish(f.native, load(f).id);
assert.equal(job.state, 'done', job.error);
assert.equal(f.state.fetchCalls.length, 1);
assert.equal(f.state.fetchCalls[0].mode, 'auto');
assert.equal(f.state.fetchCalls[0].throttle, true);
} finally { await f.native.close(); f.cleanup(); }
});
test('a plain load fetches nothing when Auto needs no download', async () => {
const f = fixture({ plan: () => ({ ...STOCK_PLAN, needed: false, gb: 0 }) });
try {
await f.native.refresh(true);
const job = await finish(f.native, load(f).id);
assert.equal(job.state, 'done', job.error);
assert.equal(f.state.fetchCalls.length, 0);
} finally { await f.native.close(); f.cleanup(); }
});
test('the Models screen own Load keeps its un-throttled fetch and does not fetch twice', async () => {
const f = fixture({ plan: planAfterFetch });
try {
await f.native.refresh(true);
const job = await finish(f.native, load(f, { values: { drafting: 'auto' } }).id);
assert.equal(job.state, 'done', job.error);
assert.equal(f.state.fetchCalls.length, 1);
assert.ok(f.state.fetchCalls[0].throttle !== true);
} finally { await f.native.close(); f.cleanup(); }
});
test('a drafter the person saved wins: MTP, off and DFlash2 are never fetched by a plain load', async () => {
for (const drafting of ['mtp', 'off', 'dflash2']) {
const f = fixture({ plan: TUNED_PLAN, config: { drafting } });
try {
await f.native.refresh(true);
const job = await finish(f.native, load(f).id);
assert.equal(job.state, 'done', job.error);
assert.equal(f.state.fetchCalls.length, 0, drafting);
} finally { await f.native.close(); f.cleanup(); }
}
});
test('nothing needed (the tuned drafter is here, or Control says MTP): a plain load fetches nothing', async () => {
const f = fixture({ plan: READY_PLAN });
try {
await f.native.refresh(true);
const job = await finish(f.native, load(f).id);
assert.equal(job.state, 'done', job.error);
assert.equal(f.state.fetchCalls.length, 0);
} finally { await f.native.close(); f.cleanup(); }
});
test('the feed has nothing (skipped): the load goes on exactly as before, no note', async () => {
const f = fixture({ plan: TUNED_PLAN, fetchResult: { ok: true, event: 'result', pack: null, skipped: true } });
try {
await f.native.refresh(true);
const job = await finish(f.native, load(f).id);
assert.equal(job.state, 'done', job.error);
assert.equal(f.state.fetchCalls.length, 1);
assert.ok(!job.drafterNote);
} finally { await f.native.close(); f.cleanup(); }
});
test('a fetch that fails for a real reason never stops the load and the job says why', async () => {
const f = fixture({ plan: TUNED_PLAN, fetchResult: new Error('The tuned DFlash2 drafter did not pass the safety check, so it was deleted.') });
try {
await f.native.refresh(true);
const job = await finish(f.native, load(f).id);
assert.equal(job.state, 'done', job.error);
assert.match(job.drafterNote ?? '', /safety check/);
} finally { await f.native.close(); f.cleanup(); }
});
test('a tuning run fetches it first, and not for a saved MTP', async () => {
for (const [drafting, calls] of [[undefined, 1], ['mtp', 0]]) {
const f = fixture({ plan: TUNED_PLAN, config: drafting ? { drafting } : {} });
try {
await f.native.refresh(true);
const id = f.native.operation('autotune', { model: 'ud_q4_k_xl' }).id;
await finish(f.native, id);
assert.equal(f.state.fetchCalls.length, calls, String(drafting));
if (calls) assert.equal(f.state.fetchCalls[0].throttle, true);
} finally { await f.native.close(); f.cleanup(); }
}
});
test('the install plan asks Control about this model and lists the tuned size, and says nothing for MTP', async () => {
const f = fixture({ plan: (o) => (o.mode === 'mtp' ? { ...READY_PLAN, choice: 'mtp', drafter: 'mtp' } : TUNED_PLAN) });
try {
await f.native.refresh(true);
const got = await f.native.plan({ family: 'qwen', model: 'UD-Q4_K_XL', drafting: 'auto' });
assert.equal(f.state.planCalls.find(c => c.model)?.model, 'UD-Q4_K_XL');
assert.equal(got.drafting.gb, 0.34);
assert.equal(got.drafting.tuned, true);
const none = await f.native.plan({ family: 'qwen', model: 'UD-Q4_K_XL', drafting: 'mtp' });
assert.equal(none.drafting, undefined, 'MTP chosen: no drafter is listed');
} finally { await f.native.close(); f.cleanup(); }
});
test('the Settings hint keeps its wording and changes only the number', async () => {
const { draftingHint, NATIVE_FIELDS } = await import('../server/native-settings.mjs');
assert.equal(NATIVE_FIELDS.find(x => x.id === 'drafting').hint, draftingHint(1.2));
assert.equal(draftingHint(0.34), draftingHint(1.2).replace('1.2 GB', '0.34 GB'));
assert.match(draftingHint(0.52), /DFlash2 downloads a 0\.52 GB drafter once, the MTP its draft layer once\./);
assert.equal(draftingHint(0), draftingHint(1.2), 'no number from Control: the generic hint');
const f = fixture({ plan: TUNED_PLAN });
try {
await f.native.refresh(true);
assert.deepEqual(await f.native.settingsHints('ud_q4_k_xl'), { drafting: draftingHint(0.34) });
assert.equal(f.state.planCalls.at(-1).mode, 'dflash2');
} finally { await f.native.close(); f.cleanup(); }
});
test('control passes the model, the family and --throttle to Control and not --model beside a run config', async () => {
const calls = [];
const control = createControl({ control: { dir: 'C:/c', python: 'py', cli: 'strata.py' }, run: async (args) => { calls.push(args); return { code: 0, json: { ok: true }, text: '', stderr: '' }; } });
await control.draftingPlan({ mode: 'auto', family: 'unsloth', model: 'UD-Q4_K_XL', dataDir: 'C:/d' });
assert.deepEqual(calls[0], ['drafting', 'plan', '--mode', 'auto', '--family', 'unsloth', '--model', 'UD-Q4_K_XL', '--data-dir', 'C:/d', '--output', 'json']);
await control.draftingPlan({ configPath: 'C:/w/c.json', model: 'UD-Q4_K_XL' });
assert.ok(!calls[1].includes('--model'));
await control.draftingFetch({ configPath: 'C:/w/c.json', mode: 'auto', throttle: true });
assert.deepEqual(calls[2], ['drafting', 'fetch', '--config', 'C:/w/c.json', '--mode', 'auto', '--if-needed', '--throttle', '--output', 'jsonl']);
});
test('a restart of the stopped models fetches it before the start, and so does every path through the one helper', async () => {
const { readFileSync: read } = await import('node:fs');
const source = read(new URL('../server/native-control.mjs', import.meta.url), 'utf8');
const restart = source.slice(source.indexOf('async function restartStopped'), source.indexOf('function dismissRestart'));
assert.match(restart, /await autoDrafterFor\(job,instance\.model,[^)]*\);[\s\S]*await control\.start\(instance\.profile/, 'before the engine starts again');
const tune = source.slice(source.indexOf('async function autotune'), source.indexOf('function operation'));
assert.match(tune, /await autoDrafterFor\(job,args\.model,/);
assert.match(source, /if\(args\.values\?\.drafting===undefined&&drafts\?\.needed===true\)\{const got=await fetchAutoDrafter\(/, 'a plain load');
assert.match(source, /!autoChoice\(readJson\(configFile\)\.drafting\)\)return false/, 'a saved choice wins');
});
+1 -1
View File
@@ -95,7 +95,7 @@ test('every sentence of the Drafting choice is in the catalog in all nine langua
// the DFlash2 option (df2-<lang>.json): the paragraph under the dropdown, the lines about the card, the two new notes
"Drafting: MTP (DFlash2 isn't available on this graphics card)", "There is no internet connection, so the DFlash2 drafter can't be downloaded now.",
'On graphics cards like yours, DFlash2 was faster than MTP on some kinds of text and slower on others in our tests.',
'On graphics cards like yours, DFlash2 was slower than MTP in our tests.', 'DFlash2 has not been tested on your graphics card yet.', 'SSD: not needed'];
'On graphics cards like yours, DFlash2 was slower than MTP in our tests.', 'DFlash2 has not been tested on your graphics card yet.', 'On your Mac, DFlash2 was faster than MTP on this model in our tests.', 'DFlash2 is off by default on this graphics card for now; choose it in Drafting to use it.', 'SSD: not needed'];
const slots = t => (t.match(/\{\d+\}/g) ?? []).sort().join();
for (const text of sentences) {
const id = idOf(text);
+3 -1
View File
@@ -215,6 +215,8 @@ test('all nine languages carry the dialog\'s words with the same slots', () => {
if (l !== 'en') assert.notEqual(catalogs[l][k], catalogs.en[k], `${l} ${k} is translated`);
}
}
// English is in the main catalog (src/lib/i18n.ts); each other language is one file (src/lib/catalog-<lang>.ts) that the app fetches when the language is used.
const i18n = src('src/lib/i18n.ts');
for (const l of langs) assert.match(i18n, new RegExp(`\\.\\.\\.upd_${l}\\b`));
assert.match(i18n, /\.\.\.upd_en\b/);
for (const l of langs.filter(x => x !== 'en')) assert.match(src(`src/lib/catalog-${l}.ts`), new RegExp(`from '\\.\\./locales/upd-${l}\\.json'`));
});
+29
View File
@@ -0,0 +1,29 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {resolve} from 'node:path';
import {createRequire} from 'node:module';
import {build} from 'esbuild';
/* The family menu rows for the update feed's models: a model this engine cannot use is a note with its reason, not a choice; one that is allowed shows the reason beside its name. */
const root=resolve(import.meta.dirname,'..');
const virtual={'./ui':"export const Icon=()=>null;",'./FloatingMenu':"export const FloatingMenu=()=>null;"};
const stubs={name:'stubs',setup(b){b.onResolve({filter:/^\.\/(ui|FloatingMenu)$/},a=>({path:a.path,namespace:'stub'}));b.onLoad({filter:/.*/,namespace:'stub'},a=>({contents:virtual[a.path],loader:'js'}));}};
const out=await build({entryPoints:[resolve(root,'src/components/FamilySwitch.tsx')],bundle:true,write:false,platform:'node',format:'cjs',jsx:'automatic',plugins:[stubs],external:['react','react-dom','react/jsx-runtime'],loader:{'.json':'json'}});
const require=createRequire(resolve(root,'package.json'));
const mod={exports:{}};new Function('module','exports','require',out.outputFiles[0].text)(mod,mod.exports,require);
const {renderToStaticMarkup}=require('react-dom/server');
const React=require('react');
const {FamilyMenuList}=mod.exports;
const fam=(family,title,extra={})=>({family,title,about:'',experimental:false,images:true,sizes:[{id:family+'-x'}],...extra});
test('a model the engine cannot use is a note with its reason; an allowed one shows the reason beside its name',()=>{
const reason='needs a newer Strata engine: update Strata to use it';
const shown=[fam('qwen3.8','Qwen3.8-Flash-Next'),fam('qwen4-test','Qwen4 Test',{feed:true,available:false,unavailable_reason:reason})];
const missing=[fam('later-one','Later One',{sizes:[],feed:true,available:false,unavailable_reason:"this Strata version doesn't know this model yet: update Strata to use it"})];
const html=renderToStaticMarkup(React.createElement(FamilyMenuList,{shown,missing,current:shown[0],onPick(){}}));
assert.equal((html.match(/role="menuitemradio"/g)||[]).length,2);
assert.equal((html.match(/role="note"/g)||[]).length,1);
assert(html.includes('Qwen4 Test</strong><small>'+reason+'</small>'));
assert(html.includes('Later One</strong><small>this Strata version doesn&#x27;t know this model yet: update Strata to use it</small>'));
const plain=renderToStaticMarkup(React.createElement(FamilyMenuList,{shown:[shown[0]],missing:[],current:shown[0],onPick(){}}));
assert(!plain.includes('<small>'));assert(!plain.includes('role="note"'));
});
Loaded 100 of 900 files, more files were not shown because too many files have changed in this diff. Show more