mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-08 08:12:16 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa663fabc5 | ||
|
|
721c6f7ff5 | ||
|
|
a04f5505aa | ||
|
|
36f580a5c3 | ||
|
|
ad638b93e5 | ||
|
|
32ad109816 | ||
|
|
6270e2d9d6 | ||
|
|
ce27d13539 | ||
|
|
2dbdea2a77 | ||
|
|
da1eb6bc69 | ||
|
|
2cea5afbc5 | ||
|
|
0b9539877f | ||
|
|
9551b4168d | ||
|
|
1aff6d1411 | ||
|
|
3f8f9aeaa6 | ||
|
|
684881246d | ||
|
|
d44144d7bb | ||
|
|
df7d407742 | ||
|
|
0597aaaeb7 | ||
|
|
c4ce40bf5e | ||
|
|
add4362139 | ||
|
|
ff91bc3729 | ||
|
|
67996022cd | ||
|
|
7988837183 | ||
|
|
77b1a50541 | ||
|
|
902239e7f7 | ||
|
|
c1c3eab540 | ||
|
|
1fac68d6f2 | ||
|
|
aa4b225237 | ||
|
|
bd71ea81b9 | ||
|
|
4d8ff9646f | ||
|
|
536460c3d8 | ||
|
|
061a285832 | ||
|
|
e752c65455 | ||
|
|
dfd1b0f291 | ||
|
|
8b8f8a10ee | ||
|
|
7bf6e4d3b7 | ||
|
|
56ac21a217 | ||
|
|
829913ea59 | ||
|
|
dba7d0e0ab | ||
|
|
535cacb052 | ||
|
|
67a6ee76f4 | ||
|
|
a264168390 | ||
|
|
e6c5997091 | ||
|
|
7063f3adca | ||
|
|
1c69e23658 | ||
|
|
f613637cbd | ||
|
|
4f47009d08 | ||
|
|
94a4a34b05 | ||
|
|
8993997e02 | ||
|
|
9741a61d7b | ||
|
|
06f313b019 | ||
|
|
236e13b4af | ||
|
|
c893eeebf1 | ||
|
|
9b4092a94d | ||
|
|
3283d2ae49 | ||
|
|
b79d6930c1 | ||
|
|
883e21bf2e | ||
|
|
4e4921f63e | ||
|
|
bd7acc0e6d | ||
|
|
c4413c0b54 | ||
|
|
bc1a3ba2ed | ||
|
|
3dd11c6dc1 | ||
|
|
eb4736c45e | ||
|
|
66b43d4a7f | ||
|
|
91eb05a228 | ||
|
|
4228e6ee11 | ||
|
|
9caf39495c | ||
|
|
0c53372a0e | ||
|
|
a3e4cb64fd | ||
|
|
dc2c4229e9 | ||
|
|
60d856a18e | ||
|
|
abab458d74 | ||
|
|
9cf91f3555 | ||
|
|
f30d191d54 | ||
|
|
75ee28fd3d | ||
|
|
fe2b9f4094 | ||
|
|
54768d25b1 | ||
|
|
4499c69681 | ||
|
|
beeea0abf3 | ||
|
|
569f7c739d | ||
|
|
ff84ff8164 | ||
|
|
61a4b6bd02 | ||
|
|
192ab03813 | ||
|
|
16c4da9443 | ||
|
|
9030da09f9 | ||
|
|
980c95a6d7 | ||
|
|
86a751af27 | ||
|
|
84b712a3d8 | ||
|
|
db42e59930 | ||
|
|
c1e80f7952 | ||
|
|
ea349eb8b8 | ||
|
|
08a5e6b34a | ||
|
|
353c9f2556 | ||
|
|
ec03ae0106 | ||
|
|
780a69b41b | ||
|
|
c7899f9c89 | ||
|
|
e61b7024cf | ||
|
|
25abbdd90c | ||
|
|
f750086183 | ||
|
|
31adebc8b7 | ||
|
|
4ccfd7ca30 | ||
|
|
b750d757f6 | ||
|
|
d9d033be86 | ||
|
|
9c375ccb7d | ||
|
|
90a7aeea51 | ||
|
|
d6c1936021 | ||
|
|
fb3bae20c6 | ||
|
|
1d20d3e2b0 | ||
|
|
13a4153be4 | ||
|
|
ea897551ee | ||
|
|
ed0c8939aa | ||
|
|
47ccec7c6a | ||
|
|
aef053e353 | ||
|
|
c22e5e7633 | ||
|
|
27cbbfe6d6 | ||
|
|
97c31168aa | ||
|
|
5faf357b49 | ||
|
|
77284b5e33 | ||
|
|
4b49729a1e | ||
|
|
014fd6b5db | ||
|
|
5bfce4bea0 | ||
|
|
15fdb69563 | ||
|
|
ad9bb57ddc | ||
|
|
b18700c8f7 | ||
|
|
c02a0b8b0d | ||
|
|
7a03fc9414 | ||
|
|
d924beffe5 | ||
|
|
bdaa93edf0 | ||
|
|
1736b558bd | ||
|
|
0cd44323e4 | ||
|
|
e3eeb93972 | ||
|
|
58f2acc05b | ||
|
|
6fcde7e305 | ||
|
|
b3c86c783f | ||
|
|
f1e41cdee2 | ||
|
|
7dd5cf18e6 | ||
|
|
8aacdbc7a0 | ||
|
|
3eabee6049 | ||
|
|
c896f73934 | ||
|
|
93a91a2767 | ||
|
|
12939aa64d | ||
|
|
dfa3f4025c | ||
|
|
ac08a0820d | ||
|
|
05041e5db0 | ||
|
|
3157c38c68 | ||
|
|
d2d198a492 | ||
|
|
8c9bd206ce | ||
|
|
46bddbbb74 | ||
|
|
6953d896d0 | ||
|
|
baa8f616de | ||
|
|
ac669f199d | ||
|
|
0003604859 | ||
|
|
e168b76b8c | ||
|
|
f53664c159 | ||
|
|
44f8c334de | ||
|
|
a22d557eee | ||
|
|
51d23fdd1e | ||
|
|
a62ca064a3 | ||
|
|
8183c6b671 | ||
|
|
c33fa7ba6d | ||
|
|
b7958c044f | ||
|
|
06e51dfaef | ||
|
|
96ca4383c7 | ||
|
|
2ecdc2e511 | ||
|
|
5353097c90 | ||
|
|
9011bd107a | ||
|
|
52b104b1ac | ||
|
|
00a2cfa079 | ||
|
|
6eaf32075c | ||
|
|
698a87d999 | ||
|
|
6c6267f33c | ||
|
|
cec75af6ae | ||
|
|
8ac2a8385e | ||
|
|
894dc24c6c | ||
|
|
c4dcde7145 | ||
|
|
67d5f44bf8 | ||
|
|
6a35eb6c5f | ||
|
|
12d0a4a051 | ||
|
|
c5e0e14823 | ||
|
|
9bb00346a2 | ||
|
|
672612ac1c | ||
|
|
f4e7685ccf | ||
|
|
267a2f4462 | ||
|
|
eec705cc98 | ||
|
|
b0d5f6e57c | ||
|
|
f2413d77e9 | ||
|
|
4f8770d13e | ||
|
|
984f52c827 | ||
|
|
aebdc298f9 | ||
|
|
fd5823ab83 | ||
|
|
d50da4e3a1 | ||
|
|
624a226d13 | ||
|
|
fa3252ad5d | ||
|
|
9797056e4d | ||
|
|
e2361d80d2 | ||
|
|
afd24ea6ae | ||
|
|
4ebba26767 | ||
|
|
598372c3fd | ||
|
|
2e477ecd40 | ||
|
|
0e121c7a13 | ||
|
|
415e618085 | ||
|
|
dfe29bce61 | ||
|
|
f51b80c222 | ||
|
|
17c585c402 | ||
|
|
41e804d93c | ||
|
|
524ff90e1a | ||
|
|
86834afedc | ||
|
|
fa77717a57 | ||
|
|
60d847ba33 | ||
|
|
a3f76dfd63 | ||
|
|
b4418e30df | ||
|
|
8a7102ec4d | ||
|
|
be26cb7995 | ||
|
|
736a4136e1 | ||
|
|
1d31e2a1e4 | ||
|
|
b8f9c63ae7 | ||
|
|
cdc3a26577 | ||
|
|
cdf8b98f54 | ||
|
|
5be4964c2b | ||
|
|
d914211e64 | ||
|
|
71c674dcdb | ||
|
|
0d38517888 | ||
|
|
e51e84e22a | ||
|
|
cffd240834 | ||
|
|
062fd4a95e | ||
|
|
f73ae5a62e | ||
|
|
e6e14f2714 | ||
|
|
a6cbc3621e | ||
|
|
aee5db5eb1 | ||
|
|
611c1701d3 | ||
|
|
5ec845f6bc | ||
|
|
a23b5f209c | ||
|
|
94413c7057 | ||
|
|
8fda835383 | ||
|
|
4278304914 | ||
|
|
75c57be831 | ||
|
|
2dbb263b02 | ||
|
|
955c709d96 | ||
|
|
70efffb5b6 | ||
|
|
027691b33d | ||
|
|
00ce92068a | ||
|
|
18022ceaf3 | ||
|
|
1904c0261b | ||
|
|
642e76948e | ||
|
|
1d0a8051d5 | ||
|
|
9f04da3afc | ||
|
|
134f24f199 | ||
|
|
6655733d5e | ||
|
|
f9f297bf90 | ||
|
|
ec5b2bdc4e | ||
|
|
a2aeb4ecb1 | ||
|
|
3feaa2aa3f | ||
|
|
8ea7fef827 | ||
|
|
36a94857a1 | ||
|
|
28ce98bf16 | ||
|
|
0be62ccad7 | ||
|
|
86ba33845f | ||
|
|
6f507fb210 | ||
|
|
1a7b4ae236 | ||
|
|
a9dd554e13 | ||
|
|
3724818db5 | ||
|
|
e9546cb564 | ||
|
|
8de8d90287 | ||
|
|
e5f536a6cb | ||
|
|
b6cc7e681b | ||
|
|
f2323d876d | ||
|
|
10af5bb1e4 | ||
|
|
237eb667d3 | ||
|
|
d6f18babe9 | ||
|
|
1747c8e538 | ||
|
|
f1cb66d09e | ||
|
|
4eedb86b4f | ||
|
|
e917cf5b37 | ||
|
|
c506238ce2 | ||
|
|
c35710e388 | ||
|
|
fb321d62cb | ||
|
|
f7d7072841 | ||
|
|
1c1005952a | ||
|
|
41856f6594 | ||
|
|
0beae0bca6 | ||
|
|
9469412db2 | ||
|
|
a345d9f8cf | ||
|
|
a82477069b | ||
|
|
81dd215de9 | ||
|
|
4b5f8d89f2 | ||
|
|
dc352b839b | ||
|
|
e838e2beca | ||
|
|
6053a43841 | ||
|
|
faaf82acd7 | ||
|
|
8fee63f4f0 | ||
|
|
7519b1c75c | ||
|
|
dbf3bb2e64 | ||
|
|
86bdfbd0da | ||
|
|
796dbb77b4 | ||
|
|
96a47b79d5 | ||
|
|
9c46ab821a | ||
|
|
12595032e7 | ||
|
|
a3d9552015 | ||
|
|
01220edb67 | ||
|
|
02b07b686a | ||
|
|
a70b006929 | ||
|
|
4cca3fac20 | ||
|
|
995ff61f3c | ||
|
|
f5fc88c599 | ||
|
|
428950b96c | ||
|
|
0553ba6c3f | ||
|
|
fd19f9d6bf | ||
|
|
b43b0bb518 | ||
|
|
d2411af9fd | ||
|
|
49e35e8bb7 | ||
|
|
c25724a222 | ||
|
|
d689e6080d | ||
|
|
cf4f8befe7 | ||
|
|
1cace5c864 | ||
|
|
7e9b0dcc25 | ||
|
|
0b48e23fa5 | ||
|
|
f6e4cd6899 | ||
|
|
c0f944e127 | ||
|
|
6aba354c16 | ||
|
|
71abb6c163 | ||
|
|
cf45533cf6 | ||
|
|
95994c45c5 | ||
|
|
ea081e3312 | ||
|
|
8cfe988383 | ||
|
|
e5c70bbaa0 | ||
|
|
c70ae83d4c | ||
|
|
f232cbfed6 | ||
|
|
08224f228f | ||
|
|
c8ead78419 | ||
|
|
55e1a53cfd | ||
|
|
8da42765dd | ||
|
|
75c9f2d81e | ||
|
|
2e61fef4d1 | ||
|
|
fb141be3e0 | ||
|
|
9051f31306 | ||
|
|
17bc654b6d | ||
|
|
084e654bcb | ||
|
|
7d2307deb0 | ||
|
|
226d73b45b | ||
|
|
52543768c7 | ||
|
|
3826885f4b | ||
|
|
e7b3f15240 | ||
|
|
7ddc17a38f | ||
|
|
d62ff4a608 | ||
|
|
488dd8459c | ||
|
|
b2d2cf22eb | ||
|
|
7b3e2eaa0b | ||
|
|
f5f205bee0 | ||
|
|
0edd96ccf6 | ||
|
|
eb6c0a23f0 | ||
|
|
c523890228 | ||
|
|
5c523d55f7 | ||
|
|
45dc5e3400 | ||
|
|
c42f4f2969 | ||
|
|
5c8dd32252 | ||
|
|
5964d113f8 | ||
|
|
fea2d02aaa | ||
|
|
81a7260194 | ||
|
|
3b1ab2848d | ||
|
|
80a70b86de | ||
|
|
9222b6a1d3 | ||
|
|
67a48427b8 | ||
|
|
d64ae1b0b1 | ||
|
|
27c0cabd81 | ||
|
|
2f5f7c1135 | ||
|
|
3bf804c9c8 | ||
|
|
0af0f9f125 | ||
|
|
ea67f6999a | ||
|
|
eb818c50ed | ||
|
|
092a62daaa | ||
|
|
1ed28f8863 | ||
|
|
38c7f7017d | ||
|
|
c924d3c9d7 | ||
|
|
6f8800aefe | ||
|
|
814716336a | ||
|
|
0530a1f7c2 | ||
|
|
f6db09bae2 | ||
|
|
217ddda0eb | ||
|
|
734ada7e1b | ||
|
|
a14e85d8e6 | ||
|
|
996b8a4044 | ||
|
|
164dd3bbef | ||
|
|
6577b69d5d | ||
|
|
dca4217501 | ||
|
|
cf4bef2f78 | ||
|
|
84b1e9815e | ||
|
|
c266cfb3cb | ||
|
|
a8301a2709 | ||
|
|
669023fc08 | ||
|
|
2f3f363c33 | ||
|
|
b440640929 | ||
|
|
155995b65c | ||
|
|
e890e30766 | ||
|
|
7b04360f93 | ||
|
|
31785cce47 | ||
|
|
e22a3c9ba9 | ||
|
|
b7196c23de | ||
|
|
fcb0c2d0ec | ||
|
|
e17efa53ce | ||
|
|
3f81245864 | ||
|
|
9471faa5f0 | ||
|
|
3b51719abc | ||
|
|
2ba95cf61f | ||
|
|
437a60676f | ||
|
|
f1b6a2144c | ||
|
|
c62d980112 | ||
|
|
9ee8b0cf79 | ||
|
|
051db74cdf | ||
|
|
49c157fae3 | ||
|
|
0c467e9d45 | ||
|
|
76ac5842ff | ||
|
|
5de8d26949 | ||
|
|
20fed551ba | ||
|
|
e4b31d6f69 | ||
|
|
b96f174775 | ||
|
|
f0010a607f | ||
|
|
ed0ed38e2d | ||
|
|
c4aec611b2 | ||
|
|
3ae6b3041b | ||
|
|
ebaeda85f1 | ||
|
|
815256d89b | ||
|
|
b14862351b | ||
|
|
f8f4e8a7df | ||
|
|
acf63bef73 | ||
|
|
098fd545b3 | ||
|
|
a7cd2de97a | ||
|
|
be58604387 | ||
|
|
5701fc65ce | ||
|
|
0351ecb11d | ||
|
|
d9042a16bd | ||
|
|
642f5120a7 | ||
|
|
98a139c3c7 | ||
|
|
828a2559a9 | ||
|
|
bf8285b8c9 | ||
|
|
a71fd34758 | ||
|
|
7d55581527 | ||
|
|
034f2fdebe | ||
|
|
c5b13accfd | ||
|
|
4791d6b676 | ||
|
|
8bf178febf | ||
|
|
1fa22cd29a | ||
|
|
e2858b9855 | ||
|
|
e6dbbc312a | ||
|
|
425bca7199 | ||
|
|
1298a3971c | ||
|
|
c40a03e6dc | ||
|
|
eebb870922 | ||
|
|
dd8201d20d | ||
|
|
a4c503d7ce | ||
|
|
23b3dfc2ca | ||
|
|
8be3f916f2 | ||
|
|
424b364a88 | ||
|
|
0788a91c64 | ||
|
|
ad98bf1d17 | ||
|
|
c67180edf1 | ||
|
|
49e82263ad | ||
|
|
736d7a840c | ||
|
|
a539e12c97 | ||
|
|
2a22daa4f0 | ||
|
|
27a5707de5 | ||
|
|
72d0bf667e | ||
|
|
e7ded666be | ||
|
|
3ae2123bce | ||
|
|
6d4874a491 | ||
|
|
b1ebc3d7ca | ||
|
|
5e52cf569f | ||
|
|
ff9ca03f3c | ||
|
|
5278ba0043 | ||
|
|
1ef68f83e7 | ||
|
|
aec327ee51 | ||
|
|
9fe2ad3f55 | ||
|
|
ba6966d83f | ||
|
|
4c953b424d | ||
|
|
6f83fe2c42 | ||
|
|
2ccf57ec46 | ||
|
|
89799596a6 | ||
|
|
48ec4a59ac | ||
|
|
e0d79b02f9 | ||
|
|
d45cd9d3cf | ||
|
|
5bc7f8cab3 | ||
|
|
b7d5ea2f82 | ||
|
|
d232721db6 | ||
|
|
5f49ec4599 | ||
|
|
54f81315bf | ||
|
|
6878105560 | ||
|
|
7f97c35aca | ||
|
|
651a56806a | ||
|
|
d276adf277 | ||
|
|
d8f58af1b5 | ||
|
|
dcba249d01 | ||
|
|
5f426c38cb | ||
|
|
128223891e | ||
|
|
17c9b92bc9 | ||
|
|
d1dec64362 | ||
|
|
2355265fdc | ||
|
|
1c2388876c | ||
|
|
e8cb1cba7f | ||
|
|
da70196350 | ||
|
|
88fcf2b443 | ||
|
|
12a3c0739e | ||
|
|
7df82763e3 | ||
|
|
e5b4021c6f | ||
|
|
d6a6c1c797 | ||
|
|
a5f54c9796 | ||
|
|
e38025769d | ||
|
|
3d8d1079bc | ||
|
|
6c8247af60 | ||
|
|
2713121725 | ||
|
|
c11e66aa8b | ||
|
|
9fcc101844 | ||
|
|
a7c50df002 | ||
|
|
a4c9336416 | ||
|
|
2189884a59 | ||
|
|
f7c645c065 | ||
|
|
4455c8bd1d | ||
|
|
d9220bd1a5 | ||
|
|
d251b2c0db | ||
|
|
5a52007005 | ||
|
|
6f703a6dbe | ||
|
|
befdd9d3e1 | ||
|
|
2d718defc0 | ||
|
|
9f78a77e01 | ||
|
|
01389205e9 | ||
|
|
a63b2d4dae | ||
|
|
8e13982710 | ||
|
|
ff5577ad70 | ||
|
|
d17c32d6a3 | ||
|
|
1c20a59675 | ||
|
|
cb725bb209 | ||
|
|
770292e41c | ||
|
|
090121c31b | ||
|
|
6b5ad4f27d | ||
|
|
c3e9a998ad | ||
|
|
a0dbad8078 | ||
|
|
453cc0d56f | ||
|
|
b73bd26a5c | ||
|
|
bbd0a2c673 | ||
|
|
a7eb99bba9 | ||
|
|
431803abb9 | ||
|
|
adf7ca9729 | ||
|
|
0b4e603cfd | ||
|
|
2ec3d2e381 | ||
|
|
a775b04695 | ||
|
|
6f99190f82 | ||
|
|
4292678b8f | ||
|
|
68a8cad3ed | ||
|
|
c23936be78 | ||
|
|
7e9531b3da | ||
|
|
512e298e6e |
No files matched your search
@@ -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.2 · engine 0.1.47"
|
||||
placeholder: "Strata 0.2.5 · engine 0.1.50"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
||||
@@ -13,7 +13,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: 'Release tag whose engine packages go into the images (for example v0.2.2)'
|
||||
description: 'Release tag whose engine packages go into the images (for example v0.2.3)'
|
||||
required: true
|
||||
type: string
|
||||
flavors:
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Metal build
|
||||
|
||||
# Compile and link the Metal engine on a macOS runner (no GPU needed: the kernels are compiled at run time). The shared host files
|
||||
# (src/core/verify.cpp, src/prefill, src/program/generate.cpp) call kernel entry points that exist only for CUDA / HIP until Metal gets a
|
||||
# stub or an implementation; a missing one is a link error here, not on a user's Mac (owner round 9, G6).
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/**'
|
||||
- 'include/**'
|
||||
- 'metal/**'
|
||||
- 'third_party/**'
|
||||
- 'CMakeLists.txt'
|
||||
- '.github/workflows/metal-build.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
metal-link:
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
# setup.py's LLAMA_CPP_COMMIT and metal/setup_mac.py's metal-cpp pin
|
||||
LLAMA_CPP_COMMIT: 3cf03257f219afbe7334045ff7c6a06ac68c627d
|
||||
METAL_CPP_URL: https://developer.apple.com/metal/cpp/files/metal-cpp_26.zip
|
||||
METAL_CPP_SHA256: 4df3c078b9aadcb516212e9cb03004cbc5ce9a3e9c068fa3144d021db585a3a4
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Tools
|
||||
run: brew install ninja
|
||||
- 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: |
|
||||
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 "$LLAMA_CPP_COMMIT"
|
||||
git checkout -q FETCH_HEAD
|
||||
- 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"
|
||||
- name: Build and link the engine
|
||||
run: cmake --build build-metal --target strata
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Strata runs the Qwen3.8-Flash-Next mixture-of-experts model (and its Coder, Swift 1.5, Unsloth, Gyro and NVFP4
|
||||
variants, and Qwen3.6-35B-A3B) on a normal PC: one NVIDIA or AMD graphics card plus system RAM on Windows or Linux,
|
||||
or an Apple Silicon Mac. Version 0.2.2, engine 0.1.47. It has three parts:
|
||||
or an Apple Silicon Mac. Version 0.2.5, engine 0.1.50. It has three parts:
|
||||
|
||||
- **The app** (`apps/desktop`, the Tauri shell; `apps/host`, the Node host and the web UI). It is for people who do
|
||||
not want a terminal. The installer has the engine inside.
|
||||
@@ -19,7 +19,7 @@ Follow **[docs/AI_SETUP.md](docs/AI_SETUP.md)**. For an agent that has a shell,
|
||||
1. Install the Terminal. It needs no administrator rights.
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh # Linux, Mac
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh # Linux, Mac
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex # Windows PowerShell
|
||||
```
|
||||
|
||||
|
||||
+174
-5
@@ -1,4 +1,4 @@
|
||||
# Strata Engine - top-level build (P1.S1).
|
||||
# Strata Engine - top-level build (P1.S1).
|
||||
#
|
||||
# Targets (phase-1 §P1.S1): `strata_artifact` (no CUDA dependency), `tests`. `strata_plan` and the C++
|
||||
# `strata-pack` are added when their sources exist - a target with no sources would make the build graph
|
||||
@@ -8,7 +8,7 @@
|
||||
# below refuses anything older: silently building for a pre-Ampere arch would produce a binary that runs
|
||||
# and is wrong about its own performance, which is the worst of both outcomes.
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
project(strata VERSION 0.1.47 LANGUAGES CXX)
|
||||
project(strata VERSION 0.1.50 LANGUAGES CXX)
|
||||
# EXPERIMENTAL: the Intel Arc engine, the SYCL port in sycl/ (PR #423; icpx + oneMKL, docs/INTEL_ARC.md). It is its
|
||||
# own project that takes the unported sources from src/; with this on, nothing below is configured.
|
||||
option(STRATA_ENABLE_SYCL "EXPERIMENTAL: build the Intel Arc (SYCL) engine from sycl/ - docs/INTEL_ARC.md" OFF)
|
||||
@@ -336,6 +336,8 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
add_library(strata_core STATIC src/core/device.cu src/core/pinned.cu src/platform/memory.cpp src/core/graph.cpp src/core/weights.cpp
|
||||
src/core/layout.cpp)
|
||||
target_include_directories(strata_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
# the DFlash2 drafter (src/core/dflash2.cpp, src/kernels/cuda/dflash2.cu) exists in the CUDA and HIP builds; the Metal and SYCL builds compile without it
|
||||
target_compile_definitions(strata_core PUBLIC STRATA_HAVE_DFLASH2=1)
|
||||
if(STRATA_ENABLE_CUDA AND STRATA_EXPERIMENTAL_SM60)
|
||||
# the runtime device check (src/core/device.cu) admits the cards this build was made for (#236)
|
||||
target_compile_definitions(strata_core PRIVATE STRATA_EXPERIMENTAL_SM60=1)
|
||||
@@ -418,6 +420,7 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
src/kernels/cuda/dequant_bf16.cu
|
||||
src/kernels/cuda/qsa_select.cu
|
||||
src/kernels/cuda/verify_kernels.cu
|
||||
src/kernels/cuda/dflash2.cu
|
||||
src/kernels/cuda/iq_kernels.cu
|
||||
src/kernels/ngram.cpp
|
||||
src/ngram/ple_reader.cpp
|
||||
@@ -579,7 +582,7 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
# about arithmetic, and this one knows the ORDER the arithmetic happens in.
|
||||
add_library(strata_engine STATIC src/core/layer.cpp src/core/session.cpp src/core/expert_source.cpp src/core/remote_experts.cpp
|
||||
src/core/remote_expert_opt.cu
|
||||
src/core/expert_cache.cpp src/core/native_head.cpp src/core/native_dense.cpp src/core/verify.cpp src/core/mtp.cpp
|
||||
src/core/expert_cache.cpp src/core/native_head.cpp src/core/native_dense.cpp src/core/verify.cpp src/core/mtp.cpp src/core/dflash2.cpp
|
||||
src/core/conversation_snapshot.cpp src/core/conversation_state.cpp src/core/conversation_memory.cpp src/core/peer_experts.cpp
|
||||
src/core/arch_defaults.cpp src/core/nv_defaults.cpp src/core/vmm.cpp src/core/conversation_file.cpp)
|
||||
target_include_directories(strata_engine PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
@@ -732,6 +735,27 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(tune_rocblas PRIVATE hip::host roc::rocblas)
|
||||
endif()
|
||||
|
||||
# DFlash2 drafter tools (not built by default: cmake --build <dir> --target dflash2_parity dflash2_gemm_bench dflash2_attn_bench): the drafter against
|
||||
# the torch port on a synthetic case (needs the pack and a case file, tools/dflash2/mk_parity_case.py), and the timing of its Q8 GEMM and attention
|
||||
add_executable(dflash2_parity EXCLUDE_FROM_ALL src/core/dflash2_parity.cpp)
|
||||
target_include_directories(dflash2_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(dflash2_parity PRIVATE strata_engine strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_executable(dflash2_gemm_bench EXCLUDE_FROM_ALL src/core/dflash2_gemm_bench.cpp)
|
||||
target_include_directories(dflash2_gemm_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(dflash2_gemm_bench PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_executable(dflash2_launch_bench EXCLUDE_FROM_ALL src/core/dflash2_launch_bench.cu)
|
||||
target_include_directories(dflash2_launch_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(dflash2_launch_bench PRIVATE ${_strata_gpu_runtime_target})
|
||||
add_executable(dflash2_bw_bench EXCLUDE_FROM_ALL src/core/dflash2_bw_bench.cu)
|
||||
target_include_directories(dflash2_bw_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(dflash2_bw_bench PRIVATE ${_strata_gpu_runtime_target})
|
||||
add_executable(dflash2_small_bench EXCLUDE_FROM_ALL src/core/dflash2_small_bench.cpp)
|
||||
target_include_directories(dflash2_small_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(dflash2_small_bench PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_executable(dflash2_attn_bench EXCLUDE_FROM_ALL src/core/dflash2_attn_bench.cpp)
|
||||
target_include_directories(dflash2_attn_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(dflash2_attn_bench PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
|
||||
add_executable(strata src/program/generate.cpp)
|
||||
strata_utf8_manifest(strata)
|
||||
target_include_directories(strata PRIVATE ${_strata_gpu_include_directories})
|
||||
@@ -770,6 +794,114 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_compile_definitions(strata PRIVATE STRATA_HAVE_QWEN35=1)
|
||||
endif()
|
||||
|
||||
# DeepSeek-V4.1-Flash: the third engine path (src/dsv41, from Project Maya's maya-ds4, MIT, third_party/project-maya).
|
||||
# Its own executable for now (strata-dsv41); a family switch in `strata` comes after first light. OFF by default: it
|
||||
# changes no existing target (the vendored kernels live in strata::dsv41k, so no symbol is shared with strata_kernels).
|
||||
option(STRATA_BUILD_DSV41 "Build the DeepSeek-V4.1 engine path (strata-dsv41)" OFF)
|
||||
if(STRATA_BUILD_DSV41 AND NOT STRATA_HIP_GFX906)
|
||||
add_library(strata_dsv41_kernels STATIC src/dsv41/vendor/exl3.cu src/dsv41/vendor/sampler.cu src/dsv41/vendor/moe_win.cu
|
||||
src/dsv41/vendor/exl3_cpu.cpp)
|
||||
if(NOT STRATA_ENABLE_HIP) # (the pre-sm_80 NVIDIA decode kernels exl3.cu dispatches to)
|
||||
target_sources(strata_dsv41_kernels PRIVATE src/dsv41/vendor/exl3_legacy.cu)
|
||||
endif()
|
||||
target_include_directories(strata_dsv41_kernels PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ${_strata_gpu_include_directories})
|
||||
target_link_libraries(strata_dsv41_kernels PUBLIC ${_strata_gpu_runtime_target})
|
||||
add_executable(strata-dsv41 src/dsv41/ds4_main.cpp src/dsv41/ds4_model.cu src/dsv41/ds4_kernels.cu src/dsv41/ds4_vision.cu
|
||||
src/dsv41/ds4_gguf.cu src/dsv41/ds4_gguf_cpu.cpp src/dsv41/ds4_iq.cu src/dsv41/ds4_iq_cpu.cpp
|
||||
src/dsv41/ds4_tq.cu src/dsv41/ds4_tq_cpu.cpp src/dsv41/ds4_gg_cpu.cpp src/dsv41/ds4_packinfo.cpp)
|
||||
# the commit the binary was built from, readable with --version (set -DSTRATA_BUILD_ID=... for a tree without .git)
|
||||
if(NOT STRATA_BUILD_ID AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/BUILD_ID) # (a git archive has no .git: the packer writes the commit here)
|
||||
file(STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/BUILD_ID STRATA_BUILD_ID LIMIT_COUNT 1)
|
||||
endif()
|
||||
if(NOT STRATA_BUILD_ID)
|
||||
execute_process(COMMAND git -C ${CMAKE_CURRENT_SOURCE_DIR} rev-parse --short=9 HEAD OUTPUT_VARIABLE STRATA_BUILD_ID
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
|
||||
endif()
|
||||
if(NOT STRATA_BUILD_ID)
|
||||
set(STRATA_BUILD_ID unknown)
|
||||
endif()
|
||||
set_source_files_properties(src/dsv41/ds4_main.cpp PROPERTIES COMPILE_DEFINITIONS "DS4_BUILD_ID=\"${STRATA_BUILD_ID}\"")
|
||||
target_include_directories(strata-dsv41 PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${_strata_gpu_include_directories}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third_party/ggml)
|
||||
target_link_libraries(strata-dsv41 PRIVATE strata_dsv41_kernels ${_strata_gpu_runtime_target} ${_strata_gpu_blas_target})
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(strata-dsv41 PRIVATE Threads::Threads)
|
||||
if(WIN32 AND STRATA_ENABLE_HIP) # cudaMemGetInfo is the DXGI-budgeted hip_compat::mem_get_info (src/core/device.cu): on a Strix Halo the
|
||||
# carve-out is not all the GPU may use, and the engine sizes its expert tiers by what the budget says is free
|
||||
target_link_libraries(strata-dsv41 PRIVATE strata_core)
|
||||
endif()
|
||||
if(STRATA_NATIVE_EXPERTS) # Strata's CPU trellis kernels (tq_cpu.hpp) for the trellis experts on the CPU lane
|
||||
target_link_libraries(strata-dsv41 PRIVATE strata_kernels_cpu)
|
||||
target_compile_definitions(strata-dsv41 PRIVATE STRATA_DSV41_TQ_CPU=1)
|
||||
endif()
|
||||
if(STRATA_NATIVE_EXPERTS)
|
||||
# the CPU lane's ggml-type rows: ggml-cpu's dots against Strata's AVX2 i-quant rows (bits and GB/s); no GPU, no model
|
||||
add_executable(ds4_gg_bench tests/dsv41/gg_dot_bench.cpp src/dsv41/ds4_gg_cpu.cpp)
|
||||
target_include_directories(ds4_gg_bench PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/src/dsv41
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third_party/ggml)
|
||||
target_link_libraries(ds4_gg_bench PRIVATE strata_kernels_cpu Threads::Threads)
|
||||
target_compile_definitions(ds4_gg_bench PRIVATE STRATA_DSV41_TQ_CPU=1)
|
||||
endif()
|
||||
if(STRATA_NATIVE_EXPERTS)
|
||||
# the trellis experts (device kernels ds4_tq.cu, CPU lane ds4_tq_cpu.cpp) against a double reference; needs a GPU, no model
|
||||
# both test .cpp files launch kernels, so they compile as HIP on the AMD backend and as CUDA on the NVIDIA one
|
||||
if(STRATA_ENABLE_HIP)
|
||||
set(_ds4_test_lang HIP)
|
||||
elseif(STRATA_ENABLE_CUDA)
|
||||
set(_ds4_test_lang CUDA)
|
||||
else()
|
||||
set(_ds4_test_lang CXX)
|
||||
endif()
|
||||
set_source_files_properties(tests/dsv41/tq_expert_test.cpp PROPERTIES LANGUAGE ${_ds4_test_lang})
|
||||
add_executable(ds4_tq_test tests/dsv41/tq_expert_test.cpp src/dsv41/ds4_tq.cu src/dsv41/ds4_iq.cu src/dsv41/ds4_tq_cpu.cpp
|
||||
src/dsv41/ds4_iq_cpu.cpp src/dsv41/ds4_gg_cpu.cpp tools/trellis/tqenc.cpp)
|
||||
target_include_directories(ds4_tq_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${_strata_gpu_include_directories}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third_party/ggml ${CMAKE_CURRENT_SOURCE_DIR}/src/kernels/cpu)
|
||||
target_link_libraries(ds4_tq_test PRIVATE strata_kernels_cpu ${_strata_gpu_runtime_target} Threads::Threads)
|
||||
target_compile_definitions(ds4_tq_test PRIVATE STRATA_DSV41_TQ_CPU=1)
|
||||
# the same with the plain-FMA tile instead of the gfx12 WMMA one (what CUDA and gfx11 builds run)
|
||||
add_executable(ds4_tq_test_portable tests/dsv41/tq_expert_test.cpp src/dsv41/ds4_tq.cu src/dsv41/ds4_iq.cu src/dsv41/ds4_tq_cpu.cpp
|
||||
src/dsv41/ds4_iq_cpu.cpp src/dsv41/ds4_gg_cpu.cpp tools/trellis/tqenc.cpp)
|
||||
target_include_directories(ds4_tq_test_portable PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${_strata_gpu_include_directories}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third_party/ggml ${CMAKE_CURRENT_SOURCE_DIR}/src/kernels/cpu)
|
||||
target_link_libraries(ds4_tq_test_portable PRIVATE strata_kernels_cpu ${_strata_gpu_runtime_target} Threads::Threads)
|
||||
target_compile_definitions(ds4_tq_test_portable PRIVATE STRATA_DSV41_TQ_CPU=1 DS4_TQ_PORTABLE=1)
|
||||
if(STRATA_ENABLE_HIP)
|
||||
target_compile_options(ds4_tq_test_portable PRIVATE "$<$<COMPILE_LANGUAGE:HIP,CXX>:SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/include/strata/dsv41/hip_extra.h>")
|
||||
endif()
|
||||
if(STRATA_ENABLE_HIP)
|
||||
target_compile_options(ds4_tq_test PRIVATE "$<$<COMPILE_LANGUAGE:HIP,CXX>:SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/include/strata/dsv41/hip_extra.h>")
|
||||
endif()
|
||||
# the i-quant experts (ds4_iq.cu) against ggml's own decode of the same blocks + a double reference, device and CPU
|
||||
# lane, for every type the device takes: the gate a new type (IQ2_XS ..) has to pass before the engine uses it
|
||||
set_source_files_properties(tests/dsv41/iq_expert_test.cpp PROPERTIES LANGUAGE ${_ds4_test_lang})
|
||||
add_executable(ds4_iq_test tests/dsv41/iq_expert_test.cpp src/dsv41/ds4_iq.cu src/dsv41/ds4_iq_cpu.cpp
|
||||
src/dsv41/ds4_gg_cpu.cpp src/dsv41/ds4_tq.cu src/dsv41/ds4_tq_cpu.cpp)
|
||||
target_include_directories(ds4_iq_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${_strata_gpu_include_directories}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third_party/ggml ${CMAKE_CURRENT_SOURCE_DIR}/src/kernels/cpu)
|
||||
target_link_libraries(ds4_iq_test PRIVATE strata_kernels_cpu ${_strata_gpu_runtime_target} Threads::Threads)
|
||||
target_compile_definitions(ds4_iq_test PRIVATE STRATA_DSV41_TQ_CPU=1)
|
||||
if(STRATA_ENABLE_HIP)
|
||||
target_compile_options(ds4_iq_test PRIVATE "$<$<COMPILE_LANGUAGE:HIP,CXX>:SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/include/strata/dsv41/hip_extra.h>")
|
||||
endif()
|
||||
# the kept conversations' .meta file and prefix matching (ds4_slotmeta.hpp): no GPU, no model
|
||||
add_executable(ds4_slot_test tests/dsv41/slot_test.cpp)
|
||||
target_include_directories(ds4_slot_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/dsv41)
|
||||
target_compile_features(ds4_slot_test PRIVATE cxx_std_17)
|
||||
add_executable(ds4_experts_rule_test tests/dsv41/experts_rule_test.cpp)
|
||||
target_include_directories(ds4_experts_rule_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/dsv41)
|
||||
target_compile_features(ds4_experts_rule_test PRIVATE cxx_std_17)
|
||||
add_executable(ds4_pick_rule_test tests/dsv41/pick_rule_test.cpp)
|
||||
target_include_directories(ds4_pick_rule_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/dsv41)
|
||||
target_compile_features(ds4_pick_rule_test PRIVATE cxx_std_17)
|
||||
endif()
|
||||
if(STRATA_ENABLE_HIP)
|
||||
foreach(_t strata_dsv41_kernels strata-dsv41)
|
||||
target_compile_options(${_t} PRIVATE "$<$<COMPILE_LANGUAGE:HIP,CXX>:SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/include/strata/dsv41/hip_extra.h>")
|
||||
endforeach()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(STRATA_ENABLE_HIP)
|
||||
enable_testing()
|
||||
set(_strata_hip_tests)
|
||||
@@ -1081,7 +1213,6 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(verify_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME verify_parity COMMAND verify_parity --selftest)
|
||||
endif()
|
||||
|
||||
add_executable(fused_gr_bench src/kernels/fused_gr_bench.cpp)
|
||||
target_include_directories(fused_gr_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(fused_gr_bench PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
@@ -1413,6 +1544,9 @@ if(STRATA_BUILD_TESTS)
|
||||
add_executable(arch_defaults_test src/core/arch_defaults_test.cpp src/core/arch_defaults.cpp)
|
||||
target_include_directories(arch_defaults_test PRIVATE include)
|
||||
add_test(NAME arch_defaults_test COMMAND arch_defaults_test)
|
||||
add_executable(adapt_async_default_test src/core/adapt_async_default_test.cpp)
|
||||
target_include_directories(adapt_async_default_test PRIVATE include)
|
||||
add_test(NAME adapt_async_default_test COMMAND adapt_async_default_test)
|
||||
add_executable(uma_cache_test src/core/uma_cache_test.cpp)
|
||||
target_include_directories(uma_cache_test PRIVATE include)
|
||||
add_test(NAME uma_cache_test COMMAND uma_cache_test)
|
||||
@@ -1453,6 +1587,10 @@ if(STRATA_BUILD_TESTS OR STRATA_BUILD_CONVERSATION_TESTS)
|
||||
add_executable(spec_prob_test src/core/spec_prob_test.cpp)
|
||||
target_include_directories(spec_prob_test PRIVATE include)
|
||||
add_test(NAME spec_prob_test COMMAND spec_prob_test)
|
||||
# the per-card window cap of --draft dflash2 (header-only, CPU)
|
||||
add_executable(draft_cap_test src/core/draft_cap_test.cpp)
|
||||
target_include_directories(draft_cap_test PRIVATE include)
|
||||
add_test(NAME draft_cap_test COMMAND draft_cap_test)
|
||||
# the f32 -> bf16 rounding against ggml's, NaN included, over every f32 bit pattern (header-only, CPU)
|
||||
add_executable(bf16_bits_test src/kernels/bf16_bits_test.cpp)
|
||||
target_include_directories(bf16_bits_test PRIVATE include)
|
||||
@@ -1538,6 +1676,13 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
foreach(_f GGML_AVX GGML_AVX2 GGML_FMA GGML_F16C GGML_BMI2)
|
||||
set(${_f} ON CACHE BOOL "" FORCE)
|
||||
endforeach()
|
||||
# The DeepSeek engine (STRATA_BUILD_DSV41) builds ggml-cpu with BMI2 OFF: ggml's IQ1_S / IQ1_M dots use _pdep_u64 with it, and AMD
|
||||
# Zen 1/2 run PDEP in microcode (EPYC 7452, IQ1_M one-token dot 1.8 GB/s against 32 GB/s without it; IQ1_S 2.3 against 37). The
|
||||
# results are bit-identical either way (ds4_gg_bench --dump, types 19 and 29, 1-16 tokens). Other engines keep BMI2 until their
|
||||
# next rebuild is looked at: no Qwen package changes with this commit.
|
||||
if(STRATA_BUILD_DSV41)
|
||||
set(GGML_BMI2 OFF CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
elseif(STRATA_ISA_FLOOR)
|
||||
# the experimental older-CPU build (STRATA_ISA_FLOOR above): one ggml-cpu for the floor, ggml's "sandybridge"
|
||||
# feature set (SSE4.2 + AVX) or SSE4.2 alone - not the build host's, which may be newer than the PC it runs on.
|
||||
@@ -1596,7 +1741,7 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
set_source_files_properties(src/kernels/cpu/iq_avx1.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX")
|
||||
else()
|
||||
set_source_files_properties(src/kernels/cpu/iq_avx512.cpp src/kernels/cpu/nvfp4_avx512.cpp PROPERTIES COMPILE_OPTIONS
|
||||
"-mavx512f;-mavx512bw;-mavx512vl;-mavx512dq;-mavx512vnni;-mavx512vbmi;-mf16c;-mfma")
|
||||
"-mavx512f;-mavx512bw;-mavx512vl;-mavx512dq;-mavx512vnni;-mavx512vbmi;-mavx512vpopcntdq;-mbmi2;-mf16c;-mfma")
|
||||
set_source_files_properties(src/kernels/cpu/iq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
|
||||
set_source_files_properties(src/kernels/cpu/kq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
|
||||
set_source_files_properties(src/kernels/cpu/nvfp4_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
|
||||
@@ -1644,6 +1789,14 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
# against ggml-cpu's dot, on random blocks; --bench: ms per expert on one thread, the weights streamed from DRAM
|
||||
add_executable(iq_avx2_parity src/kernels/cpu/iq_avx2_parity.cpp)
|
||||
target_link_libraries(iq_avx2_parity PRIVATE strata_kernels_cpu)
|
||||
# the 512-bit i-quant rows against ggml-cpu's dot: the exact rows bit for bit at 1-4 tokens, the engine's multi-token rows to float
|
||||
# rounding, with the grid lookups as scalar loads and as gathers (SKIP without AVX-512)
|
||||
add_executable(iq512_gather_parity src/kernels/cpu/iq512_gather_parity.cpp)
|
||||
target_link_libraries(iq512_gather_parity PRIVATE strata_kernels_cpu)
|
||||
if(STRATA_BUILD_TESTS)
|
||||
add_test(NAME iq512_gather_parity_scalar COMMAND ${CMAKE_COMMAND} -E env STRATA_IQ_GATHER=0 $<TARGET_FILE:iq512_gather_parity>)
|
||||
add_test(NAME iq512_gather_parity_gather COMMAND ${CMAKE_COMMAND} -E env STRATA_IQ_GATHER=1 $<TARGET_FILE:iq512_gather_parity>)
|
||||
endif()
|
||||
# MODEL-UPDATE: the pool's expert rows per format on T threads against the RAM bandwidth (a benchmark, no test)
|
||||
add_executable(kq_cpu_bench src/kernels/cpu/kq_cpu_bench.cpp)
|
||||
target_link_libraries(kq_cpu_bench PRIVATE strata_kernels_cpu)
|
||||
@@ -2238,3 +2391,19 @@ if(STRATA_HIP_GFX906)
|
||||
add_executable(gfx906_qsa_query_swizzle_probe bench/gfx906_qsa_query_swizzle_probe.cpp)
|
||||
target_link_libraries(gfx906_qsa_query_swizzle_probe PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
endif()
|
||||
|
||||
# ---- DeepSeek-V4.1 on a box with no GPU toolkit ------------------------------------------------
|
||||
# The whole DSV41 block above lives inside if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP), so with neither backend on
|
||||
# -DSTRATA_BUILD_DSV41=ON was silently unused and the host-side benchmark could not be built at all (found on p1004,
|
||||
# 2026-10-23: the CPU lane is exactly what a box without a usable GPU needs to measure). This branch builds the one target
|
||||
# that has no device code, so the CPU-lane A/B runs anywhere.
|
||||
if(STRATA_BUILD_DSV41 AND NOT STRATA_ENABLE_CUDA AND NOT STRATA_ENABLE_HIP AND NOT STRATA_HIP_GFX906)
|
||||
message(STATUS "STRATA_BUILD_DSV41: no GPU backend, building the host-side benchmark only (ds4_gg_bench)")
|
||||
if(STRATA_NATIVE_EXPERTS)
|
||||
add_executable(ds4_gg_bench tests/dsv41/gg_dot_bench.cpp src/dsv41/ds4_gg_cpu.cpp)
|
||||
target_include_directories(ds4_gg_bench PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/src/dsv41
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third_party/ggml)
|
||||
target_link_libraries(ds4_gg_bench PRIVATE strata_kernels_cpu Threads::Threads)
|
||||
target_compile_definitions(ds4_gg_bench PRIVATE STRATA_DSV41_TQ_CPU=1)
|
||||
endif()
|
||||
endif()
|
||||
+2
-1
@@ -56,7 +56,8 @@ RUN set -e; \
|
||||
ver="${ENGINE_VERSION:-$(sed -n 's/^[[:space:]]*project([^)]*VERSION[[:space:]]*\([0-9][0-9.]*\).*/\1/p' CMakeLists.txt | head -1)}"; \
|
||||
cudafull=$(nvcc --version | sed -n 's/.*release \([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' | head -1); cudamajor=${cudafull%%.*}; \
|
||||
if [ "$cudamajor" = 13 ]; then mindriver=580.65; else mindriver=575.51; fi; \
|
||||
echo "{\"version\": \"$ver\", \"source\": \"docker build\", \"archs\": \"$CUDA_ARCHITECTURES\", \"cuda\": \"$cudafull\", \"portable\": true}" > /stage/BUILD.json; \
|
||||
vision_archs=""; [ -f /stage/strata-vision ] && vision_archs=", \"vision_archs\": \"$CUDA_ARCHITECTURES\""; \
|
||||
echo "{\"version\": \"$ver\", \"source\": \"docker build\", \"archs\": \"$CUDA_ARCHITECTURES\", \"cuda\": \"$cudafull\", \"portable\": true${vision_archs}}" > /stage/BUILD.json; \
|
||||
python3 packaging/collect_libs.py --binary build/strata --search /usr/local/cuda/lib64 --out /stage --also 'libcublasLt.so*'; \
|
||||
archs=""; for a in $(echo "$CUDA_ARCHITECTURES" | tr ';,' ' '); do archs="$archs --arch sm_$a"; done; \
|
||||
last=$(echo "$CUDA_ARCHITECTURES" | tr ';,' ' ' | awk '{print $NF}'); \
|
||||
|
||||
+140
-177
@@ -12,103 +12,91 @@
|
||||
<p align="center">Unterstützt NVIDIA, AMD, Intel · Windows, Linux, Mac · kostenlos und Open Source</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Neueste Version"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT-Lizenz"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub-Sterne"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=black" alt="Spendier mir einen Kaffee"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Ein Voxel-Pagodengarten, den das Modell von Strata geschrieben hat, im Browser"></a><br>
|
||||
<sub>Ein Voxel-Pagodengarten aus einem einzigen Prompt, auf einer RTX 5070 mit Strata (IQ3_S, 128K Kontext) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">ganzes Video (49 s)</a></sub></p>
|
||||
|
||||
Strata lässt **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** laufen, ein Modell mit 125
|
||||
Milliarden Parametern, für das man sonst einen Server braucht, auf einem normalen PC. Es chattet, schreibt Code, liest
|
||||
Bilder und arbeitet mit deinen Apps und Coding-Agents zusammen. Deine Chats und Dateien bleiben auf deinem PC.
|
||||
<p align="center">
|
||||
<a href="#installation"><b>Installation</b></a> ·
|
||||
<a href="#modelle">Modelle</a> ·
|
||||
<a href="#wie-schnell-ist-es">Tempo</a> ·
|
||||
<a href="#benutzung">Benutzung</a> ·
|
||||
<a href="#wie-funktioniert-das">Wie es funktioniert</a> ·
|
||||
<a href="#etwas-ist-schiefgegangen">Hilfe</a> ·
|
||||
<a href="#strata-unterstützen">Unterstützen</a>
|
||||
</p>
|
||||
|
||||
**Neu hier?** Sieh dir an, [was du brauchst](#was-du-brauchst), und [installiere dann die App](#option-a-die-strata-app).
|
||||
Strata lässt **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** laufen, ein Modell mit 125
|
||||
Milliarden Parametern, für das man sonst einen Server braucht, auf einem normalen PC. Ab 0.2.4 läuft im Terminal auch
|
||||
**DeepSeek V4.1-Flash**. Es chattet, schreibt Code, liest Bilder und arbeitet mit deinen Apps und Coding-Agents zusammen.
|
||||
Deine Chats und Dateien bleiben auf deinem PC.
|
||||
|
||||
- **Läuft auf einem Gaming-PC.** 32 GB Arbeitsspeicher (RAM) und Grafikspeicher zusammen reichen für den Anfang.
|
||||
- **Passt zu deinen Werkzeugen.** Strata spricht die OpenAI- und die Anthropic-API, deshalb verbinden sich Claude Code, Codex CLI, OpenCode und andere Agents damit.
|
||||
- **Passt zu deinen Werkzeugen.** Claude Code, Codex CLI, OpenCode und andere Agents verbinden sich über die OpenAI- und die Anthropic-API.
|
||||
- **Liest Bilder.** Das ist standardmäßig eingeschaltet.
|
||||
- **Kostenlos.** MIT-Lizenz, kein Konto, keine Cloud.
|
||||
- **Privat.** Keine Telemetrie. Was Strata herunterlädt, wann es das Internet nutzt und wie es Downloads prüft, steht in [docs/SAFETY.md](docs/SAFETY.md).
|
||||
|
||||
Version 0.2.2 · Engine 0.1.47
|
||||
- **Kostenlos und privat.** MIT-Lizenz, kein Konto, keine Cloud, keine Telemetrie ([was es herunterlädt und wann](docs/SAFETY.md)).
|
||||
|
||||
## Was du brauchst
|
||||
|
||||
**Du brauchst mindestens 32 GB Arbeitsspeicher (RAM) und Grafikspeicher (VRAM) zusammen.** Bei einem Mac oder einem Ryzen AI Max
|
||||
ist das der gemeinsame Speicher. Mit mehr Speicher läuft ein größeres oder ein schnelleres Modell.
|
||||
**Du brauchst mindestens 32 GB Speicher: Arbeitsspeicher (RAM) und Grafikspeicher (VRAM) zusammen.** Bei einem Mac oder einem
|
||||
Ryzen AI Max ist das der gemeinsame Speicher. Ein PC mit 32 GB und einer Karte mit 12-16 GB führt die kleineren Größen aus,
|
||||
mit 64 GB laufen alle Größen.
|
||||
|
||||
- Ein PC mit 32 GB und einer Grafikkarte mit 12-16 GB führt den Coder aus.
|
||||
- Mit 64 GB RAM laufen alle Größen.
|
||||
|
||||
| Dein Computer | Details |
|
||||
| Läuft auf | Anleitung |
|
||||
| --- | --- |
|
||||
| **NVIDIA** GeForce RTX 20, 30, 40, 50 Serie (12 GB oder mehr) | Installiere die App oder das Terminal. |
|
||||
| **AMD** Radeon RX 6800 und neuer, Radeon AI PRO R9700 (12 GB oder mehr) | [AMD-Karten](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [Intel-Engine](docs/INTEL.md) |
|
||||
| **Apple-Silicon-Mac** (M1 oder neuer, macOS 15 oder neuer) | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** und andere Systeme mit gemeinsamem Speicher | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Ältere Karten und Prozessoren | [Ältere GPUs](docs/OLDER_GPUS.md), [Ältere CPUs](docs/INSTALL.md#older-cpus-experimental) |
|
||||
| **NVIDIA** GeForce RTX 20, 30, 40 und 50 Serie, 12 GB oder mehr | [Installation](#installation) |
|
||||
| **AMD** Radeon RX 6800 und neuer, Radeon AI PRO R9700 | [AMD-Karten](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md) |
|
||||
| **Apple-Silicon-Mac**, M1 oder neuer, macOS 15 oder neuer | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** und andere PCs mit gemeinsamem Speicher | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Ältere Karten und Prozessoren | [Ältere GPUs](docs/OLDER_GPUS.md) |
|
||||
|
||||
Außerdem nötig:
|
||||
|
||||
- etwa 80 GB freier Speicherplatz (mit einer SSD startet es beim ersten Mal viel schneller)
|
||||
- Windows 10 / 11, Linux oder macOS
|
||||
- ein aktueller Grafiktreiber
|
||||
|
||||
Zwei oder drei Karten desselben Herstellers können sich ein Modell teilen ([Multi-GPU](docs/MULTI_GPU.md)). Strata teilt ein
|
||||
Modell nie auf NVIDIA- und AMD-Karten auf. Die vollständige Liste: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
Außerdem brauchst du etwa 80 GB freien Speicherplatz (eine SSD), Windows 10 / 11, Linux oder macOS und einen aktuellen
|
||||
Grafiktreiber. Zwei oder drei Karten desselben Herstellers können sich ein Modell teilen ([Multi-GPU](docs/MULTI_GPU.md)).
|
||||
|
||||
## Installation
|
||||
|
||||
Such dir eine Option aus. Alle starten dieselbe Engine und nutzen dieselben Modelle.
|
||||
### 1. Die Strata-App (am einfachsten)
|
||||
|
||||
### Option A: die Strata-App
|
||||
Lade die Datei für deinen Computer herunter, öffne sie und drück auf Installieren beim empfohlenen Modell.
|
||||
|
||||
Der einfachste Weg. Die App bringt die Engine mit: installieren, ein Modell wählen, chatten. Lade die Datei für dein
|
||||
System und deine Grafikkarte von der [Releases-Seite](https://github.com/Niko1221/Strata/releases/latest) herunter. Du bist
|
||||
unsicher, welche du brauchst? Lies den Absatz unter der Tabelle.
|
||||
**Windows 10 / 11**
|
||||
|
||||
| System | Deine Grafikkarte | Datei zum Herunterladen |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, Treiber 580 oder neuer | [`Strata-0.2.2-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda13.exe) |
|
||||
| | NVIDIA, ältere Karte oder Treiber älter als 580 | [`Strata-0.2.2-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda12.exe) |
|
||||
| | AMD Radeon RX 9000 Serie oder Radeon AI PRO R9700 | [`Strata-0.2.2-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip.exe) |
|
||||
| | AMD Radeon RX 7000 Serie oder Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, Treiber 580 oder neuer | [`Strata-0.2.2-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, ältere Karte oder Treiber älter als 580 | [`Strata-0.2.2-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon RX 9000 Serie oder Radeon AI PRO R9700 | [`Strata-0.2.2-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon RX 7000 Serie | [`Strata-0.2.2-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 oder neuer) | eingebaut | [`Strata-0.2.2-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-mac.dmg) |
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-windows-x64-setup.exe)
|
||||
|
||||
**Wenn du unsicher bist:** Bei NVIDIA nimmst du `cuda13`, wenn dein Treiber Version 580 oder neuer ist, sonst `cuda12`.
|
||||
`cuda12` ist für ältere Karten und ältere Treiber. Die Dateien `hip` und `amd` sind für AMD-Karten. Für andere AMD-Karten
|
||||
(RX 6000) gibt es noch keine fertige App: siehe [AMD-Karten](docs/AMD_HIP.md).
|
||||
**Linux**
|
||||
|
||||
Danach:
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-linux-x86_64.AppImage)
|
||||
|
||||
- **Windows:** Starte die `.exe`. Sie wird für deinen Benutzer installiert und braucht keine Administratorrechte. Wenn
|
||||
Windows "Der Computer wurde durch Windows geschützt" meldet, wähle **Weitere Informationen** und dann **Trotzdem
|
||||
ausführen** (der Installer ist noch nicht signiert).
|
||||
- **Linux:** Mach die Datei ausführbar (`chmod +x`) und starte sie. Sie braucht FUSE 2: `sudo apt install libfuse2`
|
||||
(Ubuntu 22.04) oder `libfuse2t64` (Ubuntu 24.04 und neuer).
|
||||
- **Mac:** Öffne die `.dmg` und zieh Strata in den Ordner "Programme".
|
||||
**Mac** (Apple Silicon, macOS 15 oder neuer)
|
||||
|
||||
Auf dem ersten Bildschirm liest die App deinen PC aus und empfiehlt ein passendes Modell. Drück auf Installieren. Das Modell
|
||||
ist ein großer Download (etwa 40-70 GB) und wird nach einer Unterbrechung dort fortgesetzt, wo er aufgehört hat. Wenn du
|
||||
das Fenster schließt, beenden sich Strata und die Engine.
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-macos-arm64.dmg)
|
||||
|
||||
### Option B: das Terminal (eine Zeile)
|
||||
Die App wählt die passende Engine für deine Grafikkarte selbst. Alle Dateien stehen auf der
|
||||
[Releases-Seite](https://github.com/Niko1221/Strata/releases/latest).
|
||||
|
||||
Das Terminal ist der Befehl `strata`, gedacht für Skripte, Server und KI-Agents.
|
||||
| Erster Start | Was du tust |
|
||||
| --- | --- |
|
||||
| **Windows** | Starte die `.exe`. Du brauchst keine Administratorrechte. Wenn Windows "Der Computer wurde durch Windows geschützt" meldet, wähle **Weitere Informationen** und dann **Trotzdem ausführen**. |
|
||||
| **Linux** | Mach die Datei ausführbar (`chmod +x`) und starte sie. Sie braucht FUSE 2: `sudo apt install libfuse2t64` (Ubuntu 24.04 und neuer) oder `libfuse2` (22.04). |
|
||||
| **Mac** | Öffne die `.dmg` und zieh Strata in den Ordner "Programme". |
|
||||
|
||||
Das Modell ist ein großer Download (40-70 GB). Wenn er abbricht, geht er dort weiter, wo er aufgehört hat.
|
||||
|
||||
### 2. Das Terminal (eine Zeile)
|
||||
|
||||
Der Befehl `strata`, für Skripte, Server und KI-Agents. Damit läuft auch DeepSeek.
|
||||
|
||||
Linux und Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
@@ -117,26 +105,17 @@ Windows (PowerShell):
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
Das Skript prüft deinen PC, lädt das Terminal herunter, kontrolliert die SHA-256-Prüfsumme und entpackt es in deinen
|
||||
Home-Ordner. Es braucht keine Administratorrechte. Öffne unter Windows danach ein neues Terminalfenster. Dann startest du
|
||||
Strata:
|
||||
Dann startest du es. Es stellt dir eine Frage nach der anderen (Modell, Kontext, Bilder, wer sich verbinden darf), lädt,
|
||||
was es braucht, und beginnt zu laufen:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
Es stellt dir eine Frage nach der anderen: Modell, Kontext, Bilder, wer sich verbinden darf. Danach lädt es die Engine für
|
||||
deine Grafikkarte herunter (signiert und vor der Nutzung probeweise gestartet), dazu das Modell, und beginnt zu laufen.
|
||||
Für Skripte und KI-Agents gibt es Flags:
|
||||
Du brauchst keine Administratorrechte. Die Installation prüft die SHA-256-Prüfsumme von allem, was sie herunterlädt. Öffne
|
||||
unter Windows nach der Installation ein neues Terminalfenster. Alle Bildschirme und Flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
Wenn du das Installationsskript vor dem Ausführen lesen willst, lade es zuerst mit `curl -fsSL <address> -o install.sh`
|
||||
herunter, lies es und führe dann `sh install.sh` aus. Alle Bildschirme und Flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### Option C: deine KI richtet es ein
|
||||
### 3. Deine KI richtet es ein
|
||||
|
||||
Füge das in Claude Code, Cursor, Codex, GitHub Copilot oder einen anderen Coding-Assistenten ein:
|
||||
|
||||
@@ -152,48 +131,58 @@ Du willst es lieber selbst bauen? Siehe [Aus dem Quellcode bauen](#aus-dem-quell
|
||||
|
||||
## Modelle
|
||||
|
||||
Die App und `strata launch` empfehlen ein Modell passend zu deinem Speicher. Es ist ein einziges Modell in mehreren
|
||||
Größen, mehr oder weniger stark komprimiert. Kleinere Größen sind schneller. Größere Größen sind etwas klüger.
|
||||
Die App und `strata launch` empfehlen eine Größe passend zu deinem Speicher. Kleinere Größen sind schneller, größere
|
||||
Größen sind etwas klüger.
|
||||
|
||||
### Wenig Speicher? Probier Gyro
|
||||
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (von agentionai) ist die kleinste Möglichkeit, dieses Modell auszuführen.
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) von agentionai ist die kleinste Möglichkeit, Qwen3.8-Flash-Next
|
||||
auszuführen. Es läuft auf NVIDIA, AMD, Intel Arc und Apple Silicon.
|
||||
|
||||
| Größe | Benötigter Speicher (RAM + VRAM) | Download | Was es ist |
|
||||
| Größe | Benötigter Speicher (RAM + VRAM) | Download | Geeignet für |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27,6 GB | 58,5 GB | Für PCs, auf denen nichts anderes läuft. Die Qualität ist geringer als bei IQ2_XS. |
|
||||
| **Gyro-M** | 35 GB | 92,0 GB | Ungefähr die Qualität von IQ3_XXS bei etwa 20 % weniger GPU-Speicher. Es schreibt Antworten langsamer als Q2_0: 16 % auf einer RTX 3060 mit 12 GB, 21 % auf einer R9700. |
|
||||
| **Gyro-S** | 27,6 GB | 58,5 GB | PCs, auf denen nichts anderes läuft. 9 Tokens/s auf einem Mac mini mit 32 GB. |
|
||||
| **Gyro-M** | 35 GB | 92,0 GB | Ungefähr die Qualität von IQ3_XXS bei 20 % weniger GPU-Speicher. |
|
||||
|
||||
Gyro läuft auf NVIDIA- und AMD-Karten (bei AMD experimentell) auf Intel-Arc-Karten unter Linux (getestet auf einer Arc A750) und ab 0.2.2 auf Macs mit Apple Silicon (Gyro-S auf einem Mac mini mit 32 GB: 9 Tokens/s, die Experten werden von der SSD gelesen). Details und Messwerte:
|
||||
[docs/GYRO.md](docs/GYRO.md). Starte es mit `strata launch --model gyro-s`.
|
||||
Starte es mit `strata launch --model gyro-s`. Details: [docs/GYRO.md](docs/GYRO.md).
|
||||
|
||||
### Die anderen Größen
|
||||
### Die Größen von Qwen3.8-Flash-Next
|
||||
|
||||
| Dein Speicher (RAM + VRAM) | Nimm | Hinweise |
|
||||
| Dein Speicher (RAM + VRAM) | Nimm | Warum |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Gyro-S**, oder den **[Coder](docs/MODELS.md#coder)** für Code | Der Coder braucht 32 GB RAM und behält die Hälfte der Experten, deshalb ist er außerhalb von Code schwächer |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), oder **Gyro-M** (35 GB) | Swift denkt kürzer nach, bevor es antwortet. Gyro-M kommt der Qualität von IQ3_XXS nahe, schreibt Antworten aber 16-21 % langsamer als Q2_0 |
|
||||
| **32 GB** | **Gyro-S**, oder den **[Coder](docs/MODELS.md#coder)** für Code | Der Coder behält die Experten, die Code braucht |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB) oder **Gyro-M** | Swift denkt kürzer nach, bevor es antwortet |
|
||||
| **48 GB** | **IQ3_XXS** (47,0 GB) | Langsamer, besser |
|
||||
| **64 GB** | **IQ3_S** (54,8 GB), das Original oder Swift 1.5 | Am langsamsten, am besten. Es erreicht in den veröffentlichten Tests das volle Modell |
|
||||
| **96 GB oder mehr** | **UD-IQ4_XS** von Unsloth (etwa 72 GB RAM) bis **Q8_0** (etwa 136 GB), 4 bis 8 Bit | Näher am vollen Modell. Bei weniger RAM liest Strata einen Teil von der SSD, das ist viel langsamer |
|
||||
| **64 GB** | **IQ3_S** (54,8 GB) | Erreicht in den veröffentlichten Tests das volle Modell |
|
||||
| **96 GB oder mehr** | **UD-IQ4_XS** bis **Q8_0** von Unsloth | Am nächsten am vollen Modell |
|
||||
|
||||
Auch mit weniger RAM, als eine Zeile nennt, kann es funktionieren: Strata wechselt von selbst in seinen
|
||||
[Modus für wenig RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Rechne mit weniger Tempo, mit einer kleinen
|
||||
Grafikkarte mit deutlich weniger.
|
||||
Auch mit weniger RAM, als eine Zeile nennt, kann es funktionieren, nur langsamer: Strata wechselt von selbst in seinen
|
||||
[Modus für wenig RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Außerdem gibt es:
|
||||
[NVFP4](docs/NVFP4.md) für NVIDIA-Karten, [Qwen3.6-35B-A3B](docs/QWEN36.md) (kleiner, mehrere Anfragen gleichzeitig) und
|
||||
[OrcaRouters Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs). Alle Größen und was wo läuft:
|
||||
[docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
Weitere Auswahl:
|
||||
### DeepSeek V4.1-Flash (Terminal, ab 0.2.4)
|
||||
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** ist eine Feinabstimmung, die kürzer nachdenkt, bevor sie antwortet, bei etwa gleicher Qualität und gleichem RAM-Bedarf.
|
||||
- **[NVFP4](docs/NVFP4.md)** ist NVIDIAs 4-Bit-Version für NVIDIA-Karten. Sie braucht etwa 80 GB RAM, um alle Experten im RAM zu halten.
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** ist ein kleineres, anderes Modell, das Bilder liest und mehrere Anfragen gleichzeitig beantworten kann.
|
||||
- **[OrcaRouters Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** richtest du von Hand ein.
|
||||
Ein deutlich größeres Modell, das auf einer Grafikkarte mit 12 GB und 46 bis 64 GB RAM läuft. Strata hält die meistgenutzten
|
||||
Teile im Speicher und liest den Rest von deiner SSD. [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF)
|
||||
hat die Sets gebaut. Es läuft auf NVIDIA-RTX-Karten der Serien 30, 40 und 50 und auf der Radeon AI PRO R9700. Wähle es in
|
||||
`strata launch`. Die Desktop-App bietet es noch nicht an.
|
||||
|
||||
Alle Größen, Downloads und was wo läuft: [docs/MODELS.md](docs/MODELS.md).
|
||||
| Set | Download | Qualität | Schreibt Code mit |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M** (1,75 Bit) | 239 GB | Das kleinste, schwächer bei einfacher Prosa | auf einem PC noch nicht gemessen |
|
||||
| **IQ2_XXS** (2.00 Bit) | 257 GB | Gute Balance | 10 Tokens/s (RTX 3060), 5 (RTX 5070), 48 (4x R9700) |
|
||||
| **IQ2_XS** (2.25 Bit) | 274 GB | Am nächsten am Original | 15 Tokens/s (eine R9700), 80 (4x R9700) |
|
||||
| **IQ4_NL** (Engram-Tabelle) | in jedem Download oben enthalten | Die Nachschlagetabelle, die alle drei Sets teilen | |
|
||||
|
||||
Starte es mit `strata launch --model ist-das-2.00bpw` (oder `ist-das-1.75bpw`, `ist-das-2.25bpw`). Qualitätswerte,
|
||||
Geschwindigkeiten je PC und die manuelle Installation: [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
|
||||
|
||||
## Wie schnell ist es?
|
||||
|
||||
Gemessen mit einer Grafikkarte mit 12 GB (RTX 5070, Ryzen 5 7600, 64 GB RAM). Ergebnisse anderer Karten findest du hier:
|
||||
[Tempo jeder Größe](docs/MODELS.md#how-fast-is-each-size), [Ergebnisse der Community](docs/COMMUNITY_BENCHMARKS.md).
|
||||
Gemessen mit einer Grafikkarte mit 12 GB (RTX 5070, 64 GB RAM). Ergebnisse anderer Karten:
|
||||
[Tempo jeder Größe](docs/MODELS.md#how-fast-is-each-size) und [Ergebnisse der Community](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
| Modell | Antworten erscheinen mit | Liest deinen Prompt mit |
|
||||
| --- | --- | --- |
|
||||
@@ -203,10 +192,7 @@ Gemessen mit einer Grafikkarte mit 12 GB (RTX 5070, Ryzen 5 7600, 64 GB RAM). Er
|
||||
| **IQ3_S** | `███████░░░░░` 53 Tokens/s | `███████░░░░░` 1.620 Tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 Tokens/s | `██████████░░` 2.180 Tokens/s |
|
||||
|
||||
Ein Token ist etwa ¾ eines Wortes, 60 Tokens pro Sekunde sind also schneller, als du lesen kannst. "Liest deinen Prompt"
|
||||
beschreibt, wie schnell Strata ein langes Dokument, Code oder einen Chatverlauf aufnimmt.
|
||||
|
||||
Gemessen mit einer AMD-Karte mit 16 GB (RX 9070 XT, Ryzen 9 3900X, 47 GB RAM):
|
||||
Auf einer AMD-Karte mit 16 GB (RX 9070 XT, 47 GB RAM):
|
||||
|
||||
| Modell | Antworten erscheinen mit | Liest deinen Prompt mit |
|
||||
| --- | --- | --- |
|
||||
@@ -214,84 +200,59 @@ Gemessen mit einer AMD-Karte mit 16 GB (RX 9070 XT, Ryzen 9 3900X, 47 GB RAM):
|
||||
| **IQ2_XS** | `███████░░░░░` 52 Tokens/s | `█████░░░░░░░` 1.110 Tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 Tokens/s | `██████░░░░░░` 1.420 Tokens/s |
|
||||
|
||||
Die Balken haben in beiden Tabellen denselben Maßstab. Eine Karte mit mehr VRAM ist schneller. Eine RTX 3090 (24 GB) sollte
|
||||
etwa 100-140 Tokens pro Sekunde schreiben. Die Engine-Version hinter jeder Zahl steht in [DETAILS.md](docs/DETAILS.md#speed-measured).
|
||||
Ein Token ist etwa ¾ eines Wortes, 60 Tokens pro Sekunde sind also schneller, als du lesen kannst. "Liest deinen Prompt"
|
||||
beschreibt, wie schnell Strata ein langes Dokument, Code oder einen Chatverlauf aufnimmt.
|
||||
|
||||
## Benutzung
|
||||
|
||||
### In der App
|
||||
|
||||
Die App hat einen **Chat**, einen **Monitor** für das Modell sowie für GPU, CPU und RAM, eine Seite **Modelle** und die
|
||||
**Einstellungen**. Der Chat kann in einem Ordner auf deinem PC arbeiten. Im Modus **Manuell** fragt er nach, bevor er eine
|
||||
Datei ändert oder einen Befehl ausführt. Mit `/loop 10m <what to do>` wiederholst du eine Nachricht alle 10 Minuten, solange
|
||||
der Chat geöffnet ist.
|
||||
Ein Chat, ein **Monitor** für das Modell sowie für GPU, CPU und RAM, eine Seite **Modelle** und die **Einstellungen**. Der
|
||||
Chat kann in einem Ordner auf deinem PC arbeiten. Im Modus **Manuell** fragt er nach, bevor er eine Datei ändert oder einen
|
||||
Befehl ausführt. Er kann das Web durchsuchen (DuckDuckGo oder Bing, ohne Konto). Die Suchbegriffe sind das Einzige, was
|
||||
deinen PC verlässt.
|
||||
|
||||
Der Chat kann das Web mit DuckDuckGo (oder Bing) durchsuchen, ohne Konto. Unter **Einstellungen, Tools** kannst du einen
|
||||
Suchschlüssel hinzufügen. Die Suche ist die einzige Funktion, die etwas von deinem PC sendet (die Suchbegriffe).
|
||||
|
||||
Um zwei Modelle auszuführen, schalte **Mehrere Modelle ausführen** ein und gib jedem Modell eine eigene Karte desselben
|
||||
Herstellers.
|
||||
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
|
||||
<sub>Ein Chat in der Strata-App: Der Agent hat einen kleinen Voxel-Pagodengarten als eine HTML-Datei geschrieben, und die Seite öffnet sich in der Vorschau neben dem Gespräch</sub></p>
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="Die Strata-App: ein Chat, in dem der Agent einen kleinen Voxel-Pagodengarten gebaut hat, die Seite ist rechts in der Vorschau geöffnet"><br>
|
||||
<sub>Der Agent hat einen Voxel-Pagodengarten als eine HTML-Datei geschrieben, und die Seite öffnet sich in der Vorschau neben dem Chat</sub></p>
|
||||
|
||||
### Im Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="Die Übersicht des Strata-Terminal-Monitors, während das Modell eine Antwort schreibt"><br>
|
||||
<sub><code>strata monitor</code>, während das Modell antwortet: Geschwindigkeit, GPU, Speicher und die letzten Anfragen</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided screens: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
|
||||
strata launch --plan --model q2_0 # show what would happen; nothing changes
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
|
||||
strata update --check # is there a new engine? (strata update installs it)
|
||||
strata doctor # check the installed engine files
|
||||
strata launch # guided: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve and print the address
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor # watch speed, GPU, memory and requests
|
||||
strata update # get the newest engine
|
||||
strata doctor # check the installed files
|
||||
```
|
||||
|
||||
`strata --help` listet alles auf. Starten und Bereitstellen laden nichts herunter. Nur die Installation, `strata launch`
|
||||
(wenn du ein Modell wählst) und `strata update` tun das. Um Strata von deinem Handy oder einem anderen PC aus zu erreichen,
|
||||
nutze `strata launch --access lan`. Der Befehl erzeugt einen API-Schlüssel und startet in einem Netzwerk nicht ohne einen.
|
||||
Ein Browser öffnet sich nur, wenn du `--open` angibst. Anleitung: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Der ganze Client: [docs/TERMINAL.md](docs/TERMINAL.md).
|
||||
Um Strata von deinem Handy oder einem anderen PC aus zu erreichen, nutze `strata launch --access lan`. Der Befehl erzeugt
|
||||
einen API-Schlüssel für dich. `strata --help` listet alles auf, und [docs/TERMINAL.md](docs/TERMINAL.md) erklärt es.
|
||||
|
||||
### Mit KI-Agents
|
||||
### Mit KI-Agents und anderen Apps
|
||||
|
||||
Ein Agent installiert Strata mit dem Einzeiler aus Option B und führt
|
||||
`strata launch --model NAME --yes --no-tui` aus. Die Schritte, Flags und Exit-Codes stehen in [docs/AI_SETUP.md](docs/AI_SETUP.md),
|
||||
die Regeln für das Repository in [AGENTS.md](AGENTS.md).
|
||||
Ein Agent installiert Strata mit dem Einzeiler oben und führt `strata launch --model NAME --yes --no-tui` aus. Die Schritte,
|
||||
Flags und Exit-Codes stehen in [docs/AI_SETUP.md](docs/AI_SETUP.md). Die Regeln für das Repository stehen in [AGENTS.md](AGENTS.md).
|
||||
|
||||
### Andere Apps verbinden
|
||||
|
||||
| App-Typ | Basis-URL |
|
||||
| Deine App spricht | Basis-URL |
|
||||
| --- | --- |
|
||||
| OpenAI-kompatibel (die meisten Apps und Agents) | `http://127.0.0.1:8080/v1` (beliebiger API-Schlüssel, beliebiger Modellname) |
|
||||
| Anthropic-API (Claude Code und andere) | `http://127.0.0.1:8080`, ohne `/v1`. Diese Apps hängen `/v1/messages` selbst an. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI-API (die meisten Apps und Agents) | `http://127.0.0.1:8080/v1` (beliebiger API-Schlüssel, beliebiger Modellname) |
|
||||
| Anthropic-API (Claude Code) | `http://127.0.0.1:8080`, ohne `/v1`. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([Einrichtung](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` gibt die genauen Einstellungen für OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent und OpenClaw aus.
|
||||
`strata connect opencode --write` schreibt die Datei nach einer Rückfrage und legt eine Sicherung an.
|
||||
|
||||
### Bilder
|
||||
|
||||
Bilder sind für Modelle, die sie lesen können, standardmäßig eingeschaltet. Hänge ein Bild im Chat an oder füge es ein, oder
|
||||
sende es aus deiner App. Der Bildleser läuft auf dem Prozessor, wenn deine Grafikkarte weniger als 24 GB hat (einige Sekunden
|
||||
pro Bild, ohne Speicher der Karte zu belegen), und ab 24 GB auf der Grafikkarte. Die Regel gilt für NVIDIA und AMD gleich.
|
||||
Du kannst Bilder pro Modell ausschalten oder den Leser mit `--vision off`, `--vision gpu` oder `--vision cpu` wählen.
|
||||
|
||||
Die Zahlen hinter der Regel: Auf einer Radeon AI PRO R9700 brauchte der Bildleser der Karte 57 ms für ein Bild mit 260 Tokens,
|
||||
der des Prozessors 3,1 s. Auf Karten mit 16 GB (Tesla P100 und RTX A4000, Linux) kostete der Bildleser der Karte 3-5 % des
|
||||
Texttempos, weil er etwa 1,4 GB belegt, die sonst das Modell nutzen würde. Auf dem Mac liest die GPU, egal wie viel Speicher er hat (0,45 s für ein Bild mit 260 Tokens auf einem M4 mit 32 GB, etwa 1,2 GB des gemeinsamen
|
||||
Speichers); der Prozessor braucht dort etwa 50 s pro Bild und liest deshalb nur mit `--vision cpu`.
|
||||
`strata connect` gibt die genauen Einstellungen für OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent und OpenClaw
|
||||
aus, und `strata connect opencode --write` schreibt sie für dich (mit einer Sicherung).
|
||||
|
||||
### Gut zu wissen
|
||||
|
||||
- Das Denkniveau (**aus, niedrig, mittel oder hoch**) stellst du im Chat-Menü oder unter "reasoning effort" in deiner App ein. Aus ist am schnellsten.
|
||||
- Strata beantwortet standardmäßig eine Anfrage nach der anderen. Für mehrere gleichzeitig nutze `--parallel N` in `strata launch` oder
|
||||
`"parallel": 2` in der Konfiguration ([BATCHING.md](docs/BATCHING.md)). Auf einer Karte mit 12 GB wird dadurch jede Antwort langsamer.
|
||||
- Strata liest die erste Nachricht eines Chats komplett, etwa 1 Minute pro 30.000 Tokens. Folgenachrichten starten in Sekunden.
|
||||
- Wo deine Chats gespeichert werden und die ganze API: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
|
||||
- **Bilder** sind für jedes Modell eingeschaltet, das sie lesen kann. Hänge ein Bild im Chat an oder füge es ein, oder sende es aus deiner App. Details: [docs/IMAGES.md](docs/IMAGES.md).
|
||||
- **Das Denkniveau** kann aus, niedrig, mittel oder hoch sein, im Chat-Menü oder unter "reasoning effort" in deiner App. Aus ist am schnellsten.
|
||||
- **Die erste Nachricht** eines Chats wird komplett gelesen, etwa 1 Minute pro 30.000 Tokens. Folgenachrichten starten in Sekunden.
|
||||
- **Eine Anfrage nach der anderen** ist die Standardeinstellung. Für mehrere nutze `--parallel N` ([BATCHING.md](docs/BATCHING.md)). Auf einer Karte mit 12 GB wird dadurch jede Antwort langsamer.
|
||||
|
||||
## Aus dem Quellcode bauen
|
||||
|
||||
@@ -353,13 +314,13 @@ Strata bringt das Modell auf deinen PC, indem es die Arbeit auf den ganzen Rechn
|
||||
- **Deine SSD ist das Archiv.** Sie enthält eine Nachschlagetabelle von 29 GB. Das Modell liest pro Wort nur wenige Zeilen
|
||||
daraus, deshalb muss sie nicht im RAM liegen.
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Die 24.576 Experten des Modells: die meistgenutzten auf der Grafikkarte, alle im RAM, eine Nachschlagetabelle auf der SSD"></p>
|
||||
|
||||
Ein kleiner Helfer rät außerdem die nächsten Wörter. Das große Modell prüft sie alle auf einmal und behält die richtigen. Du
|
||||
bekommst dieselbe Antwort, 1,6-1,8-mal früher. Lange Texte werden in großen Stücken gelesen, bis zu 8.192 Tokens auf einmal,
|
||||
mit über 1.000 Tokens pro Sekunde.
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Ein kleiner Helfer rät die nächsten Wörter, das große Modell prüft sie alle auf einmal und behält die richtigen"></p>
|
||||
|
||||
Die ausführliche Erklärung: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Jeder Teil mit seinen Zahlen:
|
||||
[die Details](docs/DETAILS.md#how-it-works) und das [Paper](docs/paper/Strata-Paper.pdf).
|
||||
@@ -369,25 +330,27 @@ Die ausführliche Erklärung: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Jede
|
||||
- **Mein PC ist beim ersten Start von Strata eingefroren.** Strata lädt das Modell in den RAM. Warte ab und schließe das
|
||||
Fenster nicht. Nach 10 Minuten immer noch eingefroren? Starte den PC neu, schließe andere Programme und versuch es noch
|
||||
einmal, oder wähle eine kleinere Größe.
|
||||
- **Es ist beim Herunterladen oder Installieren stehen geblieben.** Starte die App oder `strata launch` noch einmal, oder
|
||||
führe `START-HERE.bat` (oder `./setup.sh`) erneut aus. Es macht dort weiter, wo es aufgehört hat.
|
||||
- **Es ist sehr langsam, die Festplattenanzeige blinkt ständig, oder es meldet "the engine stopped unexpectedly".** Dein PC
|
||||
hat nicht genug freien RAM. Schließe andere Programme (Browser brauchen viel) oder wähle eine kleinere Größe wie Q2_0 oder IQ2_XS.
|
||||
- **Es ist beim Herunterladen oder Installieren stehen geblieben.** Starte die App oder `strata launch` noch einmal. Es
|
||||
macht dort weiter, wo es aufgehört hat.
|
||||
- **Es ist sehr langsam, oder es meldet "the engine stopped unexpectedly".** Dein PC hat nicht genug freien RAM. Schließe
|
||||
andere Programme (Browser brauchen viel) oder wähle eine kleinere Größe wie Q2_0 oder IQ2_XS.
|
||||
- **Es meldet, dass es keine Engine für diesen PC gibt.** Öffne in der App **Einstellungen, Engine** und drücke **Engine
|
||||
aktualisieren**. Führe im Terminal `strata update` aus. `strata doctor` zeigt, welche Engine-Dateien installiert sind.
|
||||
aktualisieren**. Führe im Terminal `strata update` aus.
|
||||
- **Es meldet, dass Port 8080 schon belegt ist.** Strata läuft bereits. Such das Fenster.
|
||||
|
||||
Weitere Probleme und Lösungen: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Du kommst nicht weiter? Öffne ein
|
||||
[Issue](https://github.com/Niko1221/Strata/issues). Hänge in der App die Datei aus **Einstellungen, Engine, Diagnose, Protokolle anzeigen, Protokolle speichern** an. Füge im Terminal die Ausgabe von `strata doctor` ein. Hänge beim Start aus dem Quellcode die
|
||||
Datei `strata-<model>.log` aus dem Strata-Ordner an. Ein Sicherheitsproblem meldest du bitte privat: [SECURITY.md](SECURITY.md).
|
||||
Weitere Lösungen: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Du kommst nicht weiter? Öffne ein
|
||||
[Issue](https://github.com/Niko1221/Strata/issues) und hänge die Protokolle an: in der App unter **Einstellungen, Engine,
|
||||
Diagnose, Protokolle speichern**, im Terminal die Ausgabe von `strata doctor`. Ein Sicherheitsproblem meldest du bitte
|
||||
privat: [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Danksagung und Lizenz
|
||||
|
||||
Das Modell ist [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) vom Qwen-Team. Komprimiert wurde es von
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5), agentionai (Gyro)
|
||||
und Unsloth. Strata nutzt Teile von [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Alle Danksagungen:
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata ist Open Source unter der [MIT-Lizenz](LICENSE). Einige Teile und
|
||||
alle Modelle haben eigene Lizenzen ([welche](docs/HOW_IT_WORKS.md#license)).
|
||||
und Unsloth. ISTA-DASLab hat auch die Sets von DeepSeek V4.1-Flash gebaut. Strata nutzt Teile von
|
||||
[llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Alle Danksagungen: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits).
|
||||
Strata ist Open Source unter der [MIT-Lizenz](LICENSE). Einige Teile und alle Modelle haben eigene Lizenzen
|
||||
([welche](docs/HOW_IT_WORKS.md#license)).
|
||||
|
||||
## Mitmachen
|
||||
|
||||
|
||||
+140
-179
@@ -12,104 +12,91 @@
|
||||
<p align="center">Compatible con NVIDIA, AMD, Intel · Windows, Linux, Mac · libre y de código abierto</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Última versión"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licencia MIT"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="Estrellas en GitHub"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=black" alt="Invítame a un café"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Un jardín con una pagoda de vóxeles que escribió el modelo de Strata, funcionando en el navegador"></a><br>
|
||||
<sub>Un jardín con una pagoda de vóxeles, hecho con un solo prompt en una RTX 5070 con Strata (IQ3_S, contexto de 128K) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">vídeo completo (49 s)</a></sub></p>
|
||||
|
||||
Strata ejecuta **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, un modelo de 125 mil millones
|
||||
de parámetros que normalmente necesita un servidor, en un PC normal. Conversa, escribe código, lee imágenes y funciona
|
||||
con tus aplicaciones y tus agentes de programación. Tus chats y tus archivos se quedan en tu PC.
|
||||
<p align="center">
|
||||
<a href="#instalación"><b>Instalación</b></a> ·
|
||||
<a href="#modelos">Modelos</a> ·
|
||||
<a href="#qué-velocidad-tiene">Velocidad</a> ·
|
||||
<a href="#uso">Uso</a> ·
|
||||
<a href="#cómo-funciona">Cómo funciona</a> ·
|
||||
<a href="#algo-ha-ido-mal">Ayuda</a> ·
|
||||
<a href="#apoya-strata">Apoyar</a>
|
||||
</p>
|
||||
|
||||
**¿Eres nuevo?** Mira [lo que necesitas](#lo-que-necesitas) y luego [instala la aplicación](#opción-a-la-aplicación-strata).
|
||||
Strata ejecuta **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, un modelo de 125 mil millones
|
||||
de parámetros que normalmente necesita un servidor, en un PC normal. Desde 0.2.4 también ejecuta **DeepSeek V4.1-Flash**
|
||||
en la Terminal. Conversa, escribe código, lee imágenes y funciona con tus aplicaciones y tus agentes de programación. Tus
|
||||
chats y tus archivos se quedan en tu PC.
|
||||
|
||||
- **Funciona en un PC gaming.** Con 32 GB entre RAM y memoria gráfica ya puedes empezar.
|
||||
- **Funciona con tus herramientas.** Habla las API de OpenAI y de Anthropic, así que Claude Code, Codex CLI, OpenCode y otros agentes se conectan a él.
|
||||
- **Lee imágenes.** Viene activado por defecto.
|
||||
- **Gratis.** Licencia MIT, sin cuenta, sin nube.
|
||||
- **Privado.** Sin telemetría. Lo que Strata descarga, cuándo usa internet y cómo comprueba las descargas está en [docs/SAFETY.md](docs/SAFETY.md).
|
||||
|
||||
Versión 0.2.2 · motor 0.1.47
|
||||
- **Funciona con tus herramientas.** Claude Code, Codex CLI, OpenCode y otros agentes se conectan por las API de OpenAI y de Anthropic.
|
||||
- **Lee imágenes**, activado por defecto.
|
||||
- **Gratis y privado.** Licencia MIT, sin cuenta, sin nube, sin telemetría ([qué descarga y cuándo](docs/SAFETY.md)).
|
||||
|
||||
## Lo que necesitas
|
||||
|
||||
**Necesitas al menos 32 GB entre RAM y memoria gráfica (VRAM).** En un Mac o un Ryzen AI Max, es la memoria unificada.
|
||||
Con más memoria puedes usar un modelo más grande o más rápido.
|
||||
**Necesitas al menos 32 GB de memoria: RAM y memoria gráfica (VRAM) juntas.** En un Mac o un Ryzen AI Max, es la memoria
|
||||
unificada. Un PC de 32 GB con una tarjeta de 12 a 16 GB ejecuta los tamaños más pequeños, y con 64 GB funcionan todos los
|
||||
tamaños.
|
||||
|
||||
- Un PC de 32 GB con una tarjeta de 12 a 16 GB ejecuta el Coder.
|
||||
- Con 64 GB de RAM funcionan todos los tamaños.
|
||||
|
||||
| Tu ordenador | Detalles |
|
||||
| Funciona en | Guía |
|
||||
| --- | --- |
|
||||
| **NVIDIA** GeForce RTX serie 20, 30, 40, 50 (12 GB o más) | Instala la aplicación o la Terminal. |
|
||||
| **AMD** Radeon RX 6800 y posteriores, Radeon AI PRO R9700 (12 GB o más) | [Tarjetas AMD](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [motor Intel](docs/INTEL.md) |
|
||||
| **Mac con Apple Silicon** (M1 o posterior, macOS 15 o posterior) | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** y otros sistemas con memoria unificada | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Tarjetas y procesadores antiguos | [GPU antiguas](docs/OLDER_GPUS.md), [CPU antiguas](docs/INSTALL.md#older-cpus-experimental) |
|
||||
| **NVIDIA** GeForce RTX serie 20, 30, 40 y 50, 12 GB o más | [Instalación](#instalación) |
|
||||
| **AMD** Radeon RX 6800 y posteriores, Radeon AI PRO R9700 | [Tarjetas AMD](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md) |
|
||||
| **Mac con Apple Silicon**, M1 o posterior, macOS 15 o posterior | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** y otros PC con memoria unificada | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Tarjetas y procesadores antiguos | [GPU antiguas](docs/OLDER_GPUS.md) |
|
||||
|
||||
También hace falta:
|
||||
|
||||
- unos 80 GB de disco libre (con un SSD el primer arranque es mucho más rápido)
|
||||
- Windows 10 / 11, Linux o macOS
|
||||
- un controlador gráfico actual
|
||||
|
||||
Dos o tres tarjetas de la misma marca pueden compartir un modelo ([multi-GPU](docs/MULTI_GPU.md)). Strata nunca reparte un
|
||||
modelo entre tarjetas NVIDIA y AMD. Lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
También necesitas unos 80 GB de disco libre (un SSD), Windows 10 / 11, Linux o macOS, y un controlador gráfico actual. Dos
|
||||
o tres tarjetas de la misma marca pueden compartir un modelo ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
|
||||
## Instalación
|
||||
|
||||
Elige una opción. Todas arrancan el mismo motor y usan los mismos modelos.
|
||||
### 1. La aplicación Strata (la más fácil)
|
||||
|
||||
### Opción A: la aplicación Strata
|
||||
Descarga el archivo para tu ordenador, ábrelo y pulsa Instalar en el modelo que recomienda.
|
||||
|
||||
La forma más fácil. La aplicación lleva el motor dentro: la instalas, eliges un modelo y chateas. Descarga el archivo para
|
||||
tu sistema y tu tarjeta gráfica desde la [página de versiones](https://github.com/Niko1221/Strata/releases/latest). ¿No
|
||||
sabes cuál elegir? Mira el párrafo que hay debajo de la tabla.
|
||||
**Windows 10 / 11**
|
||||
|
||||
| Sistema | Tu tarjeta gráfica | Archivo que descargar |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, controlador 580 o posterior | [`Strata-0.2.2-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda13.exe) |
|
||||
| | NVIDIA, tarjeta más antigua o controlador anterior a 580 | [`Strata-0.2.2-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda12.exe) |
|
||||
| | AMD Radeon serie RX 9000 o Radeon AI PRO R9700 | [`Strata-0.2.2-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip.exe) |
|
||||
| | AMD Radeon serie RX 7000 o Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, controlador 580 o posterior | [`Strata-0.2.2-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, tarjeta más antigua o controlador anterior a 580 | [`Strata-0.2.2-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon serie RX 9000 o Radeon AI PRO R9700 | [`Strata-0.2.2-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon serie RX 7000 | [`Strata-0.2.2-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 o posterior) | integrada | [`Strata-0.2.2-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-mac.dmg) |
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-windows-x64-setup.exe)
|
||||
|
||||
**Si no estás seguro:** con NVIDIA, elige `cuda13` si la versión de tu controlador es la 580 o posterior, y `cuda12` en caso
|
||||
contrario. `cuda12` es para tarjetas y controladores más antiguos. Los archivos `hip` y `amd` son para tarjetas AMD. Las
|
||||
demás tarjetas AMD (RX 6000) todavía no tienen una aplicación ya preparada: mira
|
||||
[Tarjetas AMD](docs/AMD_HIP.md).
|
||||
**Linux**
|
||||
|
||||
Después:
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-linux-x86_64.AppImage)
|
||||
|
||||
- **Windows:** ejecuta el `.exe`. Se instala para tu usuario y no necesita permisos de administrador. Si Windows muestra
|
||||
"Windows protegió tu PC", elige **Más información** y luego **Ejecutar de todas formas** (el instalador todavía no está
|
||||
firmado).
|
||||
- **Linux:** haz el archivo ejecutable (`chmod +x`) y lánzalo. Necesita FUSE 2: `sudo apt install libfuse2` (Ubuntu 22.04)
|
||||
o `libfuse2t64` (Ubuntu 24.04 y posteriores).
|
||||
- **Mac:** abre el `.dmg` y arrastra Strata a Aplicaciones.
|
||||
**Mac** (Apple Silicon, macOS 15 o posterior)
|
||||
|
||||
En la primera pantalla, la aplicación analiza tu PC y recomienda un modelo adecuado. Pulsa Instalar. El modelo es una
|
||||
descarga grande (unos 40 a 70 GB) y, si se interrumpe, continúa donde se quedó. Al cerrar la ventana, Strata y el motor se
|
||||
detienen.
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-macos-arm64.dmg)
|
||||
|
||||
### Opción B: la Terminal (una línea)
|
||||
La aplicación elige sola el motor adecuado para tu tarjeta gráfica. Todos los archivos están en la
|
||||
[página de versiones](https://github.com/Niko1221/Strata/releases/latest).
|
||||
|
||||
La Terminal es el comando `strata`, pensado para scripts, servidores y agentes de IA.
|
||||
| Primer arranque | Qué hacer |
|
||||
| --- | --- |
|
||||
| **Windows** | Ejecuta el `.exe`. No necesita permisos de administrador. Si Windows muestra "Windows protegió tu PC", elige **Más información** y luego **Ejecutar de todas formas**. |
|
||||
| **Linux** | Haz el archivo ejecutable (`chmod +x`) y lánzalo. Necesita FUSE 2: `sudo apt install libfuse2t64` (Ubuntu 24.04 y posteriores) o `libfuse2` (22.04). |
|
||||
| **Mac** | Abre el `.dmg` y arrastra Strata a Aplicaciones. |
|
||||
|
||||
La descarga del modelo es grande (40-70 GB). Si se detiene, continúa donde se quedó.
|
||||
|
||||
### 2. La Terminal (una línea)
|
||||
|
||||
El comando `strata`, para scripts, servidores y agentes de IA. Es también la forma de ejecutar DeepSeek.
|
||||
|
||||
Linux y Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
@@ -118,25 +105,17 @@ Windows (PowerShell):
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
El script analiza tu PC, descarga la Terminal, verifica su SHA-256 y la descomprime en tu carpeta personal. No necesita
|
||||
permisos de administrador. En Windows, abre una ventana de terminal nueva después. Luego arranca Strata:
|
||||
Después arráncalo. Te hace una pregunta cada vez (modelo, contexto, imágenes, quién puede conectarse), descarga lo que
|
||||
necesita y empieza a servir:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
Te hace una pregunta cada vez: modelo, contexto, imágenes, quién puede conectarse. Después descarga el motor para tu tarjeta
|
||||
gráfica (firmado y arrancado como prueba antes de usarlo) y el modelo, y empieza a servir. Para scripts y agentes de IA, usa
|
||||
opciones:
|
||||
No necesita permisos de administrador. La instalación comprueba el SHA-256 de todo lo que descarga. En Windows, abre una
|
||||
ventana de terminal nueva después de la instalación. Todas las pantallas y opciones: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
Si quieres leer el script de instalación antes de ejecutarlo, descárgalo primero con `curl -fsSL <address> -o install.sh`,
|
||||
léelo y luego ejecuta `sh install.sh`. Todas las pantallas y opciones: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### Opción C: que lo instale tu IA
|
||||
### 3. Que lo instale tu IA
|
||||
|
||||
Pega esto en Claude Code, Cursor, Codex, GitHub Copilot u otro asistente de programación:
|
||||
|
||||
@@ -152,48 +131,58 @@ cómo conectar tus aplicaciones. Las herramientas de IA también pueden instalar
|
||||
|
||||
## Modelos
|
||||
|
||||
La aplicación y `strata launch` recomiendan un modelo según tu memoria. Es un solo modelo en varios tamaños, más o menos
|
||||
comprimido. Los tamaños pequeños son más rápidos. Los grandes son algo más inteligentes.
|
||||
La aplicación y `strata launch` recomiendan un tamaño según tu memoria. Los tamaños pequeños son más rápidos. Los grandes
|
||||
son algo más inteligentes.
|
||||
|
||||
### ¿Poca memoria? Prueba Gyro
|
||||
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (de agentionai) es la forma más pequeña de ejecutar este modelo.
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) de agentionai es la forma más pequeña de ejecutar
|
||||
Qwen3.8-Flash-Next. Funciona en NVIDIA, AMD, Intel Arc y Apple Silicon.
|
||||
|
||||
| Tamaño | Memoria que necesita (RAM + VRAM) | Descarga | Qué es |
|
||||
| Tamaño | Memoria que necesita (RAM + VRAM) | Descarga | Para qué sirve |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27,6 GB | 58,5 GB | Para PC donde no cabe nada más. La calidad es menor que la de IQ2_XS. |
|
||||
| **Gyro-M** | 35 GB | 92,0 GB | Calidad parecida a IQ3_XXS con un 20 % menos de memoria de GPU. Escribe las respuestas un 16 % más despacio que Q2_0 en una RTX 3060 de 12 GB y un 21 % más despacio en una R9700. |
|
||||
| **Gyro-S** | 27,6 GB | 58,5 GB | PC donde no cabe nada más. 9 tokens/s en un Mac mini de 32 GB. |
|
||||
| **Gyro-M** | 35 GB | 92,0 GB | Calidad parecida a IQ3_XXS con un 20 % menos de memoria de GPU. |
|
||||
|
||||
Gyro funciona en tarjetas NVIDIA y AMD (en AMD es experimental) en tarjetas Intel Arc con Linux (probado en una Arc A750) y, desde 0.2.2, en Mac con Apple Silicon (Gyro-S en un Mac mini de 32 GB: 9 tokens/s, con los expertos leídos del SSD). Detalles y mediciones:
|
||||
[docs/GYRO.md](docs/GYRO.md). Arráncalo con `strata launch --model gyro-s`.
|
||||
Arráncalo con `strata launch --model gyro-s`. Detalles: [docs/GYRO.md](docs/GYRO.md).
|
||||
|
||||
### Los demás tamaños
|
||||
### Los tamaños de Qwen3.8-Flash-Next
|
||||
|
||||
| Tu memoria (RAM + VRAM) | Elige | Notas |
|
||||
| Tu memoria (RAM + VRAM) | Elige | Por qué |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Gyro-S**, o el **[Coder](docs/MODELS.md#coder)** para código | El Coder necesita 32 GB de RAM y conserva la mitad de los expertos, así que es más flojo fuera del código |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), o **Gyro-M** (35 GB) | Swift piensa menos tiempo antes de responder. Gyro-M se acerca a la calidad de IQ3_XXS, pero escribe las respuestas un 16-21 % más despacio que Q2_0 |
|
||||
| **32 GB** | **Gyro-S**, o el **[Coder](docs/MODELS.md#coder)** para código | El Coder conserva los expertos que usa el código |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB) o **Gyro-M** | Swift piensa menos tiempo antes de responder |
|
||||
| **48 GB** | **IQ3_XXS** (47,0 GB) | Más lento, mejor |
|
||||
| **64 GB** | **IQ3_S** (54,8 GB), el original o Swift 1.5 | El más lento, el mejor. Iguala al modelo completo en las pruebas publicadas |
|
||||
| **96 GB o más** | **UD-IQ4_XS** de Unsloth (unos 72 GB de RAM) hasta **Q8_0** (unos 136 GB), de 4 a 8 bits | Más cerca del modelo completo. Con menos RAM, Strata lee una parte desde el SSD, que es mucho más lento |
|
||||
| **64 GB** | **IQ3_S** (54,8 GB) | Iguala al modelo completo en las pruebas publicadas |
|
||||
| **96 GB o más** | **UD-IQ4_XS** hasta **Q8_0** de Unsloth | Lo más cerca del modelo completo |
|
||||
|
||||
Con menos RAM de la que indica una fila también puede funcionar: Strata pasa solo a su
|
||||
[modo de poca RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Espera que sea más lento, y mucho más lento con una
|
||||
tarjeta gráfica pequeña.
|
||||
Con menos RAM de la que indica una fila también puede funcionar, solo que más lento: Strata pasa solo a su
|
||||
[modo de poca RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). También hay:
|
||||
[NVFP4](docs/NVFP4.md) para tarjetas NVIDIA, [Qwen3.6-35B-A3B](docs/QWEN36.md) (más pequeño, varias peticiones a la vez) y
|
||||
[el Uncensored IQ3_XXS de OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs). Todos los tamaños y lo que cabe en cada equipo:
|
||||
[docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
Más opciones:
|
||||
### DeepSeek V4.1-Flash (Terminal, desde 0.2.4)
|
||||
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** es un ajuste fino que piensa menos tiempo antes de responder, con una calidad y un uso de RAM casi iguales.
|
||||
- **[NVFP4](docs/NVFP4.md)** es la versión de 4 bits de NVIDIA para tarjetas NVIDIA. Necesita unos 80 GB de RAM para tener todos los expertos en RAM.
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** es un modelo más pequeño y distinto que lee imágenes y puede atender varias peticiones a la vez.
|
||||
- **[El Uncensored IQ3_XXS de OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** se configura a mano.
|
||||
Un modelo mucho más grande que funciona con una sola tarjeta gráfica de 12 GB y de 46 a 64 GB de RAM. Strata mantiene en
|
||||
memoria las partes más usadas y lee el resto de tu SSD. [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF)
|
||||
hizo los conjuntos. Funciona en tarjetas NVIDIA RTX de las series 30, 40 y 50 y en la Radeon AI PRO R9700. Elígelo en
|
||||
`strata launch`. La aplicación de escritorio todavía no lo ofrece.
|
||||
|
||||
Todos los tamaños, descargas y lo que cabe en cada equipo: [docs/MODELS.md](docs/MODELS.md).
|
||||
| Conjunto | Descarga | Calidad | Escribe código a |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M** (1,75 bits) | 239 GB | El más pequeño, más débil con prosa sencilla | aún sin medir en un PC |
|
||||
| **IQ2_XXS** (2.00 bits) | 257 GB | Buen equilibrio | 10 tokens/s (RTX 3060), 5 (RTX 5070), 48 (4x R9700) |
|
||||
| **IQ2_XS** (2.25 bits) | 274 GB | Lo más cerca del original | 15 tokens/s (una R9700), 80 (4x R9700) |
|
||||
| **IQ4_NL** (tabla Engram) | incluida en cada descarga de arriba | La tabla de consulta que comparten los tres conjuntos | |
|
||||
|
||||
Arráncalo con `strata launch --model ist-das-2.00bpw` (o `ist-das-1.75bpw`, `ist-das-2.25bpw`). Cifras de calidad,
|
||||
velocidades por PC e instalación manual: [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
|
||||
|
||||
## ¿Qué velocidad tiene?
|
||||
|
||||
Medido con una tarjeta gráfica de 12 GB (RTX 5070, Ryzen 5 7600, 64 GB de RAM). Resultados con otras tarjetas:
|
||||
[velocidad de cada tamaño](docs/MODELS.md#how-fast-is-each-size), [resultados de la comunidad](docs/COMMUNITY_BENCHMARKS.md).
|
||||
Medido con una tarjeta gráfica de 12 GB (RTX 5070, 64 GB de RAM). Resultados con otras tarjetas:
|
||||
[velocidad de cada tamaño](docs/MODELS.md#how-fast-is-each-size) y [resultados de la comunidad](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
| Modelo | Las respuestas aparecen a | Lee tu prompt a |
|
||||
| --- | --- | --- |
|
||||
@@ -203,10 +192,7 @@ Medido con una tarjeta gráfica de 12 GB (RTX 5070, Ryzen 5 7600, 64 GB de RAM).
|
||||
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1.620 tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2.180 tokens/s |
|
||||
|
||||
Un token equivale a unos ¾ de palabra, así que 60 tokens por segundo es más rápido de lo que puedes leer. "Lee tu prompt"
|
||||
es lo rápido que Strata procesa un documento largo, código o un historial de chat.
|
||||
|
||||
Medido con una tarjeta AMD de 16 GB (RX 9070 XT, Ryzen 9 3900X, 47 GB de RAM):
|
||||
Con una tarjeta AMD de 16 GB (RX 9070 XT, 47 GB de RAM):
|
||||
|
||||
| Modelo | Las respuestas aparecen a | Lee tu prompt a |
|
||||
| --- | --- | --- |
|
||||
@@ -214,83 +200,58 @@ Medido con una tarjeta AMD de 16 GB (RX 9070 XT, Ryzen 9 3900X, 47 GB de RAM):
|
||||
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1.110 tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1.420 tokens/s |
|
||||
|
||||
Las barras usan la misma escala en las dos tablas. Una tarjeta con más VRAM es más rápida. Una RTX 3090 (24 GB) debería
|
||||
escribir unos 100-140 tokens por segundo. La versión del motor detrás de cada cifra está en [DETAILS.md](docs/DETAILS.md#speed-measured).
|
||||
Un token equivale a unos ¾ de palabra, así que 60 tokens por segundo es más rápido de lo que puedes leer. "Lee tu prompt"
|
||||
es lo rápido que Strata procesa un documento largo, código o un historial de chat.
|
||||
|
||||
## Uso
|
||||
|
||||
### En la aplicación
|
||||
|
||||
La aplicación tiene un **Chat**, un **Monitor** del modelo y de tu GPU, CPU y RAM, una página de **Modelos** y los
|
||||
**Ajustes**. El chat puede trabajar en una carpeta de tu PC. En modo **Manual** pregunta antes de cambiar un archivo o
|
||||
ejecutar un comando. Escribe `/loop 10m <what to do>` para repetir un mensaje cada 10 minutos mientras el chat esté abierto.
|
||||
Un chat, un **Monitor** del modelo y de tu GPU, CPU y RAM, una página de **Modelos** y los **Ajustes**. El chat puede
|
||||
trabajar en una carpeta de tu PC. En modo **Manual** pregunta antes de cambiar un archivo o ejecutar un comando. Puede buscar
|
||||
en la web (DuckDuckGo o Bing, sin cuenta). Las palabras que buscas son lo único que sale de tu PC.
|
||||
|
||||
El chat puede buscar en la web con DuckDuckGo (o Bing), sin cuenta. Puedes añadir una clave de búsqueda en **Ajustes,
|
||||
Herramientas**. La búsqueda es la única función que envía algo fuera de tu PC (las palabras que buscas).
|
||||
|
||||
Para ejecutar dos modelos, activa **Ejecutar varios modelos** y dale a cada modelo su propia tarjeta de la misma marca.
|
||||
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
|
||||
<sub>Un chat en la app de Strata: el agente escribió un pequeño jardín de pagoda de vóxeles en un solo archivo HTML, y la página se abre en la vista previa junto a la conversación</sub></p>
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="La aplicación Strata: un chat donde el agente construyó un pequeño jardín con una pagoda de vóxeles, con la página abierta en la vista previa a la derecha"><br>
|
||||
<sub>El agente escribió un jardín con una pagoda de vóxeles en un solo archivo HTML, y la página se abre en la vista previa junto al chat</sub></p>
|
||||
|
||||
### En la Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="La vista general del monitor de la Terminal de Strata mientras el modelo escribe una respuesta"><br>
|
||||
<sub><code>strata monitor</code> mientras el modelo responde: velocidad, GPU, memoria y las últimas solicitudes</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided screens: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
|
||||
strata launch --plan --model q2_0 # show what would happen; nothing changes
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
|
||||
strata update --check # is there a new engine? (strata update installs it)
|
||||
strata doctor # check the installed engine files
|
||||
strata launch # guided: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve and print the address
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor # watch speed, GPU, memory and requests
|
||||
strata update # get the newest engine
|
||||
strata doctor # check the installed files
|
||||
```
|
||||
|
||||
`strata --help` lo lista todo. Arrancar y servir no descargan nada. Solo lo hacen la instalación, `strata launch` (cuando
|
||||
eliges un modelo) y `strata update`. Para llegar a Strata desde tu móvil u otro PC, usa `strata launch --access lan`. Crea
|
||||
una clave de API y no arranca en una red sin ella. No se abre ningún navegador salvo que añadas `--open`. Guía:
|
||||
[docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). El cliente completo: [docs/TERMINAL.md](docs/TERMINAL.md).
|
||||
Para llegar a Strata desde tu móvil u otro PC, usa `strata launch --access lan`. Crea una clave de API para ti.
|
||||
`strata --help` lo lista todo, y [docs/TERMINAL.md](docs/TERMINAL.md) lo explica.
|
||||
|
||||
### Con agentes de IA
|
||||
### Con agentes de IA y otras aplicaciones
|
||||
|
||||
Un agente instala Strata con el comando de una línea de la opción B y ejecuta
|
||||
`strata launch --model NAME --yes --no-tui`. Los pasos, las opciones y los códigos de salida están en [docs/AI_SETUP.md](docs/AI_SETUP.md),
|
||||
y las reglas del repositorio en [AGENTS.md](AGENTS.md).
|
||||
Un agente instala Strata con el comando de una línea de arriba y ejecuta `strata launch --model NAME --yes --no-tui`. Los
|
||||
pasos, las opciones y los códigos de salida están en [docs/AI_SETUP.md](docs/AI_SETUP.md). Las reglas del repositorio para agentes: [AGENTS.md](AGENTS.md).
|
||||
|
||||
### Conecta tus otras aplicaciones
|
||||
|
||||
| Tipo de aplicación | URL base |
|
||||
| Tu aplicación habla | URL base |
|
||||
| --- | --- |
|
||||
| Compatible con OpenAI (la mayoría de aplicaciones y agentes) | `http://127.0.0.1:8080/v1` (cualquier clave de API, cualquier nombre de modelo) |
|
||||
| API de Anthropic (Claude Code y otros) | `http://127.0.0.1:8080`, sin `/v1`. Estas aplicaciones añaden `/v1/messages` por su cuenta. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| API de OpenAI (la mayoría de aplicaciones y agentes) | `http://127.0.0.1:8080/v1` (cualquier clave de API, cualquier nombre de modelo) |
|
||||
| API de Anthropic (Claude Code) | `http://127.0.0.1:8080`, sin `/v1`. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| API OpenAI Responses (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([configuración](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` muestra los ajustes exactos para OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent y OpenClaw.
|
||||
`strata connect opencode --write` escribe el archivo después de preguntar y guarda una copia de seguridad.
|
||||
|
||||
### Imágenes
|
||||
|
||||
Las imágenes vienen activadas por defecto en los modelos que pueden leerlas. Adjunta o pega una imagen en el chat, o envíala
|
||||
desde tu aplicación. El lector de imágenes funciona en el procesador cuando tu tarjeta gráfica tiene menos de 24 GB (unos
|
||||
segundos por imagen, sin quitar memoria a la tarjeta) y en la tarjeta gráfica a partir de 24 GB. La regla es la misma para
|
||||
NVIDIA y AMD. Puedes desactivar las imágenes por modelo, o elegir el lector con `--vision off`, `--vision gpu` o
|
||||
`--vision cpu`.
|
||||
|
||||
Las cifras detrás de la regla: en una Radeon AI PRO R9700, el lector de la tarjeta tardó 57 ms en una imagen de 260 tokens y
|
||||
el del procesador 3,1 s. En tarjetas de 16 GB (Tesla P100 y RTX A4000, Linux), el lector de la tarjeta costó un 3-5 % de
|
||||
velocidad en texto, porque ocupa unos 1,4 GB que de otro modo usaría el modelo. En un Mac lee la GPU, tenga la memoria que tenga (0,45 s para una imagen de 260 tokens en un M4 de 32 GB, unos 1,2 GB de la memoria compartida);
|
||||
el procesador tarda unos 50 s por imagen, así que solo lee con `--vision cpu`.
|
||||
`strata connect` muestra los ajustes exactos para OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent y OpenClaw,
|
||||
y `strata connect opencode --write` los escribe por ti (con una copia de seguridad).
|
||||
|
||||
### Conviene saber
|
||||
|
||||
- Elige el nivel de razonamiento (**desactivado, bajo, medio o alto**) en el menú del chat o en "reasoning effort" de tu aplicación. Desactivado es lo más rápido.
|
||||
- Strata responde a una petición a la vez por defecto. Para varias a la vez, usa `--parallel N` en `strata launch` o
|
||||
`"parallel": 2` en la configuración ([BATCHING.md](docs/BATCHING.md)). En una tarjeta de 12 GB, esto hace más lenta cada respuesta.
|
||||
- Strata lee entero el primer mensaje de un chat, unos 1 minuto por cada 30.000 tokens. Los mensajes siguientes empiezan en segundos.
|
||||
- Dónde se guardan tus chats y la API completa: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
|
||||
- **Las imágenes** vienen activadas en todos los modelos que pueden leerlas. Adjunta o pega una imagen en el chat, o envíala desde tu aplicación. Detalles: [docs/IMAGES.md](docs/IMAGES.md).
|
||||
- **El razonamiento** puede estar desactivado, bajo, medio o alto, en el menú del chat o en "reasoning effort" de tu aplicación. Desactivado es lo más rápido.
|
||||
- **El primer mensaje** de un chat se lee entero, unos 1 minuto por cada 30.000 tokens. Los mensajes siguientes empiezan en segundos.
|
||||
- **Una petición a la vez** es lo normal. Para varias, usa `--parallel N` ([BATCHING.md](docs/BATCHING.md)). En una tarjeta de 12 GB, esto hace más lenta cada respuesta.
|
||||
|
||||
## Compilar desde el código fuente
|
||||
|
||||
@@ -351,13 +312,13 @@ Los modelos como este suelen ejecutarse en servidores con cientos de gigabytes d
|
||||
- **Tu SSD es el archivo.** Guarda una tabla de consulta de 29 GB. El modelo lee unas pocas filas por palabra, así que no
|
||||
hace falta que esté en la RAM.
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Los 24.576 expertos del modelo: los más usados en la tarjeta gráfica, todos en la RAM, una tabla de consulta en el SSD"></p>
|
||||
|
||||
Un pequeño ayudante también adivina las siguientes palabras. El modelo grande las comprueba todas a la vez y se queda con
|
||||
las correctas. Obtienes la misma respuesta, entre 1,6 y 1,8 veces antes. Los textos largos se leen en trozos grandes, de
|
||||
hasta 8.192 tokens a la vez, a más de 1.000 tokens por segundo.
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Un pequeño ayudante adivina las siguientes palabras; el modelo grande las comprueba todas a la vez y se queda con las correctas"></p>
|
||||
|
||||
La explicación más larga: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte con sus cifras:
|
||||
[los detalles](docs/DETAILS.md#how-it-works) y el [artículo](docs/paper/Strata-Paper.pdf).
|
||||
@@ -366,27 +327,27 @@ La explicación más larga: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada p
|
||||
|
||||
- **Mi PC se congeló la primera vez que arrancó Strata.** Carga el modelo en la RAM. Espera y no cierres la ventana. ¿Sigue
|
||||
congelado después de 10 minutos? Reinicia el PC, cierra otros programas e inténtalo de nuevo, o elige un tamaño más pequeño.
|
||||
- **Se detuvo durante la descarga o la instalación.** Abre de nuevo la aplicación o `strata launch`, o vuelve a ejecutar
|
||||
`START-HERE.bat` (o `./setup.sh`). Continúa donde se quedó.
|
||||
- **Va muy lento, la luz del disco parpadea sin parar, o dice "the engine stopped unexpectedly".** Tu PC no tiene bastante
|
||||
RAM libre. Cierra otros programas (los navegadores gastan mucha) o elige un tamaño más pequeño, como Q2_0 o IQ2_XS.
|
||||
- **Se detuvo durante la descarga o la instalación.** Abre de nuevo la aplicación o `strata launch`. Continúa donde se
|
||||
quedó.
|
||||
- **Va muy lento, o dice "the engine stopped unexpectedly".** Tu PC no tiene bastante RAM libre. Cierra otros programas
|
||||
(los navegadores gastan mucha) o elige un tamaño más pequeño, como Q2_0 o IQ2_XS.
|
||||
- **Dice que no hay motor para este PC.** En la aplicación, abre **Ajustes, Motor** y pulsa **Actualizar motor**. En la
|
||||
Terminal, ejecuta `strata update`. `strata doctor` muestra qué archivos del motor están instalados.
|
||||
Terminal, ejecuta `strata update`.
|
||||
- **Dice que el puerto 8080 ya está en uso.** Strata ya se está ejecutando. Busca su ventana.
|
||||
|
||||
Más problemas y soluciones: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). ¿Sigues atascado? Abre una
|
||||
[incidencia](https://github.com/Niko1221/Strata/issues). En la aplicación, adjunta el archivo de **Ajustes, Motor,
|
||||
Diagnóstico, Ver registros, Guardar registros**. En la Terminal, pega la salida de `strata doctor`. Si lo ejecutas desde el
|
||||
código fuente, adjunta `strata-<model>.log` de la carpeta de Strata. Para un problema de seguridad, avísanos en privado:
|
||||
[SECURITY.md](SECURITY.md).
|
||||
Más soluciones: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). ¿Sigues atascado? Abre una
|
||||
[incidencia](https://github.com/Niko1221/Strata/issues) y adjunta los registros: en la aplicación desde **Ajustes, Motor,
|
||||
Diagnóstico, Guardar registros**, en la Terminal la salida de `strata doctor`. Para un problema de seguridad, avísanos en
|
||||
privado: [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Créditos y licencia
|
||||
|
||||
El modelo es [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), del equipo de Qwen. Lo comprimieron
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5), agentionai (Gyro)
|
||||
y Unsloth. Strata usa partes de [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Todos los créditos:
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata es de código abierto con la [licencia MIT](LICENSE). Algunas
|
||||
partes y todos los modelos tienen sus propias licencias ([cuáles](docs/HOW_IT_WORKS.md#license)).
|
||||
y Unsloth. ISTA-DASLab hizo también los conjuntos de DeepSeek V4.1-Flash. Strata usa partes de
|
||||
[llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Todos los créditos: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits).
|
||||
Strata es de código abierto con la [licencia MIT](LICENSE). Algunas partes y todos los modelos tienen sus propias licencias
|
||||
([cuáles](docs/HOW_IT_WORKS.md#license)).
|
||||
|
||||
## Contribuir
|
||||
|
||||
|
||||
+142
-180
@@ -12,104 +12,91 @@
|
||||
<p align="center">Compatible avec NVIDIA, AMD, Intel · Windows, Linux, Mac · gratuit et open source</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Dernière version"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licence MIT"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="Étoiles GitHub"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=black" alt="Offrez-moi un café"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Un jardin de pagode en voxels écrit par le modèle de Strata, qui tourne dans le navigateur"></a><br>
|
||||
<sub>Un jardin de pagode en voxels, généré en une seule requête sur une RTX 5070 avec Strata (IQ3_S, contexte de 128K) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">vidéo complète (49 s)</a></sub></p>
|
||||
|
||||
Strata fait tourner **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, un modèle de 125 milliards
|
||||
de paramètres qui demande normalement un serveur, sur un PC ordinaire. Il discute, écrit du code, lit des images et
|
||||
fonctionne avec vos applications et vos agents de code. Vos conversations et vos fichiers restent sur votre PC.
|
||||
<p align="center">
|
||||
<a href="#installation"><b>Installation</b></a> ·
|
||||
<a href="#modèles">Modèles</a> ·
|
||||
<a href="#quelle-est-sa-vitesse-">Vitesse</a> ·
|
||||
<a href="#utilisation">Utilisation</a> ·
|
||||
<a href="#comment-ça-marche-">Comment ça marche</a> ·
|
||||
<a href="#un-problème-">Aide</a> ·
|
||||
<a href="#soutenir-strata">Soutenir</a>
|
||||
</p>
|
||||
|
||||
**Nouveau ici ?** Regardez [ce qu'il vous faut](#ce-quil-vous-faut), puis [installez l'application](#option-a--lapplication-strata).
|
||||
Strata fait tourner **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, un modèle de 125 milliards
|
||||
de paramètres qui demande normalement un serveur, sur un PC ordinaire. Depuis la 0.2.4, il fait aussi tourner **DeepSeek
|
||||
V4.1-Flash** dans le Terminal. Il discute, écrit du code, lit des images et fonctionne avec vos applications et vos agents
|
||||
de code. Vos conversations et vos fichiers restent sur votre PC.
|
||||
|
||||
- **Tourne sur un PC de jeu.** 32 Go de RAM et de mémoire graphique réunis suffisent pour commencer.
|
||||
- **Fonctionne avec vos outils.** Il parle les API d'OpenAI et d'Anthropic, donc Claude Code, Codex CLI, OpenCode et d'autres agents peuvent s'y connecter.
|
||||
- **Lit les images.** C'est activé par défaut.
|
||||
- **Gratuit.** Licence MIT, pas de compte, pas de cloud.
|
||||
- **Privé.** Aucune télémétrie. Ce que Strata télécharge, quand il utilise internet et comment il vérifie les téléchargements est expliqué dans [docs/SAFETY.md](docs/SAFETY.md).
|
||||
|
||||
Version 0.2.2 · moteur 0.1.47
|
||||
- **Fonctionne avec vos outils.** Claude Code, Codex CLI, OpenCode et d'autres agents se connectent par les API d'OpenAI et d'Anthropic.
|
||||
- **Lit les images**, activé par défaut.
|
||||
- **Gratuit et privé.** Licence MIT, pas de compte, pas de cloud, aucune télémétrie ([ce qu'il télécharge et quand](docs/SAFETY.md)).
|
||||
|
||||
## Ce qu'il vous faut
|
||||
|
||||
**Il vous faut au moins 32 Go de RAM et de mémoire graphique (VRAM) réunis.** Sur un Mac ou un Ryzen AI Max, c'est la
|
||||
mémoire unifiée. Plus de mémoire permet un modèle plus gros ou plus rapide.
|
||||
**Il vous faut au moins 32 Go de mémoire : RAM et mémoire graphique (VRAM) réunis.** Sur un Mac ou un Ryzen AI Max, c'est
|
||||
la mémoire unifiée. Un PC de 32 Go avec une carte de 12 à 16 Go fait tourner les petites tailles, et 64 Go font tourner
|
||||
toutes les tailles.
|
||||
|
||||
- Un PC de 32 Go avec une carte de 12 à 16 Go fait tourner le Coder.
|
||||
- Avec 64 Go de RAM, toutes les tailles fonctionnent.
|
||||
|
||||
| Votre ordinateur | Détails |
|
||||
| Fonctionne sur | Guide |
|
||||
| --- | --- |
|
||||
| **NVIDIA** GeForce RTX série 20, 30, 40, 50 (12 Go ou plus) | Installez l'application ou le Terminal. |
|
||||
| **AMD** Radeon RX 6800 et plus récent, Radeon AI PRO R9700 (12 Go ou plus) | [Cartes AMD](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [moteur Intel](docs/INTEL.md) |
|
||||
| **Mac Apple Silicon** (M1 ou plus récent, macOS 15 ou plus récent) | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** et autres systèmes à mémoire unifiée | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Cartes et processeurs plus anciens | [GPU anciens](docs/OLDER_GPUS.md), [CPU anciens](docs/INSTALL.md#older-cpus-experimental) |
|
||||
| **NVIDIA** GeForce RTX série 20, 30, 40 et 50, 12 Go ou plus | [Installation](#installation) |
|
||||
| **AMD** Radeon RX 6800 et plus récent, Radeon AI PRO R9700 | [Cartes AMD](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md) |
|
||||
| **Mac Apple Silicon**, M1 ou plus récent, macOS 15 ou plus récent | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** et autres PC à mémoire unifiée | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Cartes et processeurs plus anciens | [GPU anciens](docs/OLDER_GPUS.md) |
|
||||
|
||||
Il faut aussi :
|
||||
|
||||
- environ 80 Go d'espace disque libre (un SSD accélère beaucoup le premier démarrage)
|
||||
- Windows 10 / 11, Linux ou macOS
|
||||
- un pilote graphique récent
|
||||
|
||||
Deux ou trois cartes de la même marque peuvent se partager un modèle ([multi-GPU](docs/MULTI_GPU.md)). Strata ne répartit
|
||||
jamais un modèle entre des cartes NVIDIA et AMD. Liste complète : [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
Il vous faut aussi environ 80 Go d'espace disque libre (un SSD), Windows 10 / 11, Linux ou macOS, et un pilote graphique
|
||||
récent. Deux ou trois cartes de la même marque peuvent se partager un modèle ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
|
||||
## Installation
|
||||
|
||||
Choisissez une option. Elles lancent toutes le même moteur et utilisent les mêmes modèles.
|
||||
### 1. L'application Strata (le plus simple)
|
||||
|
||||
### Option A : l'application Strata
|
||||
Téléchargez le fichier pour votre ordinateur, ouvrez-le et appuyez sur Installer pour le modèle qu'elle recommande.
|
||||
|
||||
Le plus simple. L'application contient le moteur : installez-la, choisissez un modèle, discutez. Téléchargez le fichier
|
||||
correspondant à votre système et à votre carte graphique depuis la [page des versions](https://github.com/Niko1221/Strata/releases/latest).
|
||||
Vous ne savez pas lequel prendre ? Lisez le paragraphe sous le tableau.
|
||||
**Windows 10 / 11**
|
||||
|
||||
| Système | Votre carte graphique | Fichier à télécharger |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, pilote 580 ou plus récent | [`Strata-0.2.2-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda13.exe) |
|
||||
| | NVIDIA, carte plus ancienne ou pilote antérieur à 580 | [`Strata-0.2.2-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda12.exe) |
|
||||
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.2-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip.exe) |
|
||||
| | AMD Radeon série RX 7000 ou Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, pilote 580 ou plus récent | [`Strata-0.2.2-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, carte plus ancienne ou pilote antérieur à 580 | [`Strata-0.2.2-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.2-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon série RX 7000 | [`Strata-0.2.2-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 ou plus récent) | intégrée | [`Strata-0.2.2-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-mac.dmg) |
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-windows-x64-setup.exe)
|
||||
|
||||
**Si vous hésitez :** pour NVIDIA, prenez `cuda13` si la version de votre pilote est 580 ou plus récente, sinon `cuda12`.
|
||||
`cuda12` convient aux cartes plus anciennes et aux pilotes plus anciens. Les fichiers `hip` et `amd` sont pour les cartes
|
||||
AMD. Les autres cartes AMD (RX 6000) n'ont pas encore d'application prête à l'emploi : voir
|
||||
[Cartes AMD](docs/AMD_HIP.md).
|
||||
**Linux**
|
||||
|
||||
Ensuite :
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-linux-x86_64.AppImage)
|
||||
|
||||
- **Windows :** lancez le `.exe`. Il s'installe pour votre utilisateur et ne demande pas de droits d'administrateur. Si
|
||||
Windows affiche « Windows a protégé votre ordinateur », choisissez **Informations complémentaires**, puis **Exécuter
|
||||
quand même** (l'installateur n'est pas encore signé).
|
||||
- **Linux :** rendez le fichier exécutable (`chmod +x`) et lancez-le. Il a besoin de FUSE 2 : `sudo apt install libfuse2`
|
||||
(Ubuntu 22.04) ou `libfuse2t64` (Ubuntu 24.04 et plus récent).
|
||||
- **Mac :** ouvrez le `.dmg` et faites glisser Strata dans Applications.
|
||||
**Mac** (Apple Silicon, macOS 15 ou plus récent)
|
||||
|
||||
Sur le premier écran, l'application analyse votre PC et recommande un modèle adapté. Appuyez sur Installer. Le modèle est un
|
||||
gros téléchargement (environ 40 à 70 Go) qui reprend là où il s'est arrêté en cas d'interruption. Quand vous fermez la
|
||||
fenêtre, Strata et le moteur s'arrêtent.
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-macos-arm64.dmg)
|
||||
|
||||
### Option B : le Terminal (une ligne)
|
||||
L'application choisit toute seule le bon moteur pour votre carte graphique. Tous les fichiers sont sur la
|
||||
[page des versions](https://github.com/Niko1221/Strata/releases/latest).
|
||||
|
||||
Le Terminal est la commande `strata`, pensée pour les scripts, les serveurs et les agents d'IA.
|
||||
| Premier démarrage | Ce qu'il faut faire |
|
||||
| --- | --- |
|
||||
| **Windows** | Lancez le `.exe`. Pas besoin de droits d'administrateur. Si Windows affiche « Windows a protégé votre ordinateur », choisissez **Informations complémentaires**, puis **Exécuter quand même**. |
|
||||
| **Linux** | Rendez le fichier exécutable (`chmod +x`) et lancez-le. Il a besoin de FUSE 2 : `sudo apt install libfuse2t64` (Ubuntu 24.04 et plus récent) ou `libfuse2` (22.04). |
|
||||
| **Mac** | Ouvrez le `.dmg` et faites glisser Strata dans Applications. |
|
||||
|
||||
Le téléchargement du modèle est gros (40 à 70 Go). S'il s'arrête, il reprend là où il s'était arrêté.
|
||||
|
||||
### 2. Le Terminal (une ligne)
|
||||
|
||||
La commande `strata`, pour les scripts, les serveurs et les agents d'IA. C'est aussi le moyen de faire tourner DeepSeek.
|
||||
|
||||
Linux et Mac :
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows (PowerShell) :
|
||||
@@ -118,26 +105,17 @@ Windows (PowerShell) :
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
Le script analyse votre PC, télécharge le Terminal, vérifie son empreinte SHA-256 et le décompresse dans votre dossier
|
||||
personnel. Il ne demande pas de droits d'administrateur. Sous Windows, ouvrez ensuite une nouvelle fenêtre de terminal.
|
||||
Puis lancez Strata :
|
||||
Puis lancez-le. Il pose une question à la fois (modèle, contexte, images, qui peut se connecter), télécharge ce dont il a
|
||||
besoin et commence à servir :
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
Il pose une question à la fois : modèle, contexte, images, qui peut se connecter. Ensuite il télécharge le moteur pour votre
|
||||
carte graphique (signé, et lancé à titre d'essai avant usage) ainsi que le modèle, puis commence à servir. Pour les scripts
|
||||
et les agents d'IA, utilisez des options :
|
||||
Pas besoin de droits d'administrateur. L'installation vérifie l'empreinte SHA-256 de tout ce qu'elle télécharge. Sous
|
||||
Windows, ouvrez une nouvelle fenêtre de terminal après l'installation. Tous les écrans et toutes les options : [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
Pour lire le script d'installation avant de l'exécuter, téléchargez-le d'abord avec `curl -fsSL <address> -o install.sh`,
|
||||
lisez-le, puis lancez `sh install.sh`. Tous les écrans et toutes les options : [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### Option C : laissez votre IA l'installer
|
||||
### 3. Laissez votre IA l'installer
|
||||
|
||||
Collez ceci dans Claude Code, Cursor, Codex, GitHub Copilot ou un autre assistant de programmation :
|
||||
|
||||
@@ -153,48 +131,58 @@ Vous préférez le compiler vous-même ? Voir [Compiler depuis les sources](#com
|
||||
|
||||
## Modèles
|
||||
|
||||
L'application et `strata launch` recommandent un modèle selon votre mémoire. C'est un seul modèle en plusieurs tailles,
|
||||
plus ou moins compressé. Les petites tailles sont plus rapides. Les grandes sont un peu plus intelligentes.
|
||||
L'application et `strata launch` recommandent une taille selon votre mémoire. Les petites tailles sont plus rapides. Les
|
||||
grandes sont un peu plus intelligentes.
|
||||
|
||||
### Peu de mémoire ? Essayez Gyro
|
||||
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (par agentionai) est la façon la plus légère de faire tourner ce modèle.
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) d'agentionai est la façon la plus légère de faire tourner
|
||||
Qwen3.8-Flash-Next. Il fonctionne sur NVIDIA, AMD, Intel Arc et Apple Silicon.
|
||||
|
||||
| Taille | Mémoire nécessaire (RAM + VRAM) | Téléchargement | Ce que c'est |
|
||||
| Taille | Mémoire nécessaire (RAM + VRAM) | Téléchargement | Pour quoi |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27,6 Go | 58,5 Go | Pour les PC où rien d'autre ne tient. La qualité est inférieure à celle d'IQ2_XS. |
|
||||
| **Gyro-M** | 35 Go | 92,0 Go | À peu près la qualité d'IQ3_XXS avec environ 20 % de mémoire GPU en moins. Il écrit les réponses 16 % plus lentement que Q2_0 sur une RTX 3060 de 12 Go et 21 % plus lentement sur une R9700. |
|
||||
| **Gyro-S** | 27,6 Go | 58,5 Go | Les PC où rien d'autre ne tient. 9 tokens/s sur un Mac mini de 32 Go. |
|
||||
| **Gyro-M** | 35 Go | 92,0 Go | À peu près la qualité d'IQ3_XXS avec 20 % de mémoire GPU en moins. |
|
||||
|
||||
Gyro fonctionne sur les cartes NVIDIA et AMD (expérimental sur AMD) sur les cartes Intel Arc sous Linux (testé sur une Arc A750) et, depuis la 0.2.2, sur les Mac Apple Silicon (Gyro-S sur un Mac mini de 32 Go : 9 tokens/s, experts lus sur le SSD). Détails et mesures :
|
||||
[docs/GYRO.md](docs/GYRO.md). Lancez-le avec `strata launch --model gyro-s`.
|
||||
Lancez-le avec `strata launch --model gyro-s`. Détails : [docs/GYRO.md](docs/GYRO.md).
|
||||
|
||||
### Les autres tailles
|
||||
### Les tailles de Qwen3.8-Flash-Next
|
||||
|
||||
| Votre mémoire (RAM + VRAM) | Prenez | Remarques |
|
||||
| Votre mémoire (RAM + VRAM) | Prenez | Pourquoi |
|
||||
| --- | --- | --- |
|
||||
| **32 Go** | **Gyro-S**, ou le **[Coder](docs/MODELS.md#coder)** pour le code | Le Coder demande 32 Go de RAM et garde la moitié des experts, il est donc moins bon hors du code |
|
||||
| **40 Go** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 Go), ou **Gyro-M** (35 Go) | Swift réfléchit moins longtemps avant de répondre. Gyro-M approche la qualité d'IQ3_XXS, mais il écrit les réponses 16 à 21 % plus lentement que Q2_0 |
|
||||
| **32 Go** | **Gyro-S**, ou le **[Coder](docs/MODELS.md#coder)** pour le code | Le Coder garde les experts dont le code a besoin |
|
||||
| **40 Go** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 Go) ou **Gyro-M** | Swift réfléchit moins longtemps avant de répondre |
|
||||
| **48 Go** | **IQ3_XXS** (47,0 Go) | Plus lent, meilleur |
|
||||
| **64 Go** | **IQ3_S** (54,8 Go), l'original ou Swift 1.5 | Le plus lent, le meilleur. Il égale le modèle complet dans les tests publiés |
|
||||
| **96 Go ou plus** | **UD-IQ4_XS** d'Unsloth (environ 72 Go de RAM) jusqu'à **Q8_0** (environ 136 Go), 4 à 8 bits | Plus proche du modèle complet. Avec moins de RAM, Strata lit une partie depuis le SSD, ce qui est beaucoup plus lent |
|
||||
| **64 Go** | **IQ3_S** (54,8 Go) | Égale le modèle complet dans les tests publiés |
|
||||
| **96 Go ou plus** | **UD-IQ4_XS** à **Q8_0** d'Unsloth | Au plus près du modèle complet |
|
||||
|
||||
Avoir moins de RAM que ce qu'indique une ligne peut encore fonctionner : Strata passe tout seul en
|
||||
[mode peu de RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Attendez-vous à plus de lenteur, et à bien plus avec
|
||||
une petite carte graphique.
|
||||
Avoir moins de RAM que ce qu'indique une ligne peut encore fonctionner, en plus lent : Strata passe tout seul en
|
||||
[mode peu de RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Il existe aussi :
|
||||
[NVFP4](docs/NVFP4.md) pour les cartes NVIDIA, [Qwen3.6-35B-A3B](docs/QWEN36.md) (plus petit, plusieurs requêtes à la fois) et
|
||||
[l'Uncensored IQ3_XXS d'OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs). Toutes les tailles et ce qui tient où :
|
||||
[docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
Autres choix :
|
||||
### DeepSeek V4.1-Flash (Terminal, depuis 0.2.4)
|
||||
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** est une version affinée qui réfléchit moins longtemps avant de répondre, pour une qualité et un besoin en RAM à peu près identiques.
|
||||
- **[NVFP4](docs/NVFP4.md)** est la version 4 bits de NVIDIA pour les cartes NVIDIA. Elle demande environ 80 Go de RAM pour garder tous les experts en RAM.
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** est un modèle plus petit et différent, qui lit les images et peut répondre à plusieurs requêtes à la fois.
|
||||
- **[L'Uncensored IQ3_XXS d'OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** se configure à la main.
|
||||
Un modèle bien plus gros, qui tourne sur une seule carte graphique de 12 Go avec 46 à 64 Go de RAM. Strata garde en mémoire
|
||||
les parties les plus utilisées et lit le reste sur votre SSD. [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF)
|
||||
a produit les versions. Il fonctionne sur les cartes NVIDIA RTX des séries 30, 40 et 50 et sur la Radeon AI PRO R9700. Choisissez-le
|
||||
dans `strata launch`. L'application de bureau ne le propose pas encore.
|
||||
|
||||
Toutes les tailles, les téléchargements et ce qui tient où : [docs/MODELS.md](docs/MODELS.md).
|
||||
| Version | Téléchargement | Qualité | Écrit du code à |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M** (1,75 bit) | 239 Go | La plus petite, plus faible sur la prose simple | pas encore mesuré sur un PC |
|
||||
| **IQ2_XXS** (2.00 bits) | 257 Go | Bon équilibre | 10 tokens/s (RTX 3060), 5 (RTX 5070), 48 (4x R9700) |
|
||||
| **IQ2_XS** (2.25 bits) | 274 Go | Au plus près de l'original | 15 tokens/s (une R9700), 80 (4x R9700) |
|
||||
| **IQ4_NL** (table Engram) | compris dans chaque téléchargement ci-dessus | La table de consultation que les trois versions partagent | |
|
||||
|
||||
Lancez-le avec `strata launch --model ist-das-2.00bpw` (ou `ist-das-1.75bpw`, `ist-das-2.25bpw`). Chiffres de qualité,
|
||||
vitesses par PC et installation manuelle : [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
|
||||
|
||||
## Quelle est sa vitesse ?
|
||||
|
||||
Mesuré avec une carte graphique de 12 Go (RTX 5070, Ryzen 5 7600, 64 Go de RAM). Voir les résultats d'autres cartes :
|
||||
[vitesse de chaque taille](docs/MODELS.md#how-fast-is-each-size), [résultats de la communauté](docs/COMMUNITY_BENCHMARKS.md).
|
||||
Mesuré avec une carte graphique de 12 Go (RTX 5070, 64 Go de RAM). Voir les résultats d'autres cartes :
|
||||
[vitesse de chaque taille](docs/MODELS.md#how-fast-is-each-size) et [résultats de la communauté](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
| Modèle | Les réponses s'affichent à | Lit votre prompt à |
|
||||
| --- | --- | --- |
|
||||
@@ -204,10 +192,7 @@ Mesuré avec une carte graphique de 12 Go (RTX 5070, Ryzen 5 7600, 64 Go de RAM)
|
||||
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1 620 tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2 180 tokens/s |
|
||||
|
||||
Un token vaut environ ¾ de mot, donc 60 tokens par seconde, c'est plus rapide que votre lecture. « Lit votre prompt »
|
||||
indique la vitesse à laquelle Strata absorbe un long document, du code ou un historique de discussion.
|
||||
|
||||
Mesuré avec une carte AMD de 16 Go (RX 9070 XT, Ryzen 9 3900X, 47 Go de RAM) :
|
||||
Avec une carte AMD de 16 Go (RX 9070 XT, 47 Go de RAM) :
|
||||
|
||||
| Modèle | Les réponses s'affichent à | Lit votre prompt à |
|
||||
| --- | --- | --- |
|
||||
@@ -215,84 +200,59 @@ Mesuré avec une carte AMD de 16 Go (RX 9070 XT, Ryzen 9 3900X, 47 Go de RAM) :
|
||||
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1 110 tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1 420 tokens/s |
|
||||
|
||||
Les barres utilisent la même échelle dans les deux tableaux. Une carte avec plus de VRAM est plus rapide. Une RTX 3090
|
||||
(24 Go) devrait écrire environ 100 à 140 tokens par seconde. La version du moteur derrière chaque chiffre est indiquée dans
|
||||
[DETAILS.md](docs/DETAILS.md#speed-measured).
|
||||
Un token vaut environ ¾ de mot, donc 60 tokens par seconde, c'est plus rapide que votre lecture. « Lit votre prompt »
|
||||
indique la vitesse à laquelle Strata absorbe un long document, du code ou un historique de discussion.
|
||||
|
||||
## Utilisation
|
||||
|
||||
### Dans l'application
|
||||
|
||||
L'application comprend un **Chat**, un **Monitor** du modèle, de votre GPU, de votre CPU et de votre RAM, une page **Models** et les **Settings**. Le chat peut travailler dans un dossier de votre PC. En mode **Manual**, il demande
|
||||
avant de modifier un fichier ou d'exécuter une commande. Tapez `/loop 10m <what to do>` pour répéter un message toutes les
|
||||
10 minutes tant que le chat est ouvert.
|
||||
Un chat, un **Monitor** du modèle et de votre GPU, CPU et RAM, une page **Models** et les **Settings**. Le chat peut
|
||||
travailler dans un dossier de votre PC. En mode **Manual**, il demande avant de modifier un fichier ou d'exécuter une
|
||||
commande. Il peut chercher sur le web (DuckDuckGo ou Bing, sans compte). Les mots recherchés sont la seule chose qui quitte
|
||||
votre PC.
|
||||
|
||||
Le chat peut chercher sur le web avec DuckDuckGo (ou Bing), sans compte. Vous pouvez ajouter une clé de recherche sous
|
||||
**Settings, Tools**. La recherche est la seule fonction qui envoie quelque chose hors de votre PC (les mots recherchés).
|
||||
|
||||
Pour faire tourner deux modèles, activez **Run multiple models** (exécuter plusieurs modèles) et donnez à chacun sa propre carte de la même marque.
|
||||
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
|
||||
<sub>Une discussion dans l'app Strata : l'agent a écrit un petit jardin de pagode en voxels dans un seul fichier HTML, et la page s'ouvre dans l'aperçu à côté de la conversation</sub></p>
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="L'application Strata : un chat où l'agent a construit un petit jardin de pagode en voxels, avec la page ouverte dans l'aperçu à droite"><br>
|
||||
<sub>L'agent a écrit un jardin de pagode en voxels dans un seul fichier HTML, et la page s'ouvre dans l'aperçu à côté du chat</sub></p>
|
||||
|
||||
### Dans le Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="La vue d'ensemble du moniteur du Terminal Strata pendant que le modèle écrit une réponse"><br>
|
||||
<sub><code>strata monitor</code> pendant que le modèle répond : vitesse, GPU, mémoire et les dernières requêtes</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided screens: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
|
||||
strata launch --plan --model q2_0 # show what would happen; nothing changes
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
|
||||
strata update --check # is there a new engine? (strata update installs it)
|
||||
strata doctor # check the installed engine files
|
||||
strata launch # guided: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve and print the address
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor # watch speed, GPU, memory and requests
|
||||
strata update # get the newest engine
|
||||
strata doctor # check the installed files
|
||||
```
|
||||
|
||||
`strata --help` liste tout. Démarrer et servir ne téléchargent rien. Seuls l'installation, `strata launch` (quand vous
|
||||
choisissez un modèle) et `strata update` le font. Pour joindre Strata depuis votre téléphone ou un autre PC, utilisez
|
||||
`strata launch --access lan`. La commande crée une clé d'API et refuse de démarrer sur un réseau sans elle. Aucun
|
||||
navigateur ne s'ouvre, sauf si vous ajoutez `--open`. Guide : [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Le client complet : [docs/TERMINAL.md](docs/TERMINAL.md).
|
||||
Pour joindre Strata depuis votre téléphone ou un autre PC, utilisez `strata launch --access lan`. La commande crée une clé
|
||||
d'API pour vous. `strata --help` liste tout, et [docs/TERMINAL.md](docs/TERMINAL.md) l'explique.
|
||||
|
||||
### Avec des agents d'IA
|
||||
### Avec des agents d'IA et d'autres applications
|
||||
|
||||
Un agent installe Strata avec la commande en une ligne de l'option B et lance
|
||||
`strata launch --model NAME --yes --no-tui`. Les étapes, les options et les codes de sortie sont dans [docs/AI_SETUP.md](docs/AI_SETUP.md),
|
||||
et les règles du dépôt dans [AGENTS.md](AGENTS.md).
|
||||
Un agent installe Strata avec la commande en une ligne ci-dessus et lance `strata launch --model NAME --yes --no-tui`. Les
|
||||
étapes, les options et les codes de sortie sont dans [docs/AI_SETUP.md](docs/AI_SETUP.md). Les règles du dépôt pour les agents : [AGENTS.md](AGENTS.md).
|
||||
|
||||
### Connecter vos autres applications
|
||||
|
||||
| Type d'application | URL de base |
|
||||
| Votre application parle | URL de base |
|
||||
| --- | --- |
|
||||
| Compatible OpenAI (la plupart des applications et des agents) | `http://127.0.0.1:8080/v1` (n'importe quelle clé d'API, n'importe quel nom de modèle) |
|
||||
| API Anthropic (Claude Code et d'autres) | `http://127.0.0.1:8080`, sans `/v1`. Ces applications ajoutent `/v1/messages` elles-mêmes. Claude Code : `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| API OpenAI (la plupart des applications et des agents) | `http://127.0.0.1:8080/v1` (n'importe quelle clé d'API, n'importe quel nom de modèle) |
|
||||
| API Anthropic (Claude Code) | `http://127.0.0.1:8080`, sans `/v1`. Claude Code : `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| API OpenAI Responses (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([configuration](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` affiche les réglages exacts pour OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent et OpenClaw.
|
||||
`strata connect opencode --write` écrit le fichier après confirmation et garde une sauvegarde.
|
||||
|
||||
### Images
|
||||
|
||||
Les images sont activées par défaut pour les modèles qui savent les lire. Joignez ou collez une image dans le chat, ou
|
||||
envoyez-la depuis votre application. Le lecteur d'images tourne sur le processeur quand votre carte graphique a moins de
|
||||
24 Go (quelques secondes par image, sans prendre de mémoire à la carte) et sur la carte graphique à partir de 24 Go. La
|
||||
règle est la même pour NVIDIA et AMD. Vous pouvez désactiver les images par modèle, ou choisir le lecteur avec
|
||||
`--vision off`, `--vision gpu` ou `--vision cpu`.
|
||||
|
||||
Les chiffres derrière la règle : sur une Radeon AI PRO R9700, le lecteur de la carte a pris 57 ms pour une image de
|
||||
260 tokens et celui du processeur 3,1 s. Sur des cartes de 16 Go (Tesla P100 et RTX A4000, Linux), le lecteur de la carte
|
||||
a coûté 3 à 5 % de vitesse sur le texte, parce qu'il occupe environ 1,4 Go que le modèle utiliserait sinon. Sur un Mac, le lecteur tourne sur le GPU quelle que soit la mémoire (0,45 s pour une image de 260 tokens sur un M4 de 32 Go, environ 1,2 Go de la
|
||||
mémoire partagée) ; le processeur met environ 50 s par image, il ne lit donc qu'avec `--vision cpu`.
|
||||
`strata connect` affiche les réglages exacts pour OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent et OpenClaw,
|
||||
et `strata connect opencode --write` les écrit pour vous (avec une sauvegarde).
|
||||
|
||||
### Bon à savoir
|
||||
|
||||
- Choisissez le niveau de réflexion (**désactivé, faible, moyen ou élevé**) dans le menu du chat ou dans le réglage « reasoning effort » de votre application. Désactivé est le plus rapide.
|
||||
- Strata répond à une requête à la fois par défaut. Pour en traiter plusieurs en même temps, utilisez `--parallel N` dans `strata launch` ou
|
||||
`"parallel": 2` dans la configuration ([BATCHING.md](docs/BATCHING.md)). Sur une carte de 12 Go, chaque réponse devient alors plus lente.
|
||||
- Strata lit en entier le premier message d'une discussion, soit environ 1 minute pour 30 000 tokens. Les messages suivants démarrent en quelques secondes.
|
||||
- Où sont stockées vos discussions et l'API complète : [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
|
||||
- **Les images** sont activées pour tous les modèles qui savent les lire. Joignez ou collez une image dans le chat, ou envoyez-la depuis votre application. Détails : [docs/IMAGES.md](docs/IMAGES.md).
|
||||
- **La réflexion** peut être désactivée, faible, moyenne ou élevée, dans le menu du chat ou dans le réglage « reasoning effort » de votre application. Désactivée est le plus rapide.
|
||||
- **Le premier message** d'une discussion est lu en entier, environ 1 minute pour 30 000 tokens. Les messages suivants démarrent en quelques secondes.
|
||||
- **Une requête à la fois** par défaut. Pour en traiter plusieurs, utilisez `--parallel N` ([BATCHING.md](docs/BATCHING.md)). Sur une carte de 12 Go, chaque réponse devient alors plus lente.
|
||||
|
||||
## Compiler depuis les sources
|
||||
|
||||
@@ -355,13 +315,13 @@ bibliothèque.
|
||||
- **Votre SSD, ce sont les archives.** Il contient une table de consultation de 29 Go. Le modèle n'en lit que quelques lignes par
|
||||
mot, elle n'a donc pas besoin de tenir en RAM.
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Les 24 576 experts du modèle : les plus sollicités sur la carte graphique, tous dans la RAM, une table de consultation sur le SSD"></p>
|
||||
|
||||
Un petit assistant devine aussi les mots suivants. Le grand modèle les vérifie tous d'un coup et garde les bons. Vous
|
||||
obtenez la même réponse, 1,6 à 1,8 fois plus tôt. Les textes longs sont lus par gros morceaux, jusqu'à 8 192 tokens à la
|
||||
fois, à plus de 1 000 tokens par seconde.
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Un petit assistant devine les mots suivants ; le grand modèle les vérifie tous d'un coup et garde les bons"></p>
|
||||
|
||||
L'explication plus longue : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Chaque élément avec ses chiffres :
|
||||
[les détails](docs/DETAILS.md#how-it-works) et l'[article](docs/paper/Strata-Paper.pdf).
|
||||
@@ -370,25 +330,27 @@ L'explication plus longue : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Chaque
|
||||
|
||||
- **Mon PC s'est figé au premier démarrage de Strata.** Il charge le modèle en RAM. Patientez et ne fermez pas la fenêtre.
|
||||
Toujours figé après 10 minutes ? Redémarrez le PC, fermez les autres programmes et réessayez, ou choisissez une taille plus petite.
|
||||
- **Ça s'est arrêté pendant le téléchargement ou l'installation.** Relancez l'application ou `strata launch`, ou relancez
|
||||
`START-HERE.bat` (ou `./setup.sh`). Cela reprend là où ça s'était arrêté.
|
||||
- **C'est très lent, le voyant du disque clignote sans arrêt, ou il affiche « the engine stopped unexpectedly ».** Votre PC
|
||||
n'a pas assez de RAM libre. Fermez d'autres programmes (les navigateurs en consomment beaucoup) ou choisissez une taille plus petite, comme Q2_0 ou IQ2_XS.
|
||||
- **Il indique qu'il n'y a pas de moteur pour ce PC.** Dans l'application, ouvrez **Settings, Engine** et appuyez sur **Update engine** (mettre à jour le moteur). Dans le Terminal, lancez `strata update`. `strata doctor` montre quels fichiers du moteur sont installés.
|
||||
- **Ça s'est arrêté pendant le téléchargement ou l'installation.** Relancez l'application ou `strata launch`. Cela reprend là
|
||||
où ça s'était arrêté.
|
||||
- **C'est très lent, ou il affiche « the engine stopped unexpectedly ».** Votre PC n'a pas assez de RAM libre. Fermez
|
||||
d'autres programmes (les navigateurs en consomment beaucoup) ou choisissez une taille plus petite, comme Q2_0 ou IQ2_XS.
|
||||
- **Il indique qu'il n'y a pas de moteur pour ce PC.** Dans l'application, ouvrez **Settings, Engine** et appuyez sur **Update engine**
|
||||
(mettre à jour le moteur). Dans le Terminal, lancez `strata update`.
|
||||
- **Il indique que le port 8080 est déjà utilisé.** Strata tourne déjà. Cherchez sa fenêtre.
|
||||
|
||||
Autres problèmes et solutions : [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Toujours bloqué ? Ouvrez une
|
||||
[issue](https://github.com/Niko1221/Strata/issues). Dans l'application, joignez le fichier obtenu par **Settings, Engine, Diagnostics, View logs, Save logs**. Dans le Terminal, collez la sortie de `strata doctor`. Si
|
||||
vous avez lancé depuis les sources, joignez `strata-<model>.log` du dossier Strata. Pour un problème de sécurité,
|
||||
signalez-le en privé : [SECURITY.md](SECURITY.md).
|
||||
Autres solutions : [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Toujours bloqué ? Ouvrez une
|
||||
[issue](https://github.com/Niko1221/Strata/issues) et joignez les journaux : dans l'application via **Settings, Engine,
|
||||
Diagnostics, Save logs**, dans le Terminal la sortie de `strata doctor`. Pour un problème de sécurité, signalez-le en
|
||||
privé : [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Remerciements et licence
|
||||
|
||||
Le modèle est [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), de l'équipe Qwen. Il a été compressé par
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5), agentionai (Gyro)
|
||||
et Unsloth. Strata utilise des parties de [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Tous les
|
||||
remerciements : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata est open source sous [licence MIT](LICENSE).
|
||||
Quelques parties et tous les modèles ont leurs propres licences ([lesquelles](docs/HOW_IT_WORKS.md#license)).
|
||||
et Unsloth. ISTA-DASLab a aussi produit les versions de DeepSeek V4.1-Flash. Strata utilise des parties de
|
||||
[llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Tous les remerciements : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits).
|
||||
Strata est open source sous [licence MIT](LICENSE). Quelques parties et tous les modèles ont leurs propres licences
|
||||
([lesquelles](docs/HOW_IT_WORKS.md#license)).
|
||||
|
||||
## Contribuer
|
||||
|
||||
|
||||
+125
-160
@@ -12,100 +12,89 @@
|
||||
<p align="center">NVIDIA、AMD、Intel に対応 · Windows、Linux、Mac · 無料のオープンソース</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="最新リリース"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT ライセンス"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub のスター"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=black" alt="コーヒーをおごる"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Strata のモデルが書いたボクセルの塔の庭園。ブラウザで動いています"></a><br>
|
||||
<sub>ボクセルの塔の庭園。1回のプロンプトで生成し、RTX 5070 上の Strata で動かしています(IQ3_S、128K コンテキスト) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">動画全体(49秒)</a></sub></p>
|
||||
|
||||
Strata は **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** を動かします。1250億パラメータのモデルで、
|
||||
ふつうはサーバーが必要ですが、Strata なら一般的なPCで使えます。チャット、コード作成、画像の読み取りができ、
|
||||
お使いのアプリやコーディングエージェントとも連携します。チャットとファイルはPCの外に出ません。
|
||||
<p align="center">
|
||||
<a href="#インストール"><b>インストール</b></a> ·
|
||||
<a href="#モデル">モデル</a> ·
|
||||
<a href="#どのくらい速い">速度</a> ·
|
||||
<a href="#使い方">使い方</a> ·
|
||||
<a href="#仕組み">仕組み</a> ·
|
||||
<a href="#うまくいかないとき">ヘルプ</a> ·
|
||||
<a href="#strata-を応援する">応援する</a>
|
||||
</p>
|
||||
|
||||
**はじめての方へ。** まず[必要な環境](#必要な環境)を確認して、[アプリをインストール](#方法a-strata-アプリ)してください。
|
||||
Strata は **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** を動かします。1250億パラメータのモデルで、
|
||||
ふつうはサーバーが必要ですが、Strata なら一般的なPCで使えます。0.2.4 からは、ターミナル版で **DeepSeek V4.1-Flash** も動かせます。
|
||||
チャット、コード作成、画像の読み取りができ、お使いのアプリやコーディングエージェントとも連携します。チャットとファイルはPCの外に出ません。
|
||||
|
||||
- **ゲーミングPCで動きます。** RAMとグラフィックスメモリの合計が32 GBあれば始められます。
|
||||
- **お使いのツールと連携します。** OpenAI と Anthropic の API に対応しているので、Claude Code、Codex CLI、OpenCode などのエージェントから接続できます。
|
||||
- **お使いのツールと連携します。** Claude Code、Codex CLI、OpenCode などのエージェントが、OpenAI と Anthropic の API で接続できます。
|
||||
- **画像を読めます。** 初期設定でオンです。
|
||||
- **無料です。** MIT ライセンス。アカウントもクラウドも不要です。
|
||||
- **プライバシーを守ります。** テレメトリはありません。Strata が何をダウンロードするか、いつインターネットを使うか、ダウンロードをどう検証するかは [docs/SAFETY.md](docs/SAFETY.md) に書いてあります。
|
||||
|
||||
バージョン 0.2.2 · エンジン 0.1.47
|
||||
- **無料でプライバシーも守ります。** MIT ライセンス。アカウントもクラウドもテレメトリも不要です([何をいつダウンロードするか](docs/SAFETY.md))。
|
||||
|
||||
## 必要な環境
|
||||
|
||||
**RAMとグラフィックスメモリ(VRAM)の合計が、少なくとも32 GB必要です。** MacやRyzen AI Maxでは、ユニファイドメモリがその量にあたります。
|
||||
メモリが多いほど、大きなモデルを使えたり、速く動いたりします。
|
||||
RAM 32 GBのPCに12〜16 GBのカードがあれば小さいサイズが動き、64 GBならどのサイズも動きます。
|
||||
|
||||
- RAM 32 GBのPCに12〜16 GBのグラフィックスカードがあれば、Coder が動きます。
|
||||
- RAM 64 GBなら、どのサイズも動きます。
|
||||
|
||||
| お使いのPC | 詳細 |
|
||||
| 動く環境 | ガイド |
|
||||
| --- | --- |
|
||||
| **NVIDIA** GeForce RTX 20、30、40、50 シリーズ(12 GB以上) | アプリまたはターミナル版をインストールしてください。 |
|
||||
| **AMD** Radeon RX 6800 以降、Radeon AI PRO R9700(12 GB以上) | [AMD のカード](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md)、[Intel エンジン](docs/INTEL.md) |
|
||||
| **Apple Silicon の Mac**(M1 以降、macOS 15 以降) | [Mac](docs/MAC.md) |
|
||||
| **NVIDIA** GeForce RTX 20、30、40、50 シリーズ、12 GB以上 | [インストール](#インストール) |
|
||||
| **AMD** Radeon RX 6800 以降、Radeon AI PRO R9700 | [AMD のカード](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md) |
|
||||
| **Apple Silicon の Mac**、M1 以降、macOS 15 以降 | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max(Strix Halo)** などのユニファイドメモリ搭載機 | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| 古いカードやプロセッサ | [古いGPU](docs/OLDER_GPUS.md)、[古いCPU](docs/INSTALL.md#older-cpus-experimental) |
|
||||
| 古いカードやプロセッサ | [古いGPU](docs/OLDER_GPUS.md) |
|
||||
|
||||
そのほかに必要なもの:
|
||||
|
||||
- 約80 GBの空きディスク容量(SSDなら初回の起動がずっと速くなります)
|
||||
- Windows 10 / 11、Linux、または macOS
|
||||
- 最新のグラフィックスドライバー
|
||||
|
||||
同じメーカーのカードを2〜3枚使って、1つのモデルを動かせます([マルチGPU](docs/MULTI_GPU.md))。NVIDIA と AMD のカードにまたがってモデルを分けることはありません。
|
||||
一覧は [docs/INSTALL.md](docs/INSTALL.md#what-you-need) にあります。
|
||||
そのほかに、約80 GBの空きディスク容量(SSD)、Windows 10 / 11、Linux、または macOS、最新のグラフィックスドライバーが必要です。
|
||||
同じメーカーのカードを2〜3枚使って、1つのモデルを動かせます([マルチGPU](docs/MULTI_GPU.md))。
|
||||
|
||||
## インストール
|
||||
|
||||
3つの方法から1つ選んでください。どれも同じエンジンを起動し、同じモデルを使います。
|
||||
### 1. Strata アプリ(いちばん簡単)
|
||||
|
||||
### 方法A: Strata アプリ
|
||||
お使いのPCに合うファイルをダウンロードして開き、すすめられたモデルのインストールを押します。
|
||||
|
||||
いちばん簡単な方法です。エンジンはアプリに入っているので、インストールしてモデルを選べばチャットを始められます。お使いのOSとグラフィックスカードに合うファイルを
|
||||
[リリースページ](https://github.com/Niko1221/Strata/releases/latest)からダウンロードしてください。どれか分からない場合は、表の下の説明を見てください。
|
||||
**Windows 10 / 11**
|
||||
|
||||
| OS | グラフィックスカード | ダウンロードするファイル |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX、ドライバー 580 以降 | [`Strata-0.2.2-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda13.exe) |
|
||||
| | NVIDIA、古いカード、またはドライバーが 580 より古い場合 | [`Strata-0.2.2-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda12.exe) |
|
||||
| | AMD Radeon RX 9000 シリーズまたは Radeon AI PRO R9700 | [`Strata-0.2.2-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip.exe) |
|
||||
| | AMD Radeon RX 7000 シリーズまたは Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX、ドライバー 580 以降 | [`Strata-0.2.2-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA、古いカード、またはドライバーが 580 より古い場合 | [`Strata-0.2.2-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon RX 9000 シリーズまたは Radeon AI PRO R9700 | [`Strata-0.2.2-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon RX 7000 シリーズ | [`Strata-0.2.2-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac**(Apple Silicon、macOS 15 以降) | 内蔵 | [`Strata-0.2.2-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-mac.dmg) |
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-windows-x64-setup.exe)
|
||||
|
||||
**迷ったときは:** NVIDIA の場合、ドライバーがバージョン 580 以降なら `cuda13`、それより古ければ `cuda12` を選びます。`cuda12` は古いカードと古いドライバー向けです。
|
||||
`hip` と `amd` のファイルは AMD のカード用です。それ以外の AMD カード(RX 6000)には、まだ用意されたアプリがありません。
|
||||
[AMD のカード](docs/AMD_HIP.md)を見てください。
|
||||
**Linux**
|
||||
|
||||
そのあとの手順:
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-linux-x86_64.AppImage)
|
||||
|
||||
- **Windows:** `.exe` を実行します。ユーザー単位でインストールされ、管理者権限は不要です。「Windows によってPCが保護されました」と表示されたら、
|
||||
**詳細情報**を選び、**実行**を選んでください(インストーラーにはまだコード署名がありません)。
|
||||
- **Linux:** ファイルを実行可能にして(`chmod +x`)起動します。FUSE 2 が必要です。`sudo apt install libfuse2`(Ubuntu 22.04)
|
||||
または `libfuse2t64`(Ubuntu 24.04 以降)を実行してください。
|
||||
- **Mac:** `.dmg` を開き、Strata を「アプリケーション」にドラッグします。
|
||||
**Mac**(Apple Silicon、macOS 15 以降)
|
||||
|
||||
最初の画面で、アプリがPCを調べて合うモデルをすすめます。インストールを押してください。モデルは大きく(約40〜70 GB)、中断しても続きからダウンロードできます。
|
||||
ウィンドウを閉じると、Strata とエンジンが止まります。
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-macos-arm64.dmg)
|
||||
|
||||
### 方法B: ターミナル(1行)
|
||||
お使いのグラフィックスカードに合うエンジンは、アプリが自動で選びます。すべてのファイルは
|
||||
[リリースページ](https://github.com/Niko1221/Strata/releases/latest)にあります。
|
||||
|
||||
ターミナル版は `strata` コマンドです。スクリプトやサーバー、AI エージェント向けです。
|
||||
| 初回の起動 | すること |
|
||||
| --- | --- |
|
||||
| **Windows** | `.exe` を実行します。管理者権限は不要です。「Windows によってPCが保護されました」と表示されたら、**詳細情報**を選び、**実行**を選んでください。 |
|
||||
| **Linux** | ファイルを実行可能にして(`chmod +x`)起動します。FUSE 2 が必要です。`sudo apt install libfuse2t64`(Ubuntu 24.04 以降)または `libfuse2`(22.04)を実行してください。 |
|
||||
| **Mac** | `.dmg` を開き、Strata を「アプリケーション」にドラッグします。 |
|
||||
|
||||
モデルのダウンロードは大きく(40〜70 GB)、止まっても続きから再開します。
|
||||
|
||||
### 2. ターミナル(1行)
|
||||
|
||||
`strata` コマンドです。スクリプトやサーバー、AI エージェント向けです。DeepSeek を動かすにもこれを使います。
|
||||
|
||||
Linux と Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows(PowerShell):
|
||||
@@ -114,24 +103,17 @@ Windows(PowerShell):
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
スクリプトはPCを調べ、ターミナル版をダウンロードし、SHA-256 を検証して、ホームフォルダに展開します。管理者権限は不要です。Windows では、終わったら
|
||||
新しいターミナルを開いてください。そのあと Strata を起動します。
|
||||
そのあと起動します。質問は1つずつ表示されます(モデル、コンテキスト、画像、接続を許可する相手)。必要なものをダウンロードして、
|
||||
サーバーを開始します。
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
質問は1つずつ表示されます。モデル、コンテキスト、画像、接続を許可する相手の順です。そのあと、お使いのグラフィックスカード用のエンジン
|
||||
(署名付きで、使う前にテスト起動します)とモデルをダウンロードして、サーバーを開始します。スクリプトや AI エージェントからは、フラグを使います。
|
||||
管理者権限は不要です。インストールでは、ダウンロードしたものすべての SHA-256 を検証します。Windows では、インストール後に新しいターミナルを開いてください。
|
||||
すべての画面とフラグは [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md) にあります。
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
実行前にインストールスクリプトを読みたい場合は、先に `curl -fsSL <address> -o install.sh` でダウンロードし、内容を確認してから
|
||||
`sh install.sh` を実行してください。すべての画面とフラグは [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md) にあります。
|
||||
|
||||
### 方法C: AI にセットアップを任せる
|
||||
### 3. AI にセットアップを任せる
|
||||
|
||||
Claude Code、Cursor、Codex、GitHub Copilot などのコーディングアシスタントに、次の文を貼り付けます。
|
||||
|
||||
@@ -146,47 +128,55 @@ AI ツールは [MCP サーバー](docs/MCP_SERVER.md)を通じて、Strata の
|
||||
|
||||
## モデル
|
||||
|
||||
アプリと `strata launch` は、メモリ量に合うモデルをすすめます。モデルは1つで、圧縮の度合いが違う複数のサイズがあります。
|
||||
小さいサイズは速く、大きいサイズは少し賢くなります。
|
||||
アプリと `strata launch` は、メモリ量に合うサイズをすすめます。小さいサイズは速く、大きいサイズは少し賢くなります。
|
||||
|
||||
### メモリが少ないなら Gyro
|
||||
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)(agentionai 製)は、このモデルをいちばん小さく動かせる方法です。
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)(agentionai 製)は、Qwen3.8-Flash-Next をいちばん小さく動かせる方法です。
|
||||
NVIDIA、AMD、Intel Arc、Apple Silicon で動きます。
|
||||
|
||||
| サイズ | 必要なメモリ(RAM + VRAM) | ダウンロード | 内容 |
|
||||
| サイズ | 必要なメモリ(RAM + VRAM) | ダウンロード | 向いているPC |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27.6 GB | 58.5 GB | ほかのサイズが入らないPC向け。品質は IQ2_XS より下がります。 |
|
||||
| **Gyro-M** | 35 GB | 92.0 GB | 品質は IQ3_XXS 程度で、GPUメモリの使用量は約20%少なくなります。回答の書き出しは Q2_0 より、12 GBの RTX 3060 で16%、R9700 で21%遅くなります。 |
|
||||
| **Gyro-S** | 27.6 GB | 58.5 GB | ほかのサイズが入らないPC向け。32 GB の Mac mini で毎秒 9 トークン。 |
|
||||
| **Gyro-M** | 35 GB | 92.0 GB | 品質は IQ3_XXS 程度で、GPUメモリの使用量は20%少なくなります。 |
|
||||
|
||||
Gyro は NVIDIA と AMD のカード(AMD は実験的)、および Linux 上の Intel Arc カード(Arc A750 で確認)で動きます。0.2.2 からは Apple Silicon の Mac でも動きます(32 GB の Mac mini で Gyro-S は 9 トークン/秒、エキスパートは SSD から読み込み)。詳細と測定結果は
|
||||
[docs/GYRO.md](docs/GYRO.md) にあります。`strata launch --model gyro-s` で起動します。
|
||||
`strata launch --model gyro-s` で起動します。詳細は [docs/GYRO.md](docs/GYRO.md) にあります。
|
||||
|
||||
### ほかのサイズ
|
||||
### Qwen3.8-Flash-Next のサイズ
|
||||
|
||||
| メモリ(RAM + VRAM) | 選ぶモデル | 備考 |
|
||||
| メモリ(RAM + VRAM) | 選ぶモデル | 理由 |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Gyro-S**、またはコード向けの **[Coder](docs/MODELS.md#coder)** | Coder はRAM 32 GBが必要で、エキスパートを半分だけ保持するため、コード以外は弱くなります |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS(39.2 GB)、または **Gyro-M**(35 GB) | Swift は答える前に考える時間が短くなります。Gyro-M は品質が IQ3_XXS に近いものの、回答の書き出しは Q2_0 より16〜21%遅くなります |
|
||||
| **32 GB** | **Gyro-S**、またはコード向けの **[Coder](docs/MODELS.md#coder)** | Coder は、コードで使うエキスパートを残します |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS(39.2 GB)、または **Gyro-M** | Swift は答える前に考える時間が短くなります |
|
||||
| **48 GB** | **IQ3_XXS**(47.0 GB) | 遅くなりますが、品質は上がります |
|
||||
| **64 GB** | **IQ3_S**(54.8 GB)、オリジナルまたは Swift 1.5 | いちばん遅く、いちばん高品質です。公開されているテストでは完全版と同等です |
|
||||
| **96 GB 以上** | Unsloth の **UD-IQ4_XS**(RAM 約72 GB)から **Q8_0**(約136 GB)まで、4〜8ビット | 完全版に近づきます。RAM が足りないと、Strata は一部を SSD から読むため、ずっと遅くなります |
|
||||
| **64 GB** | **IQ3_S**(54.8 GB) | 公開されているテストでは完全版と同等です |
|
||||
| **96 GB 以上** | Unsloth の **UD-IQ4_XS** から **Q8_0** | 完全版にいちばん近いです |
|
||||
|
||||
表の行より RAM が少なくても動くことがあります。Strata は自動で[低RAMモード](docs/MODELS.md#a-big-graphics-card-and-little-ram)に切り替わります。
|
||||
速度は落ち、グラフィックスカードが小さいとさらに大きく落ちます。
|
||||
表の行より RAM が少なくても、遅くなるだけで動くことがあります。Strata は自動で[低RAMモード](docs/MODELS.md#a-big-graphics-card-and-little-ram)に切り替わります。
|
||||
そのほかに、NVIDIA カード向けの [NVFP4](docs/NVFP4.md)、[Qwen3.6-35B-A3B](docs/QWEN36.md)(より小さく、複数のリクエストを同時に処理)、
|
||||
[OrcaRouter の Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs) があります。すべてのサイズと、どの構成で何が動くか:
|
||||
[docs/MODELS.md](docs/MODELS.md)。
|
||||
|
||||
そのほかの選択肢:
|
||||
### DeepSeek V4.1-Flash(ターミナル版、0.2.4 から)
|
||||
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** はファインチューン版で、答える前に考える時間が短くなります。品質と RAM の必要量はほぼ同じです。
|
||||
- **[NVFP4](docs/NVFP4.md)** は NVIDIA カード向けの NVIDIA 製4ビット版です。すべてのエキスパートを RAM に置くには、約80 GBの RAM が必要です。
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** は、より小さい別のモデルです。画像を読め、複数のリクエストに同時に答えられます。
|
||||
- **[OrcaRouter の Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** は手動で設定します。
|
||||
ずっと大きなモデルで、12 GB のグラフィックスカード 1 枚と 46 から 64 GB の RAM で動きます。Strata はよく使う部分をメモリに置き、
|
||||
残りは SSD から読みます。セットは [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF) が作りました。
|
||||
NVIDIA RTX 30、40、50 シリーズのカードと Radeon AI PRO R9700 で動きます。`strata launch` で選んでください。デスクトップアプリはまだ対応していません。
|
||||
|
||||
すべてのサイズ、ダウンロード、どの構成で何が動くかは [docs/MODELS.md](docs/MODELS.md) にあります。
|
||||
| セット | ダウンロード | 品質 | コードの出力速度 |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M**(1.75 ビット) | 239 GB | いちばん小さく、ふつうの文章は弱めです | PC ではまだ測っていません |
|
||||
| **IQ2_XXS**(2.00 ビット) | 257 GB | バランスが良いです | 毎秒 10 トークン(RTX 3060)、5(RTX 5070)、48(R9700 4 枚) |
|
||||
| **IQ2_XS**(2.25 ビット) | 274 GB | 元のモデルにいちばん近いです | 毎秒 15 トークン(R9700 1 枚)、80(R9700 4 枚) |
|
||||
| **IQ4_NL**(Engram テーブル) | 上のどのダウンロードにも含まれます | 3 つのセットが共有する参照テーブルです | |
|
||||
|
||||
`strata launch --model ist-das-2.00bpw`(または `ist-das-1.75bpw`、`ist-das-2.25bpw`)で起動します。品質の数値、PC ごとの速度、手動インストールは
|
||||
[docs/DEEPSEEK41.md](docs/DEEPSEEK41.md) にあります。
|
||||
|
||||
## どのくらい速い?
|
||||
|
||||
12 GBのグラフィックスカード(RTX 5070、Ryzen 5 7600、RAM 64 GB)での数値です。ほかのカードの結果は
|
||||
[各モデルの速度](docs/MODELS.md#how-fast-is-each-size)、[コミュニティの結果](docs/COMMUNITY_BENCHMARKS.md)を見てください。
|
||||
12 GBのグラフィックスカード(RTX 5070、RAM 64 GB)での数値です。ほかのカードの結果は
|
||||
[各サイズの速度](docs/MODELS.md#how-fast-is-each-size)と[コミュニティの結果](docs/COMMUNITY_BENCHMARKS.md)を見てください。
|
||||
|
||||
| モデル | 回答が出る速さ | プロンプトを読む速さ |
|
||||
| --- | --- | --- |
|
||||
@@ -196,9 +186,7 @@ Gyro は NVIDIA と AMD のカード(AMD は実験的)、および Linux 上
|
||||
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1,620 tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2,180 tokens/s |
|
||||
|
||||
1トークンは英単語の約¾にあたるので、毎秒60トークンは読む速さより速くなります。「プロンプトを読む速さ」は、長い文書やコード、チャット履歴を Strata が取り込む速さです。
|
||||
|
||||
16 GBの AMD カード(RX 9070 XT、Ryzen 9 3900X、RAM 47 GB)での測定値:
|
||||
16 GBの AMD カード(RX 9070 XT、RAM 47 GB)の場合:
|
||||
|
||||
| モデル | 回答が出る速さ | プロンプトを読む速さ |
|
||||
| --- | --- | --- |
|
||||
@@ -206,79 +194,57 @@ Gyro は NVIDIA と AMD のカード(AMD は実験的)、および Linux 上
|
||||
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1,110 tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1,420 tokens/s |
|
||||
|
||||
バーは2つの表で同じ目盛りです。VRAM が多いカードほど速くなり、RTX 3090(24 GB)なら毎秒100〜140トークンほどになるはずです。各数値のエンジンのバージョンは
|
||||
[DETAILS.md](docs/DETAILS.md#speed-measured) にあります。
|
||||
1トークンは英単語の約¾にあたるので、毎秒60トークンは読む速さより速くなります。「プロンプトを読む速さ」は、長い文書やコード、チャット履歴を Strata が取り込む速さです。
|
||||
|
||||
## 使い方
|
||||
|
||||
### アプリで
|
||||
|
||||
アプリには、**チャット**、モデルとGPU・CPU・RAMの状態を見る**モニター**、**モデル**ページ、**設定**があります。チャットはPC上のフォルダで作業できます。
|
||||
**手動**モードでは、ファイルを変更したりコマンドを実行したりする前に確認します。`/loop 10m <やること>` と入力すると、チャットを開いている間、メッセージを10分ごとに繰り返し送ります。
|
||||
チャット、モデルとGPU・CPU・RAMの状態を見る**モニター**、**モデル**ページ、**設定**があります。チャットはPC上のフォルダで作業できます。
|
||||
**手動**モードでは、ファイルを変更したりコマンドを実行したりする前に確認します。ウェブ検索もできます(DuckDuckGo または Bing、アカウント不要)。
|
||||
PCの外に出るのは検索語だけです。
|
||||
|
||||
チャットは DuckDuckGo(または Bing)でウェブ検索ができ、アカウントは不要です。**設定、ツール**で検索キーを追加することもできます。
|
||||
検索は、PCの外に何かを送る唯一の機能で、送られるのは検索語です。
|
||||
|
||||
2つのモデルを動かすには、**複数のモデルを実行**をオンにして、それぞれのモデルに同じメーカーのカードを1枚ずつ割り当てます。
|
||||
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
|
||||
<sub>Strata アプリでのチャット。エージェントが小さなボクセルの塔の庭を 1 つの HTML ファイルとして書き、そのページが会話の横のプレビューで開きます</sub></p>
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="Strata アプリ。エージェントが小さなボクセルの塔の庭を作ったチャットで、右側のプレビューにそのページが開いています"><br>
|
||||
<sub>エージェントがボクセルの塔の庭を 1 つの HTML ファイルとして書き、そのページがチャットの横のプレビューで開きます</sub></p>
|
||||
|
||||
### ターミナルで
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="モデルが返信を書いている間の Strata ターミナルのモニター概要"><br>
|
||||
<sub>モデルが返信を書いている間の <code>strata monitor</code>。速度、GPU、メモリ、最近のリクエストが表示されます</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # ガイド付きの画面: モデル、コンテキスト、画像、アクセス、そして開始
|
||||
strata launch --model q2_0 --yes --no-tui # 質問なし: 開始し、アドレスを表示して戻る
|
||||
strata launch --plan --model q2_0 # 実行内容を表示するだけで、何も変更しない
|
||||
strata launch --status # 今、何が動いているか
|
||||
strata launch --stop # 停止する
|
||||
strata monitor --url http://127.0.0.1:8080 # 速度、GPU、メモリ、リクエストを見る
|
||||
strata update --check # 新しいエンジンがあるか確認(strata update でインストール)
|
||||
strata doctor # インストール済みのエンジンファイルを調べる
|
||||
strata launch # guided: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve and print the address
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor # watch speed, GPU, memory and requests
|
||||
strata update # get the newest engine
|
||||
strata doctor # check the installed files
|
||||
```
|
||||
|
||||
`strata --help` ですべてのコマンドを確認できます。起動とサーバーの稼働では、何もダウンロードしません。ダウンロードするのは、インストール、
|
||||
`strata launch`(モデルを選んだとき)、`strata update` だけです。スマートフォンや別のPCから Strata に接続するには、
|
||||
`strata launch --access lan` を使います。API キーが作られ、キーがない状態ではネットワーク上で起動しません。`--open` を付けない限り、ブラウザは開きません。
|
||||
ガイド: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md)。クライアント全体: [docs/TERMINAL.md](docs/TERMINAL.md)。
|
||||
スマートフォンや別のPCから Strata に接続するには、`strata launch --access lan` を使います。API キーが自動で作られます。
|
||||
`strata --help` ですべてのコマンドを確認できます。説明は [docs/TERMINAL.md](docs/TERMINAL.md) にあります。
|
||||
|
||||
### AI エージェントと使う
|
||||
### AI エージェントやほかのアプリと使う
|
||||
|
||||
エージェントは、方法Bの1行コマンドで Strata をインストールし、
|
||||
`strata launch --model NAME --yes --no-tui` を実行します。手順、フラグ、終了コードは [docs/AI_SETUP.md](docs/AI_SETUP.md) に、
|
||||
リポジトリのルールは [AGENTS.md](AGENTS.md) にあります。
|
||||
エージェントは、上の1行コマンドで Strata をインストールし、`strata launch --model NAME --yes --no-tui` を実行します。手順、フラグ、終了コードは
|
||||
[docs/AI_SETUP.md](docs/AI_SETUP.md) にあります。エージェント向けのリポジトリのルールは [AGENTS.md](AGENTS.md) にあります。
|
||||
|
||||
### ほかのアプリをつなぐ
|
||||
|
||||
| アプリの種類 | ベース URL |
|
||||
| アプリが使うもの | ベース URL |
|
||||
| --- | --- |
|
||||
| OpenAI 互換(ほとんどのアプリとエージェント) | `http://127.0.0.1:8080/v1`(API キーもモデル名も何でも可) |
|
||||
| Anthropic API(Claude Code など) | `http://127.0.0.1:8080`、`/v1` は付けません。これらのアプリは `/v1/messages` を自分で付けます。Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI API(ほとんどのアプリとエージェント) | `http://127.0.0.1:8080/v1`(API キーもモデル名も何でも可) |
|
||||
| Anthropic API(Claude Code) | `http://127.0.0.1:8080`、`/v1` は付けません。Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API(Codex CLI) | `http://127.0.0.1:8080/v1/responses`([設定方法](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` は、OpenCode、Codex CLI、Claude Code、oh-my-pi、pi、Hermes Agent、OpenClaw の正確な設定を表示します。
|
||||
`strata connect opencode --write` は、確認したうえでファイルを書き込み、バックアップを残します。
|
||||
|
||||
### 画像
|
||||
|
||||
画像を読めるモデルでは、画像は初期設定でオンです。チャットに画像を添付または貼り付けるか、アプリから送ってください。
|
||||
グラフィックスカードが24 GB未満のときは、画像リーダーがプロセッサで動きます(画像1枚につき数秒、カードのメモリは使いません)。24 GB以上のときはグラフィックスカードで動きます。
|
||||
このルールは NVIDIA と AMD で同じです。画像はモデルごとにオフにでき、`--vision off`、`--vision gpu`、`--vision cpu` でリーダーを選べます。
|
||||
|
||||
このルールの根拠となる数値です。Radeon AI PRO R9700 では、カードのリーダーは260トークンの画像に57 ms、プロセッサのリーダーは3.1秒かかりました。
|
||||
16 GBのカード(Tesla P100 と RTX A4000、Linux)では、カードのリーダーを使うとテキストの速度が3〜5%下がりました。
|
||||
本来モデルが使う約1.4 GBをリーダーが占めるためです。Mac ではメモリ量にかかわらず GPU が読み取ります(32 GB の M4 で260トークンの画像に0.45秒、共有メモリを約1.2 GB使います)。プロセッサでは1枚あたり約50秒かかるため、`--vision cpu` を指定したときだけ使います。
|
||||
`strata connect opencode --write` なら、設定を書き込みます(バックアップ付き)。
|
||||
|
||||
### 知っておくと便利なこと
|
||||
|
||||
- 思考のレベル(**off、low、medium、high**)は、チャットのメニューか、お使いのアプリの「reasoning effort」で選びます。off がいちばん速くなります。
|
||||
- Strata は初期設定では、一度に1つのリクエストに答えます。同時に複数処理するには、`strata launch` で `--parallel N` を使うか、設定に
|
||||
`"parallel": 2` を書きます([BATCHING.md](docs/BATCHING.md))。12 GBのカードでは、1つずつの回答が遅くなります。
|
||||
- Strata はチャットの最初のメッセージを全文読みます。30,000トークンあたり約1分かかります。続きのメッセージは数秒で始まります。
|
||||
- チャットの保存場所と API 全体: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored)、[docs/DETAILS.md](docs/DETAILS.md#using-it)。
|
||||
- **画像**は、画像を読めるモデルすべてでオンです。チャットに画像を添付または貼り付けるか、アプリから送ってください。詳細: [docs/IMAGES.md](docs/IMAGES.md)。
|
||||
- **思考のレベル**は off、low、medium、high から、チャットのメニューか、お使いのアプリの「reasoning effort」で選びます。off がいちばん速くなります。
|
||||
- **チャットの最初のメッセージ**は全文読みます。30,000トークンあたり約1分かかります。続きのメッセージは数秒で始まります。
|
||||
- **一度に1つのリクエスト**が初期設定です。複数にするには `--parallel N` を使います([BATCHING.md](docs/BATCHING.md))。12 GBのカードでは、1つずつの回答が遅くなります。
|
||||
|
||||
## ソースからビルド
|
||||
|
||||
@@ -335,12 +301,12 @@ Strata のデータフォルダにある `strata-<model>.json`([場所](docs/I
|
||||
- **RAM は閲覧室の書棚です。** 24,576冊すべてがここにあります。単語に必要な本が机になければ、プロセッサがすぐに書棚から読み、その間もカードは作業を続けます。
|
||||
- **SSD は書庫です。** 29 GBの参照テーブルがしまってあります。モデルは単語ごとにその数行だけを読むので、RAM に置いておく必要はありません。
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="モデルの24,576人のエキスパート。よく使うものはグラフィックスカードに、全員が RAM に、参照テーブルは SSD にあります"></p>
|
||||
|
||||
小さな助手が、次の数単語も予想します。大きなモデルがそれをまとめて確認し、正しいものだけを残します。答えは同じで、1.6〜1.8倍早く返ります。
|
||||
長い文章は、一度に最大8,192トークンの大きな塊で、毎秒1,000トークンを超える速さで読みます。
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="小さな助手が次の単語を予想し、大きなモデルがそれをまとめて確認して、正しいものだけを残します"></p>
|
||||
|
||||
詳しい説明は [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md) にあります。各部分と数値は
|
||||
[詳細](docs/DETAILS.md#how-it-works)と[論文](docs/paper/Strata-Paper.pdf)を見てください。
|
||||
@@ -349,24 +315,23 @@ Strata のデータフォルダにある `strata-<model>.json`([場所](docs/I
|
||||
|
||||
- **Strata を初めて起動したときに、PCが固まりました。** モデルを RAM に読み込んでいます。ウィンドウを閉じずに待ってください。10分たっても固まったままなら、
|
||||
PCを再起動し、ほかのプログラムを閉じてからもう一度試すか、小さいサイズを選んでください。
|
||||
- **ダウンロードやインストールの途中で止まりました。** アプリか `strata launch` をもう一度起動するか、`START-HERE.bat`(または
|
||||
`./setup.sh`)をもう一度実行してください。止まったところから再開します。
|
||||
- **とても遅い、ディスクのランプが点滅し続ける、または「エンジンが予期せず停止しました」と表示される。** PCの空き RAM が足りません。
|
||||
- **ダウンロードやインストールの途中で止まりました。** アプリか `strata launch` をもう一度起動してください。止まったところから再開します。
|
||||
- **とても遅い、または「the engine stopped unexpectedly」と表示される。** PCの空き RAM が足りません。
|
||||
ほかのプログラムを閉じるか(ブラウザはメモリを多く使います)、Q2_0 や IQ2_XS のような小さいサイズを選んでください。
|
||||
- **このPCに合うエンジンがないと表示される。** アプリでは、**設定、エンジン**を開いて**エンジンを更新**を押します。ターミナルでは
|
||||
`strata update` を実行します。`strata doctor` で、どのエンジンファイルがインストールされているか分かります。
|
||||
`strata update` を実行します。
|
||||
- **ポート 8080 はすでに使われていると表示される。** Strata がすでに動いています。ウィンドウを探してください。
|
||||
|
||||
そのほかの問題と対処法は [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) にあります。それでも解決しない場合は、
|
||||
[issue](https://github.com/Niko1221/Strata/issues) を作成してください。アプリでは、**設定、エンジン、診断、ログを見る、ログを保存**で作られるファイルを添付します。
|
||||
ターミナルでは、`strata doctor` の出力を貼り付けます。ソースから動かしている場合は、Strata フォルダの `strata-<model>.log` を添付してください。
|
||||
セキュリティ上の問題は、非公開で報告してください: [SECURITY.md](SECURITY.md)。
|
||||
そのほかの対処法は [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) にあります。それでも解決しない場合は、
|
||||
[issue](https://github.com/Niko1221/Strata/issues) を作成して、ログを添付してください。アプリでは**設定、エンジン、診断、ログを保存**、
|
||||
ターミナルでは `strata doctor` の出力です。セキュリティ上の問題は、非公開で報告してください: [SECURITY.md](SECURITY.md)。
|
||||
|
||||
## クレジットとライセンス
|
||||
|
||||
モデルは Qwen チームの [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) です。圧縮は
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF)、UkisAI(Swift 1.5)、agentionai(Gyro)、
|
||||
Unsloth が行いました。Strata は [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) の一部を使っています。すべてのクレジットは
|
||||
Unsloth が行いました。DeepSeek V4.1-Flash のセットも ISTA-DASLab が作りました。Strata は
|
||||
[llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) の一部を使っています。すべてのクレジットは
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits) にあります。Strata は [MIT ライセンス](LICENSE)のオープンソースです。一部のコンポーネントと
|
||||
すべてのモデルには、それぞれのライセンスがあります([該当するもの](docs/HOW_IT_WORKS.md#license))。
|
||||
|
||||
|
||||
@@ -12,102 +12,88 @@
|
||||
<p align="center">Support for NVIDIA, AMD, Intel · Windows, Linux, Mac · free and open source</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=black" alt="Buy me a coffee"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<sub>A voxel pagoda garden, 1 shot prompt running on an RTX 5070 with Strata (IQ3_S, 128K context) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">full video (49 s)</a></sub></p>
|
||||
|
||||
Strata runs **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, a 125-billion-parameter model that
|
||||
normally needs a server, on a normal PC. It chats, writes code, reads pictures and works with your apps and coding
|
||||
agents. Your chats and files stay on your PC.
|
||||
<p align="center">
|
||||
<a href="#install"><b>Install</b></a> ·
|
||||
<a href="#models">Models</a> ·
|
||||
<a href="#how-fast-is-it">Speed</a> ·
|
||||
<a href="#using-it">Using it</a> ·
|
||||
<a href="#how-does-it-work">How it works</a> ·
|
||||
<a href="#something-went-wrong">Help</a> ·
|
||||
<a href="#support-strata">Support</a>
|
||||
</p>
|
||||
|
||||
**New here?** Check [what you need](#what-you-need), then [install the app](#option-a-the-strata-app).
|
||||
Strata runs **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, a 125-billion-parameter model that
|
||||
normally needs a server, on a normal PC. From 0.2.4 it also runs **DeepSeek V4.1-Flash** in the Terminal. It chats, writes
|
||||
code, reads pictures and works with your apps and coding agents. Your chats and files stay on your PC.
|
||||
|
||||
- **Runs on a gaming PC.** 32 GB of RAM and graphics memory together is enough to start.
|
||||
- **Works with your tools.** It speaks the OpenAI and Anthropic APIs, so Claude Code, Codex CLI, OpenCode and other agents connect to it.
|
||||
- **Reads pictures.** They are on by default.
|
||||
- **Free.** MIT license, no account, no cloud.
|
||||
- **Private.** No telemetry. What Strata downloads, when it uses the internet and how it checks downloads is in [docs/SAFETY.md](docs/SAFETY.md).
|
||||
|
||||
Version 0.2.2 · engine 0.1.47
|
||||
- **Works with your tools.** Claude Code, Codex CLI, OpenCode and other agents connect through the OpenAI and Anthropic APIs.
|
||||
- **Reads pictures**, on by default.
|
||||
- **Free and private.** MIT license, no account, no cloud, no telemetry ([what it downloads and when](docs/SAFETY.md)).
|
||||
|
||||
## What you need
|
||||
|
||||
**You need at least 32 GB of RAM and graphics memory (VRAM) together.** On a Mac or a Ryzen AI Max, that is the unified
|
||||
memory. More memory gives a bigger model or a faster one.
|
||||
**At least 32 GB of memory: RAM and graphics memory (VRAM) together.** On a Mac or a Ryzen AI Max that is the unified
|
||||
memory. A 32 GB PC with a 12-16 GB card runs the smaller sizes, and 64 GB runs every size.
|
||||
|
||||
- A 32 GB PC with a 12-16 GB card runs the Coder.
|
||||
- 64 GB of RAM runs every size.
|
||||
|
||||
| Your computer | Details |
|
||||
| Works on | Guide |
|
||||
| --- | --- |
|
||||
| **NVIDIA** GeForce RTX 20, 30, 40, 50 series (12 GB or more) | Install the app or the Terminal. |
|
||||
| **AMD** Radeon RX 6800 and newer, Radeon AI PRO R9700 (12 GB or more) | [AMD cards](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [Intel engine](docs/INTEL.md) |
|
||||
| **Apple Silicon Mac** (M1 or newer, macOS 15 or newer) | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** and other unified-memory systems | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Older cards and processors | [Older GPUs](docs/OLDER_GPUS.md), [Older CPUs](docs/INSTALL.md#older-cpus-experimental) |
|
||||
| **NVIDIA** GeForce RTX 20, 30, 40 and 50 series, 12 GB or more | [Install](#install) |
|
||||
| **AMD** Radeon RX 6800 and newer, Radeon AI PRO R9700 | [AMD cards](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md) |
|
||||
| **Apple Silicon Mac**, M1 or newer, macOS 15 or newer | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** and other unified-memory PCs | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Older cards and processors | [Older GPUs](docs/OLDER_GPUS.md) |
|
||||
|
||||
Also needed:
|
||||
|
||||
- about 80 GB of free disk (an SSD makes the first start much faster)
|
||||
- Windows 10 / 11, Linux or macOS
|
||||
- a current graphics driver
|
||||
|
||||
Two or three cards of the same make can share a model ([multi-GPU](docs/MULTI_GPU.md)). Strata never splits a model across
|
||||
NVIDIA and AMD cards. Full list: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
You also need about 80 GB of free disk (an SSD), Windows 10 / 11, Linux or macOS, and a current graphics driver. Two or
|
||||
three cards of the same make can share a model ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
|
||||
## Install
|
||||
|
||||
Pick one option. They all start the same engine and use the same models.
|
||||
### 1. The Strata app (easiest)
|
||||
|
||||
### Option A: the Strata app
|
||||
Download the file for your computer, open it, and press install on the model it recommends.
|
||||
|
||||
The easiest way. The app has the engine inside: install it, pick a model, chat. Download the file for your system and
|
||||
graphics card from the [releases page](https://github.com/Niko1221/Strata/releases/latest). Not sure which one? See the
|
||||
line under the table.
|
||||
**Windows 10 / 11**
|
||||
|
||||
| System | Your graphics card | File to download |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, driver 580 or newer | [`Strata-0.2.2-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda13.exe) |
|
||||
| | NVIDIA, older card or driver older than 580 | [`Strata-0.2.2-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda12.exe) |
|
||||
| | AMD Radeon RX 9000 series or Radeon AI PRO R9700 | [`Strata-0.2.2-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip.exe) |
|
||||
| | AMD Radeon RX 7000 series or Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, driver 580 or newer | [`Strata-0.2.2-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, older card or driver older than 580 | [`Strata-0.2.2-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon RX 9000 series or Radeon AI PRO R9700 | [`Strata-0.2.2-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon RX 7000 series | [`Strata-0.2.2-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 or newer) | built in | [`Strata-0.2.2-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-mac.dmg) |
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-windows-x64-setup.exe)
|
||||
|
||||
**If you are unsure:** for NVIDIA, take `cuda13` if your driver is version 580 or newer, otherwise `cuda12`. `cuda12` is
|
||||
for older cards and older drivers. The `hip` and `amd` files are for AMD cards. RX 6000 cards have no installer of
|
||||
their own: install `hip-rdna3` (Windows) or `amd-rx7900` (Linux), and on a PC with an RX 6000 card it fetches the RX 6000 engine by itself
|
||||
(also `strata update`). That engine is built but not yet tested on a real RX 6000 card: see [AMD cards](docs/AMD_HIP.md).
|
||||
**Linux**
|
||||
|
||||
Then:
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-linux-x86_64.AppImage)
|
||||
|
||||
- **Windows:** run the `.exe`. It installs for your user and needs no administrator rights. If Windows says "Windows
|
||||
protected your PC", choose **More info**, then **Run anyway** (the installer is not code-signed yet).
|
||||
- **Linux:** make the file runnable (`chmod +x`) and start it. It needs FUSE 2: `sudo apt install libfuse2` (Ubuntu 22.04)
|
||||
or `libfuse2t64` (Ubuntu 24.04 and newer).
|
||||
- **Mac:** open the `.dmg` and drag Strata onto Applications.
|
||||
**Mac** (Apple Silicon, macOS 15 or newer)
|
||||
|
||||
On the first screen the app reads your PC and recommends a model that fits. Press install. The model is a big download
|
||||
(about 40-70 GB) and continues where it stopped if it is interrupted. Closing the window stops Strata and the engine.
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-macos-arm64.dmg)
|
||||
|
||||
### Option B: the Terminal (one line)
|
||||
The app picks the right engine for your graphics card by itself. All files are on the [releases page](https://github.com/Niko1221/Strata/releases/latest).
|
||||
|
||||
The Terminal is the `strata` command, for scripts, servers and AI agents.
|
||||
| First start | What to do |
|
||||
| --- | --- |
|
||||
| **Windows** | Run the `.exe`. No administrator rights needed. If Windows says "Windows protected your PC", choose **More info**, then **Run anyway**. |
|
||||
| **Linux** | `chmod +x` the file and start it. It needs FUSE 2: `sudo apt install libfuse2t64` (Ubuntu 24.04 and newer) or `libfuse2` (22.04). |
|
||||
| **Mac** | Open the `.dmg` and drag Strata onto Applications. |
|
||||
|
||||
The model download is big (40-70 GB). If it stops, it continues where it left off.
|
||||
|
||||
### 2. The Terminal (one line)
|
||||
|
||||
The `strata` command, for scripts, servers and AI agents. It is also the way to run DeepSeek.
|
||||
|
||||
Linux and Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
@@ -116,24 +102,17 @@ Windows (PowerShell):
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
The script checks your PC, downloads the Terminal, verifies its SHA-256 and unpacks it into your home folder. It needs no
|
||||
administrator rights. On Windows, open a new terminal window afterwards. Then start Strata:
|
||||
Then start it. It asks one question at a time (model, context, pictures, who may connect), downloads what it needs and
|
||||
starts serving:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
It asks one question at a time: model, context, pictures, who may connect. Then it downloads the engine for your graphics
|
||||
card (signed, and test-started before use) and the model, and starts serving. For scripts and AI agents, use flags:
|
||||
No admin rights needed. The install checks the SHA-256 of everything it downloads. On Windows, open a new terminal window
|
||||
after the install. All screens and flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
To read the install script before it runs, download it first with `curl -fsSL <address> -o install.sh`, read it, then run
|
||||
`sh install.sh`. All screens and flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### Option C: let your AI set it up
|
||||
### 3. Let your AI set it up
|
||||
|
||||
Paste this into Claude Code, Cursor, Codex, GitHub Copilot or another coding assistant:
|
||||
|
||||
@@ -141,57 +120,64 @@ Paste this into Claude Code, Cursor, Codex, GitHub Copilot or another coding ass
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
It checks your graphics card, RAM and disk, picks the model that fits, installs and starts it, and tells you how to
|
||||
connect your apps. AI tools can also install, start and stop Strata through its [MCP server](docs/MCP_SERVER.md).
|
||||
It checks your graphics card, RAM and disk, picks the model that fits, installs it, starts it and tells you how to connect
|
||||
your apps. AI tools can also install, start and stop Strata through its [MCP server](docs/MCP_SERVER.md).
|
||||
|
||||
Prefer to build it yourself? See [Build from source](#build-from-source).
|
||||
|
||||
## Models
|
||||
|
||||
The app and `strata launch` recommend a model for your memory. It is one model in several sizes, compressed more or less.
|
||||
Smaller sizes are faster. Larger sizes are a bit smarter.
|
||||
The app and `strata launch` recommend a size for your memory. Smaller sizes are faster, larger sizes are a bit smarter.
|
||||
|
||||
### Low on memory? Try Gyro
|
||||
### Low on memory? Start with Gyro
|
||||
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (by agentionai) is the smallest way to run this model.
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) by agentionai is the smallest way to run
|
||||
Qwen3.8-Flash-Next. It runs on NVIDIA, AMD, Intel Arc and Apple Silicon.
|
||||
|
||||
| Size | Memory it needs (RAM + VRAM) | Download | What it is |
|
||||
| Size | Memory it needs (RAM + VRAM) | Download | Good for |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27.6 GB | 58.5 GB | For PCs that fit nothing else. Quality is lower than IQ2_XS. |
|
||||
| **Gyro-M** | 35 GB | 92.0 GB | About IQ3_XXS quality with about 20% less GPU memory. It writes answers 16% (12 GB RTX 3060) to 21% (R9700) slower than Q2_0. |
|
||||
| **Gyro-S** | 27.6 GB | 58.5 GB | PCs that fit nothing else. 9 tokens/s on a 32 GB Mac mini. |
|
||||
| **Gyro-M** | 35 GB | 92.0 GB | About IQ3_XXS quality with 20% less GPU memory. |
|
||||
|
||||
Gyro runs on NVIDIA and AMD cards (AMD is experimental) on Intel Arc cards on Linux (tested on an Arc A750) and, from 0.2.2, on Apple Silicon Macs (Gyro-S on a 32 GB Mac mini: 9 tok/s, streaming from the SSD; [docs/MAC.md](docs/MAC.md#gyro-s-and-gyro-m-agentionais-trellis-experts)). Details and measurements:
|
||||
[docs/GYRO.md](docs/GYRO.md). Start it with `strata launch --model gyro-s`.
|
||||
Start it with `strata launch --model gyro-s`. Details: [docs/GYRO.md](docs/GYRO.md).
|
||||
|
||||
### The other sizes
|
||||
### Qwen3.8-Flash-Next sizes
|
||||
|
||||
The tiers count your RAM plus your VRAM, less 5 GB that Windows and the system keep for themselves. Every other size stays a choice.
|
||||
|
||||
| Your memory (RAM + VRAM) | Take | Notes |
|
||||
| Your memory (RAM + VRAM) | Take | Why |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Gyro-S**, or the **[Coder](docs/MODELS.md#coder)** for code | The Coder needs 32 GB of RAM and keeps half of the experts, so it is weaker outside code |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39.2 GB), or **Gyro-M** (35 GB) | Swift thinks for a shorter time before it answers. Gyro-M is close to IQ3_XXS in quality, but it writes answers 16-21% slower than Q2_0 |
|
||||
| **32 GB** | **Gyro-S**, or the **[Coder](docs/MODELS.md#coder)** for code | The Coder keeps the experts that code uses |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39.2 GB) or **Gyro-M** | Swift thinks for a shorter time before it answers |
|
||||
| **48 GB** | **IQ3_XXS** (47.0 GB) | Slower, better |
|
||||
| **64 GB** | **IQ3_S** (54.8 GB), the original or Swift 1.5 | The slowest, the best: it matches the full model on the published tests |
|
||||
| **96 GB or more** | Unsloth's **UD-IQ4_XS** (about 72 GB of RAM) to **Q8_0** (about 136 GB), 4 to 8 bit | Closer to the full model. With less RAM, Strata reads part of them from the SSD, which is much slower |
|
||||
| **64 GB** | **IQ3_S** (54.8 GB) | Matches the full model on the published tests |
|
||||
| **96 GB or more** | Unsloth's **UD-IQ4_XS** to **Q8_0** | Closest to the full model |
|
||||
|
||||
Less RAM than a row says can still work: Strata switches to its
|
||||
[low-RAM mode](docs/MODELS.md#a-big-graphics-card-and-little-ram) by itself. Expect it to be slower, and much slower
|
||||
with a small graphics card.
|
||||
Less RAM than a row says can still work, only slower: Strata switches to its
|
||||
[low-RAM mode](docs/MODELS.md#a-big-graphics-card-and-little-ram) by itself. Also available:
|
||||
[NVFP4](docs/NVFP4.md) for NVIDIA cards, [Qwen3.6-35B-A3B](docs/QWEN36.md) (smaller, several requests at once) and
|
||||
[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs). Every size and what fits where:
|
||||
[docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
More choices:
|
||||
### DeepSeek V4.1-Flash (Terminal, from 0.2.4)
|
||||
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** is a fine-tune that thinks for a shorter time before it answers, at about the same quality and RAM.
|
||||
- **[NVFP4](docs/NVFP4.md)** is NVIDIA's 4-bit version for NVIDIA cards. It needs about 80 GB of RAM to keep every expert in RAM.
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** is a smaller, different model that reads pictures and can answer several requests at once.
|
||||
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** is set up by hand.
|
||||
A much bigger model that runs on one 12 GB graphics card with 46 to 64 GB of RAM. Strata keeps the busiest parts in memory and
|
||||
reads the rest from your SSD. [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF)
|
||||
made the sets. It runs on NVIDIA RTX 30, 40 and 50 cards and the Radeon AI PRO R9700. Pick it in `strata launch`; the desktop
|
||||
app does not offer it yet.
|
||||
|
||||
Every size, download and what fits where: [docs/MODELS.md](docs/MODELS.md).
|
||||
| Set | Download | Quality | Writes code at |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M** (1.75 bits) | 239 GB | The smallest; weaker on plain prose | not measured on a PC yet |
|
||||
| **IQ2_XXS** (2.00 bits) | 257 GB | Good balance | 10 tokens/s (RTX 3060), 5 (RTX 5070), 48 (4x R9700) |
|
||||
| **IQ2_XS** (2.25 bits) | 274 GB | Closest to the original | 15 tokens/s (one R9700), 80 (4x R9700) |
|
||||
| **IQ4_NL** (Engram table) | in every download above | The lookup table all three sets share | |
|
||||
|
||||
Start it with `strata launch --model ist-das-2.00bpw` (or `ist-das-1.75bpw`, `ist-das-2.25bpw`). Quality numbers, speeds
|
||||
per PC and the manual install: [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
|
||||
|
||||
## How fast is it?
|
||||
|
||||
Based on a 12 GB graphics card (RTX 5070, Ryzen 5 7600, 64 GB RAM). See results from other cards:
|
||||
[speed of each model](docs/MODELS.md#how-fast-is-each-size), [community results](docs/COMMUNITY_BENCHMARKS.md).
|
||||
Based on a 12 GB graphics card (RTX 5070, 64 GB RAM). Results from other cards:
|
||||
[speed of each model](docs/MODELS.md#how-fast-is-each-size) and [community results](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
| Model | Answers appear at | Reads your prompt at |
|
||||
| --- | --- | --- |
|
||||
@@ -201,10 +187,7 @@ Based on a 12 GB graphics card (RTX 5070, Ryzen 5 7600, 64 GB RAM). See results
|
||||
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1,620 tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2,180 tokens/s |
|
||||
|
||||
A token is about ¾ of a word, so 60 tokens per second is faster than you can read. "Reads your prompt" is how fast Strata
|
||||
takes in a long document, code or chat history.
|
||||
|
||||
Measured on a 16 GB AMD card (RX 9070 XT, Ryzen 9 3900X, 47 GB RAM):
|
||||
On a 16 GB AMD card (RX 9070 XT, 47 GB RAM):
|
||||
|
||||
| Model | Answers appear at | Reads your prompt at |
|
||||
| --- | --- | --- |
|
||||
@@ -212,24 +195,19 @@ Measured on a 16 GB AMD card (RX 9070 XT, Ryzen 9 3900X, 47 GB RAM):
|
||||
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1,110 tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1,420 tokens/s |
|
||||
|
||||
The bars use one scale for both tables. A card with more VRAM is faster: an RTX 3090 (24 GB) should write about 100-140
|
||||
tokens per second. The engine version behind each number is in [DETAILS.md](docs/DETAILS.md#speed-measured).
|
||||
A token is about ¾ of a word, so 60 tokens per second is faster than you can read. "Reads your prompt" is how fast Strata
|
||||
takes in a long document, code or chat history.
|
||||
|
||||
## Using it
|
||||
|
||||
### In the app
|
||||
|
||||
The app has a **Chat**, a **Monitor** of the model and your GPU, CPU and RAM, a **Models** page and **Settings**. The chat
|
||||
can work in a folder on your PC. In **Manual** mode it asks before it changes a file or runs a command. Type
|
||||
`/loop 10m <what to do>` to repeat a message every 10 minutes while the chat is open.
|
||||
|
||||
The chat can search the web with DuckDuckGo (or Bing), with no account. You can add a search key under **Settings, Tools**.
|
||||
Search is the one feature that sends something off your PC, the search words.
|
||||
|
||||
To run two models, turn on **Run multiple models** and give each model its own card of the same make.
|
||||
Chat, a **Monitor** of the model and your GPU, CPU and RAM, a **Models** page and **Settings**. The chat can work in a
|
||||
folder on your PC. In **Manual** mode it asks before it changes a file or runs a command. It can search the web
|
||||
(DuckDuckGo or Bing, no account); the search words are the one thing that leaves your PC.
|
||||
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
|
||||
<sub>A chat in the Strata app: the agent wrote a small voxel pagoda garden as one HTML file, and the page opens in the preview next to the conversation</sub></p>
|
||||
<sub>The agent wrote a voxel pagoda garden as one HTML file, and the page opens in the preview next to the chat</sub></p>
|
||||
|
||||
### In the Terminal
|
||||
|
||||
@@ -237,57 +215,38 @@ To run two models, turn on **Run multiple models** and give each model its own c
|
||||
<sub><code>strata monitor</code> while the model writes a reply: speed, GPU, memory and the recent requests</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided screens: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
|
||||
strata launch --plan --model q2_0 # show what would happen; nothing changes
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
|
||||
strata update --check # is there a new engine? (strata update installs it)
|
||||
strata doctor # check the installed engine files
|
||||
strata launch # guided: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve and print the address
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor # watch speed, GPU, memory and requests
|
||||
strata update # get the newest engine
|
||||
strata doctor # check the installed files
|
||||
```
|
||||
|
||||
`strata --help` lists everything. Starting and serving never download anything. Only the install, `strata launch` (when
|
||||
you choose a model) and `strata update` do. To reach Strata from your phone or another PC, use
|
||||
`strata launch --access lan`. It makes an API key (recommended; add `--no-api-key` to go without, with a warning). No browser opens unless
|
||||
you add `--open`. Guide: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Whole client: [docs/TERMINAL.md](docs/TERMINAL.md).
|
||||
To reach Strata from your phone or another PC, use `strata launch --access lan`. It makes an API key for you.
|
||||
`strata --help` lists everything, and [docs/TERMINAL.md](docs/TERMINAL.md) explains it.
|
||||
|
||||
### With AI agents
|
||||
### With AI agents and other apps
|
||||
|
||||
An agent installs Strata with the one-line command in Option B and runs
|
||||
`strata launch --model NAME --yes --no-tui`. The steps, flags and exit codes are in [docs/AI_SETUP.md](docs/AI_SETUP.md),
|
||||
and the repository rules are in [AGENTS.md](AGENTS.md).
|
||||
An agent installs Strata with the one-line command above and runs `strata launch --model NAME --yes --no-tui`. Steps,
|
||||
flags and exit codes: [docs/AI_SETUP.md](docs/AI_SETUP.md). Repository rules for agents: [AGENTS.md](AGENTS.md).
|
||||
|
||||
### Connect your other apps
|
||||
|
||||
| App type | Base URL |
|
||||
| Your app speaks | Base URL |
|
||||
| --- | --- |
|
||||
| OpenAI-compatible (most apps and agents) | `http://127.0.0.1:8080/v1` (any API key, any model name) |
|
||||
| Anthropic API (Claude Code and others) | `http://127.0.0.1:8080`, without `/v1`. These apps add `/v1/messages` themselves. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI API (most apps and agents) | `http://127.0.0.1:8080/v1` (any API key, any model name) |
|
||||
| Anthropic API (Claude Code) | `http://127.0.0.1:8080`, without `/v1`. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([setup](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` prints the exact settings for OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent and OpenClaw.
|
||||
`strata connect opencode --write` writes the file after asking and keeps a backup.
|
||||
|
||||
### Pictures
|
||||
|
||||
Pictures are on by default for models that can read them. Attach or paste a picture in the chat, or send it from your app.
|
||||
The picture reader runs on the processor when your graphics card has less than 24 GB (a few seconds per picture, nothing
|
||||
taken from the card's memory) and on the graphics card from 24 GB. The rule is the same for NVIDIA and AMD. Turn pictures
|
||||
off per model, or choose the reader with `--vision off`, `--vision gpu` or `--vision cpu`.
|
||||
|
||||
The numbers behind the rule: on a Radeon AI PRO R9700 the card's reader took 57 ms for a 260-token picture and the
|
||||
processor's took 3.1 s. On 16 GB cards (Tesla P100 and RTX A4000, Linux) the card's reader cost 3-5% of text speed,
|
||||
because it holds about 1.4 GB that the model would use otherwise. On a Mac the reader runs on the Mac's GPU whatever its memory (0.45 s for a 260-token picture on an M4 with 32 GB, about 1.2 GB of the shared
|
||||
memory); the processor reader needs about 50 s per picture there, so it runs only when you ask for it with `--vision cpu`.
|
||||
`strata connect` prints the exact settings for OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent and OpenClaw,
|
||||
and `strata connect opencode --write` writes them for you (with a backup).
|
||||
|
||||
### Good to know
|
||||
|
||||
- Choose the thinking level (**off, low, medium or high**) in the chat menu or in your app's "reasoning effort". Off is fastest.
|
||||
- Strata answers one request at a time by default. For several at once, use `--parallel N` in `strata launch` or
|
||||
`"parallel": 2` in the config ([BATCHING.md](docs/BATCHING.md)). On a 12 GB card this makes each answer slower.
|
||||
- Strata reads the first message of a chat in full, about 1 minute per 30,000 tokens. Follow-up messages start in seconds.
|
||||
- Where your chats are stored and the whole API: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
|
||||
- **Pictures** are on for every model that can read them. Attach or paste one in the chat, or send it from your app. Details: [docs/IMAGES.md](docs/IMAGES.md).
|
||||
- **Thinking** can be off, low, medium or high, in the chat menu or your app's "reasoning effort". Off is fastest.
|
||||
- **The first message** of a chat is read in full, about 1 minute per 30,000 tokens. Follow-ups start in seconds.
|
||||
- **One request at a time** by default. For more, use `--parallel N` ([BATCHING.md](docs/BATCHING.md)). On a 12 GB card each answer gets slower.
|
||||
|
||||
## Build from source
|
||||
|
||||
@@ -363,26 +322,25 @@ The longer explanation: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Every part
|
||||
|
||||
- **My PC froze the first time Strata started.** It loads the model into RAM. Wait, and don't close the window. Still
|
||||
frozen after 10 minutes? Restart the PC, close other programs and try again, or pick a smaller size.
|
||||
- **It stopped while downloading or installing.** Start the app or `strata launch` again, or run `START-HERE.bat` (or
|
||||
`./setup.sh`) again. It continues where it stopped.
|
||||
- **It is very slow, the disk light keeps blinking, or it says "the engine stopped unexpectedly".** Your PC does not have
|
||||
enough free RAM. Close other programs (browsers use a lot), or pick a smaller size such as Q2_0 or IQ2_XS.
|
||||
- **It stopped while downloading or installing.** Start the app or `strata launch` again. It continues where it stopped.
|
||||
- **It is very slow, or it says "the engine stopped unexpectedly".** Your PC is short on free RAM. Close other programs
|
||||
(browsers use a lot), or pick a smaller size such as Q2_0 or IQ2_XS.
|
||||
- **It says there is no engine for this PC.** In the app, open **Settings, Engine** and press **Update engine**. In the
|
||||
Terminal, run `strata update`. `strata doctor` shows which engine files are installed.
|
||||
Terminal, run `strata update`.
|
||||
- **It says port 8080 is already in use.** Strata is already running. Look for its window.
|
||||
|
||||
More problems and fixes: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Still stuck? Open an
|
||||
[issue](https://github.com/Niko1221/Strata/issues). In the app, attach the file from **Settings, Engine, Diagnostics, View
|
||||
logs, Save logs**. In the Terminal, paste the output of `strata doctor`. From source, attach `strata-<model>.log` from the
|
||||
Strata folder. For a security problem, report it privately: [SECURITY.md](SECURITY.md).
|
||||
More fixes: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Still stuck? Open an
|
||||
[issue](https://github.com/Niko1221/Strata/issues) and attach the logs: in the app from **Settings, Engine, Diagnostics,
|
||||
Save logs**, in the Terminal the output of `strata doctor`. For a security problem, report it privately: [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Credits and license
|
||||
|
||||
The model is [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) by the Qwen team. It was compressed by
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5), agentionai (Gyro)
|
||||
and Unsloth. Strata uses parts of [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). All credits:
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata is open source under the [MIT License](LICENSE). A few parts and
|
||||
every model have their own licenses ([which ones](docs/HOW_IT_WORKS.md#license)).
|
||||
and Unsloth. ISTA-DASLab also made the DeepSeek V4.1-Flash sets. Strata uses parts of
|
||||
[llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). All credits: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits).
|
||||
Strata is open source under the [MIT License](LICENSE). A few parts and every model have their own licenses
|
||||
([which ones](docs/HOW_IT_WORKS.md#license)).
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
+137
-171
@@ -12,100 +12,90 @@
|
||||
<p align="center">Suporte a NVIDIA, AMD, Intel · Windows, Linux, Mac · gratuito e de código aberto</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Última versão"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licença MIT"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="Estrelas no GitHub"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=black" alt="Me pague um café"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Um jardim de pagodes em voxels que o modelo do Strata escreveu, rodando no navegador"></a><br>
|
||||
<sub>Um jardim de pagodes em voxels, gerado com um único prompt, rodando numa RTX 5070 com o Strata (IQ3_S, contexto de 128K) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">vídeo completo (49 s)</a></sub></p>
|
||||
|
||||
O Strata roda o **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, um modelo de 125 bilhões de parâmetros que
|
||||
normalmente precisa de um servidor, num PC comum. Ele conversa, escreve código, lê imagens e funciona com os seus aplicativos e
|
||||
agentes de programação. Suas conversas e arquivos ficam no seu PC.
|
||||
<p align="center">
|
||||
<a href="#instalação"><b>Instalação</b></a> ·
|
||||
<a href="#modelos">Modelos</a> ·
|
||||
<a href="#qual-é-a-velocidade">Velocidade</a> ·
|
||||
<a href="#como-usar">Como usar</a> ·
|
||||
<a href="#como-funciona">Como funciona</a> ·
|
||||
<a href="#algo-deu-errado">Ajuda</a> ·
|
||||
<a href="#apoie-o-strata">Apoiar</a>
|
||||
</p>
|
||||
|
||||
**Primeira vez aqui?** Veja [o que você precisa](#o-que-você-precisa) e depois [instale o app](#opção-a-o-app-strata).
|
||||
O Strata roda o **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**, um modelo de 125 bilhões de parâmetros que
|
||||
normalmente precisa de um servidor, num PC comum. A partir da 0.2.4 ele também roda o **DeepSeek V4.1-Flash** no Terminal. Ele
|
||||
conversa, escreve código, lê imagens e funciona com os seus aplicativos e agentes de programação. Suas conversas e arquivos
|
||||
ficam no seu PC.
|
||||
|
||||
- **Roda num PC gamer.** 32 GB somando memória RAM e memória da placa de vídeo já bastam para começar.
|
||||
- **Funciona com as suas ferramentas.** Ele fala as APIs da OpenAI e da Anthropic, então Claude Code, Codex CLI, OpenCode e outros agentes se conectam a ele.
|
||||
- **Lê imagens.** Já vêm ativadas.
|
||||
- **Gratuito.** Licença MIT, sem conta, sem nuvem.
|
||||
- **Privado.** Sem telemetria. O que o Strata baixa, quando usa a internet e como confere os downloads está em [docs/SAFETY.md](docs/SAFETY.md).
|
||||
|
||||
Versão 0.2.2 · mecanismo 0.1.47
|
||||
- **Funciona com as suas ferramentas.** Claude Code, Codex CLI, OpenCode e outros agentes se conectam pelas APIs da OpenAI e da Anthropic.
|
||||
- **Lê imagens**, ativado por padrão.
|
||||
- **Gratuito e privado.** Licença MIT, sem conta, sem nuvem, sem telemetria ([o que ele baixa e quando](docs/SAFETY.md)).
|
||||
|
||||
## O que você precisa
|
||||
|
||||
**Você precisa de pelo menos 32 GB somando a memória RAM e a memória da placa de vídeo (VRAM).** Num Mac ou num Ryzen AI Max, isso é a
|
||||
memória unificada. Mais memória permite um modelo maior ou mais rápido.
|
||||
**Você precisa de pelo menos 32 GB de memória: RAM e memória da placa de vídeo (VRAM) somadas.** Num Mac ou num Ryzen AI Max, isso é a
|
||||
memória unificada. Um PC de 32 GB com uma placa de 12 a 16 GB roda os tamanhos menores, e 64 GB rodam todos os tamanhos.
|
||||
|
||||
- Um PC de 32 GB com uma placa de 12 a 16 GB roda o Coder.
|
||||
- 64 GB de RAM rodam todos os tamanhos.
|
||||
|
||||
| Seu computador | Detalhes |
|
||||
| Funciona em | Guia |
|
||||
| --- | --- |
|
||||
| **NVIDIA** GeForce RTX série 20, 30, 40, 50 (12 GB ou mais) | Instale o app ou o Terminal. |
|
||||
| **AMD** Radeon RX 6800 e mais novas, Radeon AI PRO R9700 (12 GB ou mais) | [Placas AMD](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md), [mecanismo Intel](docs/INTEL.md) |
|
||||
| **Mac com Apple Silicon** (M1 ou mais novo, macOS 15 ou mais novo) | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** e outros sistemas de memória unificada | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Placas e processadores mais antigos | [GPUs antigas](docs/OLDER_GPUS.md), [CPUs antigas](docs/INSTALL.md#older-cpus-experimental) |
|
||||
| **NVIDIA** GeForce RTX série 20, 30, 40 e 50, 12 GB ou mais | [Instalação](#instalação) |
|
||||
| **AMD** Radeon RX 6800 e mais novas, Radeon AI PRO R9700 | [Placas AMD](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md) |
|
||||
| **Mac com Apple Silicon**, M1 ou mais novo, macOS 15 ou mais novo | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max (Strix Halo)** e outros PCs de memória unificada | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| Placas e processadores mais antigos | [GPUs antigas](docs/OLDER_GPUS.md) |
|
||||
|
||||
Você também precisa de:
|
||||
|
||||
- cerca de 80 GB de espaço livre no disco (um SSD deixa a primeira inicialização bem mais rápida)
|
||||
- Windows 10 / 11, Linux ou macOS
|
||||
- um driver de vídeo atualizado
|
||||
|
||||
Duas ou três placas da mesma marca podem dividir um modelo ([multi-GPU](docs/MULTI_GPU.md)). O Strata nunca divide um modelo entre
|
||||
placas NVIDIA e AMD. Lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
Você também precisa de cerca de 80 GB de espaço livre no disco (um SSD), Windows 10 / 11, Linux ou macOS, e um driver de vídeo
|
||||
atualizado. Duas ou três placas da mesma marca podem dividir um modelo ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
|
||||
## Instalação
|
||||
|
||||
Escolha uma opção. Todas iniciam o mesmo mecanismo e usam os mesmos modelos.
|
||||
### 1. O app Strata (o mais fácil)
|
||||
|
||||
### Opção A: o app Strata
|
||||
Baixe o arquivo para o seu computador, abra e clique em instalar no modelo que ele recomenda.
|
||||
|
||||
O jeito mais fácil. O app já traz o mecanismo: instale, escolha um modelo e converse. Baixe o arquivo para o seu sistema e a sua
|
||||
placa de vídeo na [página de releases](https://github.com/Niko1221/Strata/releases/latest). Não sabe qual? Veja a explicação
|
||||
logo abaixo da tabela.
|
||||
**Windows 10 / 11**
|
||||
|
||||
| Sistema | Sua placa de vídeo | Arquivo para baixar |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, driver 580 ou mais novo | [`Strata-0.2.2-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda13.exe) |
|
||||
| | NVIDIA, placa mais antiga ou driver anterior ao 580 | [`Strata-0.2.2-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda12.exe) |
|
||||
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.2-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip.exe) |
|
||||
| | AMD Radeon série RX 7000 ou Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, driver 580 ou mais novo | [`Strata-0.2.2-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, placa mais antiga ou driver anterior ao 580 | [`Strata-0.2.2-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.2-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon série RX 7000 | [`Strata-0.2.2-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 ou mais novo) | integrada | [`Strata-0.2.2-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-mac.dmg) |
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-windows-x64-setup.exe)
|
||||
|
||||
**Se você estiver em dúvida:** para NVIDIA, use `cuda13` se o seu driver for a versão 580 ou mais nova, e `cuda12` nos outros casos. O `cuda12`
|
||||
serve para placas e drivers mais antigos. Os arquivos `hip` e `amd` são para placas AMD. Outras placas AMD (RX 6000) ainda não têm app pronto: veja [Placas AMD](docs/AMD_HIP.md).
|
||||
**Linux**
|
||||
|
||||
Depois:
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-linux-x86_64.AppImage)
|
||||
|
||||
- **Windows:** execute o `.exe`. Ele instala só para o seu usuário e não pede direitos de administrador. Se o Windows disser "O Windows
|
||||
protegeu o computador", escolha **Mais informações** e depois **Executar assim mesmo** (o instalador ainda não tem assinatura de código).
|
||||
- **Linux:** torne o arquivo executável (`chmod +x`) e inicie. É preciso ter o FUSE 2: `sudo apt install libfuse2` (Ubuntu 22.04)
|
||||
ou `libfuse2t64` (Ubuntu 24.04 e mais novos).
|
||||
- **Mac:** abra o `.dmg` e arraste o Strata para Aplicativos.
|
||||
**Mac** (Apple Silicon, macOS 15 ou mais novo)
|
||||
|
||||
Na primeira tela, o app lê o seu PC e recomenda um modelo que caiba nele. Clique em instalar. O modelo é um download grande
|
||||
(cerca de 40 a 70 GB) e continua de onde parou se for interrompido. Fechar a janela encerra o Strata e o mecanismo.
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-macos-arm64.dmg)
|
||||
|
||||
### Opção B: o Terminal (uma linha)
|
||||
O aplicativo escolhe sozinho o mecanismo certo para a sua placa de vídeo. Todos os arquivos estão na
|
||||
[página de releases](https://github.com/Niko1221/Strata/releases/latest).
|
||||
|
||||
O Terminal é o comando `strata`, para scripts, servidores e agentes de IA.
|
||||
| Primeira inicialização | O que fazer |
|
||||
| --- | --- |
|
||||
| **Windows** | Execute o `.exe`. Não precisa de direitos de administrador. Se o Windows disser "O Windows protegeu o computador", escolha **Mais informações** e depois **Executar assim mesmo**. |
|
||||
| **Linux** | Torne o arquivo executável (`chmod +x`) e inicie. É preciso ter o FUSE 2: `sudo apt install libfuse2t64` (Ubuntu 24.04 e mais novos) ou `libfuse2` (22.04). |
|
||||
| **Mac** | Abra o `.dmg` e arraste o Strata para Aplicativos. |
|
||||
|
||||
O download do modelo é grande (40 a 70 GB). Se parar, continua de onde parou.
|
||||
|
||||
### 2. O Terminal (uma linha)
|
||||
|
||||
O comando `strata`, para scripts, servidores e agentes de IA. É também o jeito de rodar o DeepSeek.
|
||||
|
||||
Linux e Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
@@ -114,24 +104,17 @@ Windows (PowerShell):
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
O script confere o seu PC, baixa o Terminal, verifica o SHA-256 e descompacta na sua pasta pessoal. Não precisa de direitos de
|
||||
administrador. No Windows, abra uma nova janela do terminal depois. Então inicie o Strata:
|
||||
Depois inicie. Ele faz uma pergunta por vez (modelo, contexto, imagens, quem pode se conectar), baixa o que precisa e começa
|
||||
a servir:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
Ele faz uma pergunta por vez: modelo, contexto, imagens, quem pode se conectar. Depois baixa o mecanismo para a sua placa de vídeo
|
||||
(assinada e testada antes do uso) e o modelo, e começa a servir. Em scripts e agentes de IA, use flags:
|
||||
Não precisa de direitos de administrador. A instalação confere o SHA-256 de tudo que baixa. No Windows, abra uma nova janela do
|
||||
terminal depois da instalação. Todas as telas e flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
Para ler o script de instalação antes de executá-lo, baixe-o primeiro com `curl -fsSL <address> -o install.sh`, leia e então rode
|
||||
`sh install.sh`. Todas as telas e flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### Opção C: deixe a sua IA configurar
|
||||
### 3. Deixe a sua IA configurar
|
||||
|
||||
Cole isto no Claude Code, Cursor, Codex, GitHub Copilot ou outro assistente de programação:
|
||||
|
||||
@@ -146,47 +129,58 @@ Prefere compilar por conta própria? Veja [Compilar a partir do código-fonte](#
|
||||
|
||||
## Modelos
|
||||
|
||||
O app e o `strata launch` recomendam um modelo para a sua memória. É um único modelo em vários tamanhos, mais ou menos comprimidos.
|
||||
Os tamanhos menores são mais rápidos. Os maiores são um pouco mais inteligentes.
|
||||
O app e o `strata launch` recomendam um tamanho para a sua memória. Os tamanhos menores são mais rápidos. Os maiores são um
|
||||
pouco mais inteligentes.
|
||||
|
||||
### Pouca memória? Experimente o Gyro
|
||||
|
||||
O [Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (da agentionai) é a menor forma de rodar este modelo.
|
||||
O [Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF), da agentionai, é a menor forma de rodar o
|
||||
Qwen3.8-Flash-Next. Ele roda em NVIDIA, AMD, Intel Arc e Apple Silicon.
|
||||
|
||||
| Tamanho | Memória necessária (RAM + VRAM) | Download | O que é |
|
||||
| Tamanho | Memória necessária (RAM + VRAM) | Download | Serve para |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27,6 GB | 58,5 GB | Para PCs onde nenhum outro tamanho cabe. A qualidade é menor que a do IQ2_XS. |
|
||||
| **Gyro-M** | 35 GB | 92,0 GB | Qualidade próxima à do IQ3_XXS, com cerca de 20% menos memória da GPU. Ele escreve as respostas 16% mais devagar que o Q2_0 numa RTX 3060 de 12 GB e 21% mais devagar numa R9700. |
|
||||
| **Gyro-S** | 27,6 GB | 58,5 GB | PCs onde nenhum outro tamanho cabe. 9 tokens/s num Mac mini de 32 GB. |
|
||||
| **Gyro-M** | 35 GB | 92,0 GB | Qualidade próxima à do IQ3_XXS, com 20% menos memória da GPU. |
|
||||
|
||||
O Gyro roda em placas NVIDIA e AMD (AMD é experimental) em placas Intel Arc no Linux (testado numa Arc A750) e, desde a 0.2.2, em Macs com Apple Silicon (Gyro-S num Mac mini de 32 GB: 9 tokens/s, com os especialistas lidos do SSD). Detalhes e medições:
|
||||
[docs/GYRO.md](docs/GYRO.md). Inicie com `strata launch --model gyro-s`.
|
||||
Inicie com `strata launch --model gyro-s`. Detalhes: [docs/GYRO.md](docs/GYRO.md).
|
||||
|
||||
### Os outros tamanhos
|
||||
### Os tamanhos do Qwen3.8-Flash-Next
|
||||
|
||||
| Sua memória (RAM + VRAM) | Escolha | Observações |
|
||||
| Sua memória (RAM + VRAM) | Escolha | Por quê |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Gyro-S**, ou o **[Coder](docs/MODELS.md#coder)** para código | O Coder precisa de 32 GB de RAM e mantém metade dos experts, então é mais fraco fora de código |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), ou **Gyro-M** (35 GB) | O Swift pensa por menos tempo antes de responder. O Gyro-M chega perto do IQ3_XXS em qualidade, mas escreve as respostas 16 a 21% mais devagar que o Q2_0 |
|
||||
| **32 GB** | **Gyro-S**, ou o **[Coder](docs/MODELS.md#coder)** para código | O Coder mantém os experts que o código usa |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB) ou **Gyro-M** | O Swift pensa por menos tempo antes de responder |
|
||||
| **48 GB** | **IQ3_XXS** (47,0 GB) | Mais lento, melhor |
|
||||
| **64 GB** | **IQ3_S** (54,8 GB), o original ou o Swift 1.5 | O mais lento, o melhor: empata com o modelo completo nos testes publicados |
|
||||
| **96 GB ou mais** | **UD-IQ4_XS** da Unsloth (cerca de 72 GB de RAM) até **Q8_0** (cerca de 136 GB), de 4 a 8 bits | Mais perto do modelo completo. Com menos RAM, o Strata lê parte deles do SSD, o que é bem mais lento |
|
||||
| **64 GB** | **IQ3_S** (54,8 GB) | Empata com o modelo completo nos testes publicados |
|
||||
| **96 GB ou mais** | **UD-IQ4_XS** a **Q8_0** da Unsloth | O mais perto do modelo completo |
|
||||
|
||||
Com menos RAM do que a linha indica, ainda pode funcionar: o Strata muda sozinho para o
|
||||
[modo de pouca RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Espere mais lentidão, e muito mais com uma placa de vídeo pequena.
|
||||
Com menos RAM do que a linha indica, ainda pode funcionar, só que mais devagar: o Strata muda sozinho para o
|
||||
[modo de pouca RAM](docs/MODELS.md#a-big-graphics-card-and-little-ram). Há também:
|
||||
[NVFP4](docs/NVFP4.md) para placas NVIDIA, [Qwen3.6-35B-A3B](docs/QWEN36.md) (menor, vários pedidos ao mesmo tempo) e
|
||||
[o Uncensored IQ3_XXS da OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs). Todos os tamanhos e o que cabe em cada configuração:
|
||||
[docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
Mais opções:
|
||||
### DeepSeek V4.1-Flash (Terminal, a partir da 0.2.4)
|
||||
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** é um ajuste fino que pensa por menos tempo antes de responder, com qualidade e RAM parecidas.
|
||||
- **[NVFP4](docs/NVFP4.md)** é a versão de 4 bits da NVIDIA para placas NVIDIA. Precisa de cerca de 80 GB de RAM para manter todos os experts na RAM.
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** é outro modelo, menor, que lê imagens e pode atender vários pedidos ao mesmo tempo.
|
||||
- **[Uncensored IQ3_XXS da OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** é configurado manualmente.
|
||||
Um modelo bem maior, que roda numa única placa de vídeo de 12 GB com 46 a 64 GB de RAM. O Strata mantém na memória as partes mais
|
||||
usadas e lê o resto do seu SSD. A [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF)
|
||||
fez os conjuntos. Ele roda em placas NVIDIA RTX das séries 30, 40 e 50 e na Radeon AI PRO R9700. Escolha no `strata launch`. O
|
||||
app de desktop ainda não o oferece.
|
||||
|
||||
Todos os tamanhos, downloads e o que cabe em cada configuração: [docs/MODELS.md](docs/MODELS.md).
|
||||
| Conjunto | Download | Qualidade | Escreve código a |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M** (1,75 bits) | 239 GB | O menor, mais fraco em prosa simples | ainda não medido num PC |
|
||||
| **IQ2_XXS** (2.00 bits) | 257 GB | Bom equilíbrio | 10 tokens/s (RTX 3060), 5 (RTX 5070), 48 (4x R9700) |
|
||||
| **IQ2_XS** (2.25 bits) | 274 GB | O mais perto do original | 15 tokens/s (uma R9700), 80 (4x R9700) |
|
||||
| **IQ4_NL** (tabela Engram) | incluída em cada download acima | A tabela de consulta que os três conjuntos compartilham | |
|
||||
|
||||
Inicie com `strata launch --model ist-das-2.00bpw` (ou `ist-das-1.75bpw`, `ist-das-2.25bpw`). Números de qualidade,
|
||||
velocidades por PC e a instalação manual: [docs/DEEPSEEK41.md](docs/DEEPSEEK41.md).
|
||||
|
||||
## Qual é a velocidade?
|
||||
|
||||
Com base numa placa de vídeo de 12 GB (RTX 5070, Ryzen 5 7600, 64 GB de RAM). Veja resultados de outras placas:
|
||||
[velocidade de cada modelo](docs/MODELS.md#how-fast-is-each-size), [resultados da comunidade](docs/COMMUNITY_BENCHMARKS.md).
|
||||
Com base numa placa de vídeo de 12 GB (RTX 5070, 64 GB de RAM). Veja resultados de outras placas:
|
||||
[velocidade de cada tamanho](docs/MODELS.md#how-fast-is-each-size) e [resultados da comunidade](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
| Modelo | As respostas aparecem a | Lê o seu prompt a |
|
||||
| --- | --- | --- |
|
||||
@@ -196,10 +190,7 @@ Com base numa placa de vídeo de 12 GB (RTX 5070, Ryzen 5 7600, 64 GB de RAM). V
|
||||
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1.620 tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2.180 tokens/s |
|
||||
|
||||
Um token equivale a cerca de ¾ de palavra, então 60 tokens por segundo é mais rápido do que você lê. "Lê o seu prompt" é a velocidade com que o Strata
|
||||
absorve um documento longo, um código ou um histórico de conversa.
|
||||
|
||||
Medido numa placa AMD de 16 GB (RX 9070 XT, Ryzen 9 3900X, 47 GB de RAM):
|
||||
Numa placa AMD de 16 GB (RX 9070 XT, 47 GB de RAM):
|
||||
|
||||
| Modelo | As respostas aparecem a | Lê o seu prompt a |
|
||||
| --- | --- | --- |
|
||||
@@ -207,82 +198,58 @@ Medido numa placa AMD de 16 GB (RX 9070 XT, Ryzen 9 3900X, 47 GB de RAM):
|
||||
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1.110 tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1.420 tokens/s |
|
||||
|
||||
As barras usam a mesma escala nas duas tabelas. Uma placa com mais VRAM é mais rápida: uma RTX 3090 (24 GB) deve escrever cerca de 100 a 140
|
||||
tokens por segundo. A versão do mecanismo por trás de cada número está em [DETAILS.md](docs/DETAILS.md#speed-measured).
|
||||
Um token equivale a cerca de ¾ de palavra, então 60 tokens por segundo é mais rápido do que você lê. "Lê o seu prompt" é a velocidade com que o Strata
|
||||
absorve um documento longo, um código ou um histórico de conversa.
|
||||
|
||||
## Como usar
|
||||
|
||||
### No app
|
||||
|
||||
O app tem um **Chat**, um **Monitor** do modelo e da sua GPU, CPU e RAM, uma página de **Modelos** e as **Configurações**. O chat
|
||||
pode trabalhar numa pasta do seu PC. No modo **Manual**, ele pergunta antes de alterar um arquivo ou executar um comando. Digite
|
||||
`/loop 10m <o que fazer>` para repetir uma mensagem a cada 10 minutos enquanto o chat estiver aberto.
|
||||
Um chat, um **Monitor** do modelo e da sua GPU, CPU e RAM, uma página de **Modelos** e as **Configurações**. O chat pode
|
||||
trabalhar numa pasta do seu PC. No modo **Manual**, ele pergunta antes de alterar um arquivo ou executar um comando. Ele pode
|
||||
pesquisar na web (DuckDuckGo ou Bing, sem conta). Os termos da pesquisa são a única coisa que sai do seu PC.
|
||||
|
||||
O chat pode pesquisar na web com o DuckDuckGo (ou o Bing), sem conta. Você pode adicionar uma chave de busca em **Configurações, Ferramentas**.
|
||||
A busca é o único recurso que envia algo para fora do seu PC: os termos da pesquisa.
|
||||
|
||||
Para rodar dois modelos, ative **Executar vários modelos** e dê a cada modelo a sua própria placa da mesma marca.
|
||||
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
|
||||
<sub>Um chat no app Strata: o agente escreveu um pequeno jardim de pagode em voxels em um único arquivo HTML, e a página abre na pré-visualização ao lado da conversa</sub></p>
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="O app Strata: um chat em que o agente montou um pequeno jardim de pagodes em voxels, com a página aberta na pré-visualização à direita"><br>
|
||||
<sub>O agente escreveu um jardim de pagodes em voxels em um único arquivo HTML, e a página abre na pré-visualização ao lado do chat</sub></p>
|
||||
|
||||
### No Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="A visão geral do monitor do Terminal do Strata enquanto o modelo escreve uma resposta"><br>
|
||||
<sub><code>strata monitor</code> enquanto o modelo responde: velocidade, GPU, memória e as solicitações recentes</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # telas guiadas: modelo, contexto, imagens, acesso e depois servir
|
||||
strata launch --model q2_0 --yes --no-tui # sem perguntas: serve, mostra o endereço e volta
|
||||
strata launch --plan --model q2_0 # mostra o que aconteceria; nada muda
|
||||
strata launch --status # o que está sendo servido agora
|
||||
strata launch --stop # para o serviço
|
||||
strata monitor --url http://127.0.0.1:8080 # acompanha velocidade, GPU, memória e requisições
|
||||
strata update --check # há um mecanismo novo? (strata update instala)
|
||||
strata doctor # confere os arquivos do mecanismo instalada
|
||||
strata launch # guided: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve and print the address
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor # watch speed, GPU, memory and requests
|
||||
strata update # get the newest engine
|
||||
strata doctor # check the installed files
|
||||
```
|
||||
|
||||
`strata --help` lista tudo. Iniciar e servir nunca baixam nada. Só a instalação, o `strata launch` (quando você
|
||||
escolhe um modelo) e o `strata update` baixam. Para acessar o Strata do celular ou de outro PC, use
|
||||
`strata launch --access lan`. Ele cria uma chave de API e não inicia numa rede sem uma. Nenhum navegador abre, a menos que
|
||||
você adicione `--open`. Guia: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Cliente completo: [docs/TERMINAL.md](docs/TERMINAL.md).
|
||||
Para acessar o Strata do celular ou de outro PC, use `strata launch --access lan`. Ele cria uma chave de API para você.
|
||||
`strata --help` lista tudo, e o [docs/TERMINAL.md](docs/TERMINAL.md) explica.
|
||||
|
||||
### Com agentes de IA
|
||||
### Com agentes de IA e outros aplicativos
|
||||
|
||||
Um agente instala o Strata com o comando de uma linha da Opção B e executa
|
||||
`strata launch --model NAME --yes --no-tui`. Os passos, as flags e os códigos de saída estão em [docs/AI_SETUP.md](docs/AI_SETUP.md),
|
||||
e as regras do repositório estão em [AGENTS.md](AGENTS.md).
|
||||
Um agente instala o Strata com o comando de uma linha acima e executa `strata launch --model NAME --yes --no-tui`. Os passos,
|
||||
as flags e os códigos de saída estão em [docs/AI_SETUP.md](docs/AI_SETUP.md). As regras do repositório para agentes: [AGENTS.md](AGENTS.md).
|
||||
|
||||
### Conecte os seus outros aplicativos
|
||||
|
||||
| Tipo de aplicativo | URL base |
|
||||
| Seu aplicativo fala | URL base |
|
||||
| --- | --- |
|
||||
| Compatível com OpenAI (a maioria dos apps e agentes) | `http://127.0.0.1:8080/v1` (qualquer chave de API, qualquer nome de modelo) |
|
||||
| API da Anthropic (Claude Code e outros) | `http://127.0.0.1:8080`, sem `/v1`. Esses aplicativos adicionam `/v1/messages` sozinhos. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| API da OpenAI (a maioria dos apps e agentes) | `http://127.0.0.1:8080/v1` (qualquer chave de API, qualquer nome de modelo) |
|
||||
| API da Anthropic (Claude Code) | `http://127.0.0.1:8080`, sem `/v1`. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([configuração](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` mostra as configurações exatas para OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent e OpenClaw.
|
||||
`strata connect opencode --write` grava o arquivo depois de perguntar e mantém um backup.
|
||||
|
||||
### Imagens
|
||||
|
||||
As imagens vêm ativadas nos modelos que sabem lê-las. Anexe ou cole uma imagem no chat, ou envie pelo seu aplicativo.
|
||||
O leitor de imagens roda no processador quando a sua placa de vídeo tem menos de 24 GB (alguns segundos por imagem, sem tirar nada
|
||||
da memória da placa) e roda na placa de vídeo a partir de 24 GB. A regra é a mesma para NVIDIA e AMD. Desative as imagens
|
||||
por modelo, ou escolha o leitor com `--vision off`, `--vision gpu` ou `--vision cpu`.
|
||||
|
||||
Os números por trás da regra: numa Radeon AI PRO R9700, o leitor da placa levou 57 ms para uma imagem de 260 tokens e o do
|
||||
processador levou 3,1 s. Em placas de 16 GB (Tesla P100 e RTX A4000, Linux), o leitor da placa custou de 3 a 5% da velocidade do texto,
|
||||
porque ocupa cerca de 1,4 GB que o modelo usaria de outra forma. No Mac, a GPU faz a leitura, seja qual for a memória (0,45 s para uma imagem de 260 tokens em um M4 de 32 GB, cerca de 1,2 GB da memória compartilhada);
|
||||
o processador leva cerca de 50 s por imagem, então só lê com `--vision cpu`.
|
||||
`strata connect` mostra as configurações exatas para OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent e OpenClaw,
|
||||
e `strata connect opencode --write` grava para você (com um backup).
|
||||
|
||||
### Bom saber
|
||||
|
||||
- Escolha o nível de raciocínio (**off, low, medium ou high**) no menu do chat ou no "reasoning effort" do seu aplicativo. Off é o mais rápido.
|
||||
- Por padrão, o Strata responde a um pedido por vez. Para vários ao mesmo tempo, use `--parallel N` no `strata launch` ou
|
||||
`"parallel": 2` na configuração ([BATCHING.md](docs/BATCHING.md)). Numa placa de 12 GB, isso deixa cada resposta mais lenta.
|
||||
- O Strata lê a primeira mensagem de uma conversa por inteiro, cerca de 1 minuto a cada 30.000 tokens. As mensagens seguintes começam em segundos.
|
||||
- Onde ficam as suas conversas e a API completa: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
|
||||
- **As imagens** vêm ativadas em todo modelo que sabe lê-las. Anexe ou cole uma imagem no chat, ou envie pelo seu aplicativo. Detalhes: [docs/IMAGES.md](docs/IMAGES.md).
|
||||
- **O raciocínio** pode ser off, low, medium ou high, no menu do chat ou no "reasoning effort" do seu aplicativo. Off é o mais rápido.
|
||||
- **A primeira mensagem** de uma conversa é lida por inteiro, cerca de 1 minuto a cada 30.000 tokens. As mensagens seguintes começam em segundos.
|
||||
- **Um pedido por vez** é o padrão. Para mais de um, use `--parallel N` ([BATCHING.md](docs/BATCHING.md)). Numa placa de 12 GB, isso deixa cada resposta mais lenta.
|
||||
|
||||
## Compilar a partir do código-fonte
|
||||
|
||||
@@ -343,13 +310,13 @@ Modelos como este costumam rodar em servidores com centenas de gigabytes de mem
|
||||
- **O seu SSD é o arquivo.** Ele guarda uma tabela de consulta de 29 GB. O modelo lê algumas linhas dela por palavra, então ela não
|
||||
precisa ficar na RAM.
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="Os 24.576 experts do modelo: os mais usados na placa de vídeo, todos na RAM, uma tabela de consulta no SSD"></p>
|
||||
|
||||
Um pequeno ajudante também adivinha as próximas palavras. O modelo grande confere todas de uma vez e fica com as certas. Você recebe
|
||||
a mesma resposta, de 1,6 a 1,8 vez mais rápido. Textos longos são lidos em blocos grandes, de até 8.192 tokens por vez, a mais de 1.000 tokens
|
||||
por segundo.
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="Um pequeno ajudante adivinha as próximas palavras; o modelo grande confere todas de uma vez e fica com as certas"></p>
|
||||
|
||||
A explicação mais longa: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte e seus números:
|
||||
[os detalhes](docs/DETAILS.md#how-it-works) e o [artigo](docs/paper/Strata-Paper.pdf).
|
||||
@@ -358,26 +325,25 @@ A explicação mais longa: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada pa
|
||||
|
||||
- **Meu PC travou na primeira vez que o Strata iniciou.** Ele carrega o modelo na RAM. Espere e não feche a janela. Ainda
|
||||
travado depois de 10 minutos? Reinicie o PC, feche outros programas e tente de novo, ou escolha um tamanho menor.
|
||||
- **Parou durante o download ou a instalação.** Abra o app ou o `strata launch` de novo, ou execute `START-HERE.bat` (ou
|
||||
`./setup.sh`) outra vez. Ele continua de onde parou.
|
||||
- **Está muito lento, a luz do disco não para de piscar, ou aparece "o mecanismo parou inesperadamente".** O seu PC não tem
|
||||
RAM livre suficiente. Feche outros programas (os navegadores usam muita), ou escolha um tamanho menor, como Q2_0 ou IQ2_XS.
|
||||
- **Parou durante o download ou a instalação.** Abra o app ou o `strata launch` de novo. Ele continua de onde parou.
|
||||
- **Está muito lento, ou aparece "the engine stopped unexpectedly".** O seu PC não tem RAM livre suficiente. Feche outros
|
||||
programas (os navegadores usam muita), ou escolha um tamanho menor, como Q2_0 ou IQ2_XS.
|
||||
- **Diz que não há mecanismo para este PC.** No app, abra **Configurações, Mecanismo** e clique em **Atualizar mecanismo**. No
|
||||
Terminal, execute `strata update`. O `strata doctor` mostra quais arquivos do mecanismo estão instalados.
|
||||
Terminal, execute `strata update`.
|
||||
- **Diz que a porta 8080 já está em uso.** O Strata já está rodando. Procure a janela dele.
|
||||
|
||||
Mais problemas e soluções: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Ainda sem resolver? Abra uma
|
||||
[issue](https://github.com/Niko1221/Strata/issues). No app, anexe o arquivo de **Configurações, Mecanismo, Diagnóstico, Ver
|
||||
registros, Salvar logs**. No Terminal, cole a saída de `strata doctor`. Se estiver rodando pelo código-fonte, anexe o `strata-<model>.log` da pasta do
|
||||
Strata. Para um problema de segurança, informe em particular: [SECURITY.md](SECURITY.md).
|
||||
Mais soluções: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Ainda sem resolver? Abra uma
|
||||
[issue](https://github.com/Niko1221/Strata/issues) e anexe os logs: no app, em **Configurações, Mecanismo, Diagnóstico,
|
||||
Salvar logs**; no Terminal, a saída de `strata doctor`. Para um problema de segurança, informe em particular: [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Créditos e licença
|
||||
|
||||
O modelo é o [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next), da equipe Qwen. Ele foi comprimido por
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF), UkisAI (Swift 1.5), agentionai (Gyro)
|
||||
e Unsloth. O Strata usa partes do [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Todos os créditos:
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). O Strata é de código aberto sob a [Licença MIT](LICENSE). Algumas partes e
|
||||
todos os modelos têm licenças próprias ([quais](docs/HOW_IT_WORKS.md#license)).
|
||||
e Unsloth. A ISTA-DASLab também fez os conjuntos do DeepSeek V4.1-Flash. O Strata usa partes do
|
||||
[llama.cpp / ggml](https://github.com/ggml-org/llama.cpp). Todos os créditos: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits).
|
||||
O Strata é de código aberto sob a [Licença MIT](LICENSE). Algumas partes e todos os modelos têm licenças próprias
|
||||
([quais](docs/HOW_IT_WORKS.md#license)).
|
||||
|
||||
## Contribuindo
|
||||
|
||||
|
||||
+124
-161
@@ -12,100 +12,87 @@
|
||||
<p align="center">支持 NVIDIA、AMD、Intel · Windows、Linux、Mac · 免费开源</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="Latest release"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub stars"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?include_prereleases&label=release" alt="最新版本"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT 许可证"></a>
|
||||
<a href="https://github.com/Niko1221/Strata/stargazers"><img src="https://img.shields.io/github/stars/Niko1221/Strata?style=flat" alt="GitHub 星标"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?logo=buymeacoffee&logoColor=black" alt="请我喝杯咖啡"></a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="Strata 的模型写出的体素宝塔花园,在浏览器里运行"></a><br>
|
||||
<sub>一座体素宝塔花园,由一条提示词一次生成,在装有 RTX 5070 的电脑上用 Strata 运行(IQ3_S,128K 上下文) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">完整视频(49 秒)</a></sub></p>
|
||||
|
||||
Strata 运行 **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**。这是一个 1250 亿参数的模型,
|
||||
通常需要服务器才能跑,现在用普通电脑就行。它能聊天、写代码、看图片,也能配合你的应用和编程智能体使用。
|
||||
你的聊天记录和文件都留在你自己的电脑上。
|
||||
<p align="center">
|
||||
<a href="#安装"><b>安装</b></a> ·
|
||||
<a href="#模型">模型</a> ·
|
||||
<a href="#速度如何">速度</a> ·
|
||||
<a href="#使用方法">使用方法</a> ·
|
||||
<a href="#工作原理">工作原理</a> ·
|
||||
<a href="#出问题了">帮助</a> ·
|
||||
<a href="#支持-strata">支持</a>
|
||||
</p>
|
||||
|
||||
**第一次用?** 先看[硬件要求](#硬件要求),再[安装应用](#方式-astrata-应用)。
|
||||
Strata 运行 **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**。这是一个 1250 亿参数的模型,
|
||||
通常需要服务器才能跑,现在用普通电脑就行。从 0.2.4 起,它在终端版里也能运行 **DeepSeek V4.1-Flash**。它能聊天、写代码、
|
||||
看图片,也能配合你的应用和编程智能体使用。你的聊天记录和文件都留在你自己的电脑上。
|
||||
|
||||
- **游戏电脑就能跑。** 内存和显存加起来有 32 GB 就可以开始。
|
||||
- **能接入你的工具。** 支持 OpenAI 和 Anthropic 的 API,Claude Code、Codex CLI、OpenCode 等智能体都能连上。
|
||||
- **能看图片。** 默认开启。
|
||||
- **免费。** MIT 许可证,不用注册账号,不走云端。
|
||||
- **隐私。** 没有遥测。Strata 下载什么、什么时候联网、如何校验下载内容,都写在 [docs/SAFETY.md](docs/SAFETY.md)。
|
||||
|
||||
版本 0.2.2 · 引擎 0.1.47
|
||||
- **能接入你的工具。** Claude Code、Codex CLI、OpenCode 等智能体通过 OpenAI 和 Anthropic 的 API 连接。
|
||||
- **能看图片**,默认开启。
|
||||
- **免费且隐私。** MIT 许可证,不用注册账号,不走云端,没有遥测([它下载什么、什么时候下载](docs/SAFETY.md))。
|
||||
|
||||
## 硬件要求
|
||||
|
||||
**内存和显存(VRAM)加起来至少要有 32 GB。** Mac 和 Ryzen AI Max 上指的是统一内存。内存越大,能跑的模型越大,
|
||||
或者速度越快。
|
||||
**内存和显存(VRAM)加起来至少要有 32 GB。** Mac 和 Ryzen AI Max 上指的是统一内存。32 GB 内存加一张 12-16 GB 显存的显卡,
|
||||
可以跑较小的尺寸;64 GB 可以跑所有尺寸。
|
||||
|
||||
- 32 GB 内存加一张 12-16 GB 显存的显卡,可以跑 Coder。
|
||||
- 64 GB 内存可以跑所有尺寸。
|
||||
|
||||
| 你的电脑 | 说明 |
|
||||
| 适用于 | 指南 |
|
||||
| --- | --- |
|
||||
| **NVIDIA** GeForce RTX 20、30、40、50 系列(12 GB 及以上) | 安装应用或终端版即可。 |
|
||||
| **AMD** Radeon RX 6800 及更新型号、Radeon AI PRO R9700(12 GB 及以上) | [AMD 显卡](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md)、[Intel 引擎](docs/INTEL.md) |
|
||||
| **Apple 芯片 Mac**(M1 及更新,macOS 15 及以上) | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max(Strix Halo)** 及其他统一内存系统 | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| 较旧的显卡和处理器 | [旧显卡](docs/OLDER_GPUS.md)、[旧处理器](docs/INSTALL.md#older-cpus-experimental) |
|
||||
| **NVIDIA** GeForce RTX 20、30、40 和 50 系列,12 GB 及以上 | [安装](#安装) |
|
||||
| **AMD** Radeon RX 6800 及更新型号、Radeon AI PRO R9700 | [AMD 显卡](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md) |
|
||||
| **Apple 芯片 Mac**,M1 及更新,macOS 15 及以上 | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max(Strix Halo)** 及其他统一内存的电脑 | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| 较旧的显卡和处理器 | [旧显卡](docs/OLDER_GPUS.md) |
|
||||
|
||||
另外还需要:
|
||||
|
||||
- 约 80 GB 的可用磁盘空间(用固态硬盘,首次启动会快很多)
|
||||
- Windows 10 / 11、Linux 或 macOS
|
||||
- 较新的显卡驱动
|
||||
|
||||
同一品牌的两三张显卡可以共同运行一个模型([多显卡](docs/MULTI_GPU.md))。Strata 不会把一个模型拆到 NVIDIA 和 AMD
|
||||
显卡上。完整列表见 [docs/INSTALL.md](docs/INSTALL.md#what-you-need)。
|
||||
你还需要约 80 GB 的可用磁盘空间(固态硬盘)、Windows 10 / 11、Linux 或 macOS,以及较新的显卡驱动。同一品牌的两三张显卡可以共同运行一个模型([多显卡](docs/MULTI_GPU.md))。
|
||||
|
||||
## 安装
|
||||
|
||||
三种方式任选一种。它们启动的是同一个引擎,用的是同样的模型。
|
||||
### 1. Strata 应用(最简单)
|
||||
|
||||
### 方式 A:Strata 应用
|
||||
下载适合你电脑的文件,打开它,然后对推荐的模型点击安装。
|
||||
|
||||
最简单的方式。应用内置引擎:安装、选模型、开始聊天。请根据你的系统和显卡,从
|
||||
[发布页](https://github.com/Niko1221/Strata/releases/latest)下载对应的文件。不确定选哪个?看表格下面那一段。
|
||||
**Windows 10 / 11**
|
||||
|
||||
| 系统 | 你的显卡 | 要下载的文件 |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX,驱动 580 或更新 | [`Strata-0.2.2-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda13.exe) |
|
||||
| | NVIDIA,较旧的显卡,或驱动低于 580 | [`Strata-0.2.2-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda12.exe) |
|
||||
| | AMD Radeon RX 9000 系列或 Radeon AI PRO R9700 | [`Strata-0.2.2-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip.exe) |
|
||||
| | AMD Radeon RX 7000 系列或 Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX,驱动 580 或更新 | [`Strata-0.2.2-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA,较旧的显卡,或驱动低于 580 | [`Strata-0.2.2-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon RX 9000 系列或 Radeon AI PRO R9700 | [`Strata-0.2.2-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon RX 7000 系列 | [`Strata-0.2.2-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac**(Apple 芯片,macOS 15 及以上) | 内置 | [`Strata-0.2.2-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-mac.dmg) |
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-windows-x64-setup.exe)
|
||||
|
||||
**不确定时:** NVIDIA 显卡的驱动版本在 580 及以上就选 `cuda13`,否则选 `cuda12`。`cuda12` 适用于较旧的显卡和驱动。
|
||||
`hip` 和 `amd` 的文件是给 AMD 显卡用的。其他 AMD 显卡(RX 6000)目前还没有现成的应用,
|
||||
请看 [AMD 显卡](docs/AMD_HIP.md)。
|
||||
**Linux**
|
||||
|
||||
然后:
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-linux-x86_64.AppImage)
|
||||
|
||||
- **Windows:** 运行 `.exe`。它只为当前用户安装,不需要管理员权限。如果 Windows 提示“Windows 已保护你的电脑”,
|
||||
点击**更多信息**,再点**仍要运行**(安装程序还没有代码签名)。
|
||||
- **Linux:** 给文件加上可执行权限(`chmod +x`),然后启动。需要 FUSE 2:`sudo apt install libfuse2`(Ubuntu 22.04),
|
||||
或 `libfuse2t64`(Ubuntu 24.04 及更新)。
|
||||
- **Mac:** 打开 `.dmg`,把 Strata 拖到“应用程序”。
|
||||
**Mac**(Apple 芯片,macOS 15 及以上)
|
||||
|
||||
第一个界面里,应用会检测你的电脑,并推荐合适的模型。点击安装即可。模型很大,要下载约 40-70 GB,中断后会从断点继续。
|
||||
关闭窗口会同时停止 Strata 和引擎。
|
||||
[](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.5-macos-arm64.dmg)
|
||||
|
||||
### 方式 B:终端(一行命令)
|
||||
应用会自己为你的显卡选择合适的引擎。所有文件都在[发布页](https://github.com/Niko1221/Strata/releases/latest)。
|
||||
|
||||
终端版就是 `strata` 命令,适合脚本、服务器和 AI 智能体。
|
||||
| 首次启动 | 怎么做 |
|
||||
| --- | --- |
|
||||
| **Windows** | 运行 `.exe`。不需要管理员权限。如果 Windows 提示“Windows 已保护你的电脑”,点击**更多信息**,再点**仍要运行**。 |
|
||||
| **Linux** | 给文件加上可执行权限(`chmod +x`),然后启动。需要 FUSE 2:`sudo apt install libfuse2t64`(Ubuntu 24.04 及更新)或 `libfuse2`(22.04)。 |
|
||||
| **Mac** | 打开 `.dmg`,把 Strata 拖到“应用程序”。 |
|
||||
|
||||
模型下载很大(40-70 GB)。如果中断,会从断点继续。
|
||||
|
||||
### 2. 终端版(一行命令)
|
||||
|
||||
`strata` 命令,适合脚本、服务器和 AI 智能体。运行 DeepSeek 也要用它。
|
||||
|
||||
Linux 和 Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
```
|
||||
|
||||
Windows(PowerShell):
|
||||
@@ -114,24 +101,16 @@ Windows(PowerShell):
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
脚本会检查你的电脑,下载终端版,校验 SHA-256,然后解压到你的用户目录。不需要管理员权限。在 Windows 上,装完后请
|
||||
打开一个新的终端窗口。然后启动 Strata:
|
||||
然后启动它。它会一次问一个问题(模型、上下文、图片、允许谁连接),下载需要的东西,并开始提供服务:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
它会一次问一个问题:模型、上下文、图片、允许谁连接。接着为你的显卡下载引擎(已签名,使用前会先试启动),再下载
|
||||
模型,并开始提供服务。在脚本和 AI 智能体里,请使用参数:
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
如果想在运行前先看看安装脚本,先用 `curl -fsSL <address> -o install.sh` 下载,读完后再用 `sh install.sh` 运行。
|
||||
不需要管理员权限。安装程序会校验它下载的所有内容的 SHA-256。在 Windows 上,装完后请打开一个新的终端窗口。
|
||||
所有界面和参数见 [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md)。
|
||||
|
||||
### 方式 C:让你的 AI 来安装
|
||||
### 3. 让你的 AI 来安装
|
||||
|
||||
把下面这段话粘贴到 Claude Code、Cursor、Codex、GitHub Copilot 或其他编程助手里:
|
||||
|
||||
@@ -146,46 +125,55 @@ Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow doc
|
||||
|
||||
## 模型
|
||||
|
||||
应用和 `strata launch` 会按你的内存推荐模型。它是同一个模型的几种尺寸,压缩程度不同。尺寸小的更快,尺寸大的稍微更聪明一点。
|
||||
应用和 `strata launch` 会按你的内存推荐尺寸。尺寸小的更快,尺寸大的稍微更聪明一点。
|
||||
|
||||
### 内存不够?试试 Gyro
|
||||
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)(作者 agentionai)是运行这个模型占用最小的方式。
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)(作者 agentionai)是运行 Qwen3.8-Flash-Next 占用最小的方式。
|
||||
它可在 NVIDIA、AMD、Intel Arc 和 Apple 芯片上运行。
|
||||
|
||||
| 尺寸 | 所需内存(RAM + VRAM) | 下载大小 | 说明 |
|
||||
| 尺寸 | 所需内存(RAM + VRAM) | 下载大小 | 适合 |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27.6 GB | 58.5 GB | 给其他尺寸都放不下的电脑。质量低于 IQ2_XS。 |
|
||||
| **Gyro-M** | 35 GB | 92.0 GB | 质量约相当于 IQ3_XXS,显存占用少约 20%。输出回答比 Q2_0 慢,12 GB 的 RTX 3060 上慢 16%,R9700 上慢 21%。 |
|
||||
| **Gyro-S** | 27.6 GB | 58.5 GB | 其他尺寸都放不下的电脑。32 GB 的 Mac mini 上每秒 9 个 token。 |
|
||||
| **Gyro-M** | 35 GB | 92.0 GB | 质量约相当于 IQ3_XXS,显存占用少 20%。 |
|
||||
|
||||
Gyro 可在 NVIDIA 和 AMD 显卡(AMD 为实验性支持)以及 Linux 上的 Intel Arc 显卡(已在 Arc A750 上测试)上运行,自 0.2.2 起也可在 Apple Silicon 的 Mac 上运行(32 GB 的 Mac mini 上 Gyro-S 为 9 token/秒,专家权重从 SSD 读取)。详情和测试数据:
|
||||
[docs/GYRO.md](docs/GYRO.md)。用 `strata launch --model gyro-s` 启动。
|
||||
用 `strata launch --model gyro-s` 启动。详情:[docs/GYRO.md](docs/GYRO.md)。
|
||||
|
||||
### 其他尺寸
|
||||
### Qwen3.8-Flash-Next 的尺寸
|
||||
|
||||
| 你的内存(RAM + VRAM) | 选择 | 备注 |
|
||||
| 你的内存(RAM + VRAM) | 选择 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Gyro-S**,写代码可选 **[Coder](docs/MODELS.md#coder)** | Coder 需要 32 GB 内存,只保留一半的专家,所以代码以外的能力较弱 |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS(39.2 GB),或 **Gyro-M**(35 GB) | Swift 回答前思考的时间更短。Gyro-M 的质量接近 IQ3_XXS,但输出回答比 Q2_0 慢 16-21% |
|
||||
| **32 GB** | **Gyro-S**,写代码可选 **[Coder](docs/MODELS.md#coder)** | Coder 保留代码用到的专家 |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS(39.2 GB)或 **Gyro-M** | Swift 回答前思考的时间更短 |
|
||||
| **48 GB** | **IQ3_XXS**(47.0 GB) | 更慢,更好 |
|
||||
| **64 GB** | **IQ3_S**(54.8 GB),原版或 Swift 1.5 | 最慢,最好:在公开测试中与完整模型持平 |
|
||||
| **96 GB 及以上** | Unsloth 的 **UD-IQ4_XS**(约 72 GB 内存)到 **Q8_0**(约 136 GB),4 到 8 位 | 更接近完整模型。内存不足时,Strata 会从固态硬盘读取一部分,速度会慢很多 |
|
||||
| **64 GB** | **IQ3_S**(54.8 GB) | 在公开测试中与完整模型持平 |
|
||||
| **96 GB 及以上** | Unsloth 的 **UD-IQ4_XS** 到 **Q8_0** | 最接近完整模型 |
|
||||
|
||||
内存比这一行写的少,也可能能用:Strata 会自动切换到[低内存模式](docs/MODELS.md#a-big-graphics-card-and-little-ram)。
|
||||
速度会变慢,显卡较小时会慢得多。
|
||||
内存比这一行写的少,也可能能用,只是更慢:Strata 会自动切换到[低内存模式](docs/MODELS.md#a-big-graphics-card-and-little-ram)。
|
||||
另外还有:给 NVIDIA 显卡用的 [NVFP4](docs/NVFP4.md)、[Qwen3.6-35B-A3B](docs/QWEN36.md)(更小,能同时处理多个请求),
|
||||
以及 [OrcaRouter 的 Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)。所有尺寸,以及各种配置能跑什么:
|
||||
[docs/MODELS.md](docs/MODELS.md)。
|
||||
|
||||
更多选择:
|
||||
### DeepSeek V4.1-Flash(终端版,0.2.4 起)
|
||||
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** 是微调版本,回答前思考的时间更短,质量和内存占用大致相同。
|
||||
- **[NVFP4](docs/NVFP4.md)** 是 NVIDIA 为 NVIDIA 显卡提供的 4 位版本。要把所有专家放进内存,需要约 80 GB 内存。
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** 是另一个更小的模型,能看图片,还能同时回答多个请求。
|
||||
- **[OrcaRouter 的 Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** 需要手动设置。
|
||||
一个大得多的模型,用一块 12 GB 显卡加 46 到 64 GB 内存就能运行。Strata 把最常用的部分留在内存里,其余的从你的固态硬盘读取。
|
||||
这些版本由 [ISTA-DASLab](https://huggingface.co/ISTA-DASLab/DeepSeek-V4.1-Flash-1017-GSQ-RCO-GGUF) 制作。它可在
|
||||
NVIDIA RTX 30、40、50 系列显卡和 Radeon AI PRO R9700 上运行。请在 `strata launch` 里选择它。桌面应用暂时不提供。
|
||||
|
||||
所有尺寸、下载大小,以及各种配置能跑什么:[docs/MODELS.md](docs/MODELS.md)。
|
||||
| 版本 | 下载大小 | 质量 | 写代码速度 |
|
||||
| --- | ---: | --- | --- |
|
||||
| **IQ1_M**(1.75 位) | 239 GB | 最小,普通文字表现较弱 | 还没在电脑上测量 |
|
||||
| **IQ2_XXS**(2.00 位) | 257 GB | 平衡得不错 | 每秒 10 个 token(RTX 3060),5(RTX 5070),48(4 块 R9700) |
|
||||
| **IQ2_XS**(2.25 位) | 274 GB | 最接近原模型 | 每秒 15 个 token(一块 R9700),80(4 块 R9700) |
|
||||
| **IQ4_NL**(Engram 表) | 已包含在上面每个下载中 | 三个版本共用的查找表 | |
|
||||
|
||||
用 `strata launch --model ist-das-2.00bpw`(或 `ist-das-1.75bpw`、`ist-das-2.25bpw`)启动。质量数据、各台电脑的速度和手动安装:
|
||||
[docs/DEEPSEEK41.md](docs/DEEPSEEK41.md)。
|
||||
|
||||
## 速度如何?
|
||||
|
||||
以下数据基于 12 GB 显卡(RTX 5070、Ryzen 5 7600、64 GB 内存)。其他显卡的结果见:
|
||||
[各模型的速度](docs/MODELS.md#how-fast-is-each-size)、[社区测试结果](docs/COMMUNITY_BENCHMARKS.md)。
|
||||
以下数据基于 12 GB 显卡(RTX 5070、64 GB 内存)。其他显卡的结果见:
|
||||
[各尺寸的速度](docs/MODELS.md#how-fast-is-each-size)和[社区测试结果](docs/COMMUNITY_BENCHMARKS.md)。
|
||||
|
||||
| 模型 | 回答输出速度 | 读取提示词速度 |
|
||||
| --- | --- | --- |
|
||||
@@ -195,9 +183,7 @@ Gyro 可在 NVIDIA 和 AMD 显卡(AMD 为实验性支持)以及 Linux 上的
|
||||
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1,620 tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2,180 tokens/s |
|
||||
|
||||
一个 token 约等于 ¾ 个英文单词,所以每秒 60 个 token 比你阅读的速度还快。“读取提示词”指 Strata 吸收长文档、代码或聊天历史的速度。
|
||||
|
||||
在 16 GB 的 AMD 显卡上测得的结果(RX 9070 XT、Ryzen 9 3900X、47 GB 内存):
|
||||
在 16 GB 的 AMD 显卡上(RX 9070 XT、47 GB 内存):
|
||||
|
||||
| 模型 | 回答输出速度 | 读取提示词速度 |
|
||||
| --- | --- | --- |
|
||||
@@ -205,78 +191,57 @@ Gyro 可在 NVIDIA 和 AMD 显卡(AMD 为实验性支持)以及 Linux 上的
|
||||
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1,110 tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1,420 tokens/s |
|
||||
|
||||
两张表的进度条使用同一个比例。显存更大的显卡更快:RTX 3090(24 GB)每秒大约能输出 100-140 个 token。每个数字对应的引擎版本见
|
||||
[DETAILS.md](docs/DETAILS.md#speed-measured)。
|
||||
一个 token 约等于 ¾ 个英文单词,所以每秒 60 个 token 比你阅读的速度还快。“读取提示词”指 Strata 吸收长文档、代码或聊天历史的速度。
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 在应用里
|
||||
|
||||
应用包含**聊天**、显示模型以及 GPU、CPU 和内存状态的**监控**、**模型**页面和**设置**。聊天可以在你电脑上的某个文件夹里工作。
|
||||
在**手动**模式下,修改文件或运行命令之前它会先询问你。输入 `/loop 10m <要做的事>`,在聊天保持打开期间每 10 分钟重复发送一次消息。
|
||||
聊天、显示模型以及 GPU、CPU 和内存状态的**监控**、**模型**页面和**设置**。聊天可以在你电脑上的某个文件夹里工作。
|
||||
在**手动**模式下,修改文件或运行命令之前它会先询问你。它可以搜索网页(DuckDuckGo 或 Bing,不需要账号)。
|
||||
发出电脑的只有搜索词。
|
||||
|
||||
聊天可以用 DuckDuckGo(或 Bing)搜索网页,不需要账号。你也可以在**设置,工具**里添加搜索密钥。搜索是唯一会把内容发出电脑的功能,
|
||||
发出的就是搜索词。
|
||||
|
||||
要同时运行两个模型,请打开**运行多个模型**,并为每个模型分配同一品牌的单独一张显卡。
|
||||
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
|
||||
<sub>Strata 应用中的一次对话:智能体用一个 HTML 文件写出了一个小型体素宝塔花园,页面在对话旁的预览中打开</sub></p>
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="Strata 应用:一次对话,智能体做出了一个小型体素宝塔花园,页面在右侧的预览中打开"><br>
|
||||
<sub>智能体用一个 HTML 文件写出了体素宝塔花园,页面在聊天旁的预览中打开</sub></p>
|
||||
|
||||
### 在终端里
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="模型写回复时,Strata 终端监控的总览界面"><br>
|
||||
<sub>模型回复时的 <code>strata monitor</code>:速度、GPU、内存和最近的请求</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # 引导界面:模型、上下文、图片、访问方式,然后开始服务
|
||||
strata launch --model q2_0 --yes --no-tui # 不提问:开始服务,打印地址,然后返回
|
||||
strata launch --plan --model q2_0 # 显示将要执行的操作;不做任何更改
|
||||
strata launch --status # 当前正在提供什么服务
|
||||
strata launch --stop # 停止服务
|
||||
strata monitor --url http://127.0.0.1:8080 # 查看速度、GPU、内存和请求
|
||||
strata update --check # 有新引擎吗?(strata update 会安装它)
|
||||
strata doctor # 检查已安装的引擎文件
|
||||
strata launch # guided: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve and print the address
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor # watch speed, GPU, memory and requests
|
||||
strata update # get the newest engine
|
||||
strata doctor # check the installed files
|
||||
```
|
||||
|
||||
`strata --help` 会列出所有命令。启动和提供服务时不会下载任何东西。只有安装、`strata launch`(在你选择模型时)和
|
||||
`strata update` 会下载。要从手机或另一台电脑访问 Strata,请使用 `strata launch --access lan`。它会生成一个 API 密钥,
|
||||
没有密钥就不会在网络上启动。除非你加上 `--open`,否则不会打开浏览器。指南:[docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md)。
|
||||
完整的客户端说明:[docs/TERMINAL.md](docs/TERMINAL.md)。
|
||||
要从手机或另一台电脑访问 Strata,请使用 `strata launch --access lan`。它会为你生成一个 API 密钥。
|
||||
`strata --help` 会列出所有命令,[docs/TERMINAL.md](docs/TERMINAL.md) 有详细说明。
|
||||
|
||||
### 配合 AI 智能体
|
||||
### 配合 AI 智能体和其他应用
|
||||
|
||||
智能体用方式 B 中的一行命令安装 Strata,然后运行 `strata launch --model NAME --yes --no-tui`。步骤、参数和退出码见
|
||||
[docs/AI_SETUP.md](docs/AI_SETUP.md),仓库规则见 [AGENTS.md](AGENTS.md)。
|
||||
智能体用上面的一行命令安装 Strata,然后运行 `strata launch --model NAME --yes --no-tui`。步骤、参数和退出码见
|
||||
[docs/AI_SETUP.md](docs/AI_SETUP.md)。给智能体看的仓库规则:[AGENTS.md](AGENTS.md)。
|
||||
|
||||
### 连接你的其他应用
|
||||
|
||||
| 应用类型 | 基础 URL |
|
||||
| 你的应用使用 | 基础 URL |
|
||||
| --- | --- |
|
||||
| 兼容 OpenAI(大多数应用和智能体) | `http://127.0.0.1:8080/v1`(API 密钥和模型名称随便填) |
|
||||
| Anthropic API(Claude Code 等) | `http://127.0.0.1:8080`,不带 `/v1`。这些应用会自己加上 `/v1/messages`。Claude Code:`ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI API(大多数应用和智能体) | `http://127.0.0.1:8080/v1`(API 密钥和模型名称随便填) |
|
||||
| Anthropic API(Claude Code) | `http://127.0.0.1:8080`,不带 `/v1`。Claude Code:`ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API(Codex CLI) | `http://127.0.0.1:8080/v1/responses`([设置方法](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` 会打印 OpenCode、Codex CLI、Claude Code、oh-my-pi、pi、Hermes Agent 和 OpenClaw 的具体设置。
|
||||
`strata connect opencode --write` 会在询问后写入配置文件,并保留备份。
|
||||
|
||||
### 图片
|
||||
|
||||
能看图片的模型默认开启图片功能。在聊天里附加或粘贴图片,或者从你的应用发送图片。显卡的显存小于 24 GB 时,读图由处理器完成
|
||||
(每张图片几秒钟,不占用显卡的显存);显存达到 24 GB 时,由显卡完成。NVIDIA 和 AMD 的规则相同。你可以按模型关闭图片功能,
|
||||
也可以用 `--vision off`、`--vision gpu` 或 `--vision cpu` 选择读图方式。
|
||||
|
||||
这条规则背后的数据:在 Radeon AI PRO R9700 上,显卡读一张 260 token 的图片用了 57 ms,处理器用了 3.1 秒。在 16 GB 的显卡
|
||||
(Tesla P100 和 RTX A4000,Linux)上,用显卡读图会让文字速度下降 3-5%,因为它占用约 1.4 GB 显存,否则这部分会被模型使用。
|
||||
在 Mac 上,无论内存多大,都由 GPU 读图(32 GB 的 M4 上读一张 260 token 的图片用 0.45 秒,占用约 1.2 GB 共享内存);处理器读一张约需 50 秒,所以只有指定 `--vision cpu` 时才用。
|
||||
`strata connect` 会打印 OpenCode、Codex CLI、Claude Code、oh-my-pi、pi、Hermes Agent 和 OpenClaw 的具体设置,
|
||||
`strata connect opencode --write` 会帮你写入(并保留备份)。
|
||||
|
||||
### 使用须知
|
||||
|
||||
- 在聊天菜单或你的应用的“reasoning effort”里选择思考级别(**off、low、medium 或 high**)。off 最快。
|
||||
- Strata 默认一次回答一个请求。要同时处理多个,请在 `strata launch` 中使用 `--parallel N`,或在配置里写
|
||||
`"parallel": 2`([BATCHING.md](docs/BATCHING.md))。在 12 GB 显卡上,这会让每个回答变慢。
|
||||
- Strata 会完整读取聊天的第一条消息,每 30,000 个 token 约需 1 分钟。之后的消息几秒钟就开始。
|
||||
- 聊天记录存放的位置和完整的 API:[docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored)、[docs/DETAILS.md](docs/DETAILS.md#using-it)。
|
||||
- **图片**对每个能看图片的模型都是默认开启的。在聊天里附加或粘贴图片,或者从你的应用发送图片。详情:[docs/IMAGES.md](docs/IMAGES.md)。
|
||||
- **思考级别**可以是 off、low、medium 或 high,在聊天菜单或你的应用的“reasoning effort”里设置。off 最快。
|
||||
- **聊天的第一条消息**会被完整读取,每 30,000 个 token 约需 1 分钟。之后的消息几秒钟就开始。
|
||||
- **默认一次处理一个请求。** 要处理更多,请使用 `--parallel N`([BATCHING.md](docs/BATCHING.md))。在 12 GB 显卡上,这会让每个回答变慢。
|
||||
|
||||
## 从源码构建
|
||||
|
||||
@@ -331,12 +296,12 @@ strata doctor # 检查已安装的引擎文件
|
||||
- **你的内存是阅览室的书架。** 24,576 本书都在这里。某个词需要一本不在书桌上的书时,处理器会立刻从书架上取来读,显卡则继续工作。
|
||||
- **你的固态硬盘是档案室。** 里面存着一张 29 GB 的查找表。模型每个词只读其中几行,所以不用常驻内存。
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="The model's 24,576 experts: the busiest on the graphics card, all of them in RAM, a lookup table on the SSD"></p>
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="模型的 24,576 位专家:最忙的在显卡上,全部在内存里,查找表在固态硬盘上"></p>
|
||||
|
||||
另外有一个小助手会猜接下来的几个词。大模型一次检查所有猜测,保留正确的部分。得到的答案不变,但快了 1.6-1.8 倍。
|
||||
长文本按大块读取,一次最多 8,192 个 token,速度超过每秒 1,000 个 token。
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="A small helper guesses the next words; the big model checks them all at once and keeps the right ones"></p>
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="小助手猜接下来的几个词;大模型一次检查全部猜测,保留正确的部分"></p>
|
||||
|
||||
更详细的解释:[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)。每个部分和它的数据:
|
||||
[详细说明](docs/DETAILS.md#how-it-works)和[论文](docs/paper/Strata-Paper.pdf)。
|
||||
@@ -345,24 +310,22 @@ strata doctor # 检查已安装的引擎文件
|
||||
|
||||
- **第一次启动 Strata 时我的电脑卡住了。** 它在把模型加载到内存。请等待,不要关闭窗口。10 分钟后还是卡住?重启电脑,
|
||||
关闭其他程序后再试,或者选一个更小的尺寸。
|
||||
- **下载或安装时中断了。** 重新启动应用或 `strata launch`,或者再运行一次 `START-HERE.bat`(或 `./setup.sh`)。
|
||||
它会从中断的地方继续。
|
||||
- **非常慢、硬盘灯一直闪,或者提示“引擎意外停止”。** 你的电脑可用内存不够。关闭其他程序(浏览器很占内存),
|
||||
- **下载或安装时中断了。** 重新启动应用或 `strata launch`。它会从中断的地方继续。
|
||||
- **非常慢,或者提示“the engine stopped unexpectedly”。** 你的电脑可用内存不够。关闭其他程序(浏览器很占内存),
|
||||
或者选一个更小的尺寸,比如 Q2_0 或 IQ2_XS。
|
||||
- **提示这台电脑没有可用的引擎。** 在应用里打开**设置,引擎**,点击**更新引擎**。在终端里运行 `strata update`。
|
||||
`strata doctor` 会显示已安装哪些引擎文件。
|
||||
- **提示 8080 端口已被占用。** Strata 已经在运行了。找一下它的窗口。
|
||||
|
||||
更多问题和解决办法:[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)。还是解决不了?请提交
|
||||
[issue](https://github.com/Niko1221/Strata/issues)。在应用里,附上**设置,引擎,诊断,查看日志,保存日志**得到的文件。
|
||||
在终端里,粘贴 `strata doctor` 的输出。从源码运行时,附上 Strata 文件夹里的 `strata-<model>.log`。
|
||||
如果是安全问题,请私下报告:[SECURITY.md](SECURITY.md)。
|
||||
更多解决办法:[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)。还是解决不了?请提交
|
||||
[issue](https://github.com/Niko1221/Strata/issues) 并附上日志:在应用里是**设置,引擎,诊断,保存日志**,在终端里是
|
||||
`strata doctor` 的输出。如果是安全问题,请私下报告:[SECURITY.md](SECURITY.md)。
|
||||
|
||||
## 致谢与许可证
|
||||
|
||||
模型是 Qwen 团队的 [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)。压缩工作由
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF)、UkisAI(Swift 1.5)、agentionai(Gyro)
|
||||
和 Unsloth 完成。Strata 使用了 [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) 的部分代码。全部致谢:
|
||||
和 Unsloth 完成。DeepSeek V4.1-Flash 的各个版本也由 ISTA-DASLab 制作。Strata 使用了
|
||||
[llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) 的部分代码。全部致谢:
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits)。Strata 以 [MIT 许可证](LICENSE)开源。少数部分和所有模型有各自的许可证
|
||||
([具体是哪些](docs/HOW_IT_WORKS.md#license))。
|
||||
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ the model. The section above is about the engine's HTTP server. This is what the
|
||||
The Terminal installs with one line:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh -o install.sh && sh install.sh
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "strata-desktop",
|
||||
"productName": "Strata",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.5",
|
||||
"private": true,
|
||||
"description": "Strata Desktop - local AI, tuned to one model.",
|
||||
"scripts": {
|
||||
|
||||
Generated
+1
-1
@@ -3524,7 +3524,7 @@ checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7"
|
||||
|
||||
[[package]]
|
||||
name = "strata-desktop"
|
||||
version = "0.2.2"
|
||||
version = "0.2.5"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"serde",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "strata-desktop"
|
||||
version = "0.2.2"
|
||||
version = "0.2.5"
|
||||
description = "Strata Desktop — local AI, tuned to one model."
|
||||
edition = "2021"
|
||||
rust-version = "1.77"
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "strata",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "strata",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.5",
|
||||
"dependencies": {
|
||||
"html-to-image": "^1.11.13",
|
||||
"katex": "^0.19.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "strata",
|
||||
"private": true,
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.5",
|
||||
"type": "module",
|
||||
"description": "Strata \u2014 local AI, tuned to one model. Web shell + engine host.",
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* The context fixes against a REAL engine (not the scripted worker): Gyro-S at 64K on this PC, the agent bridge and the real Harness.
|
||||
*
|
||||
* gpu + heavy locks held first (python phaseA-tests\lock.py acquire gpu "023-agent: context e2e", then heavy); one engine; nothing of the
|
||||
* installed app is used or touched: its model files are read, its Harness is copied (STRATA_HARNESS_DIR), data goes to a scratch folder.
|
||||
*
|
||||
* node scripts/context-e2e.mjs (needs STRATA_HARNESS_DIR; see the E2E_* names below)
|
||||
*
|
||||
* It starts serve/server.py as ITS OWN child (foreground: this script waits for it and stops it by PID at the end or on a memory alarm),
|
||||
* runs four scenarios and prints one table. Exit code 0 when every expectation held.
|
||||
*
|
||||
* A a message that can never fit (about 60K tokens in a 64K window) is refused in seconds, before the engine reads it
|
||||
* B a chat of dense log messages grows past the window several times over: automatic compaction runs, nothing is refused
|
||||
* C /compact at a high fill: summarises, says the figures; and a second /compact is answered in plain words
|
||||
* D the needle: a fact from the first message is asked about at the end (reported, not asserted: it is the summary's quality)
|
||||
*/
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { createAgentBridge } from '../server/agent/agent-bridge.mjs';
|
||||
import { countTokens } from '../server/agent/context-fit.mjs';
|
||||
|
||||
const REPO = join(import.meta.dirname, '..', '..', '..');
|
||||
const env = (name, fallback) => process.env[name] ?? fallback;
|
||||
const DOCS = env('E2E_DOCS', 'C:\\Users\\AI-Server\\Documents\\Strata');
|
||||
const ENGINE = env('E2E_ENGINE', 'C:\\Users\\AI-Server\\Desktop\\Strata\\GitHub\\wt-023-int\\build-cuda\\strata.exe');
|
||||
const PROFILE = env('E2E_EXPERT_PROFILE', 'C:\\Users\\AI-Server\\AppData\\Local\\Programs\\Strata\\control\\data\\expert-profile.bin');
|
||||
const PYTHON = env('E2E_PYTHON', 'python');
|
||||
const PORT = Number(env('E2E_PORT', '18590'));
|
||||
const MIN_FREE_GB = Number(env('E2E_MIN_FREE_GB', '7'));
|
||||
const GGUF = join(DOCS, 'models', 'gyro-Gyro-S', 'Qwen3.8-Flash-Next-Gyro-S-TQ1_0.gguf');
|
||||
const CUDA_BIN = env('E2E_CUDA_BIN', 'C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA\\v13.0\\bin');
|
||||
const WINDOW = 65536;
|
||||
/** A model server that is already running (the scripted worker, to try this script without a GPU): nothing is started or stopped. */
|
||||
const EXISTING = process.env.E2E_BASE_URL ?? '';
|
||||
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
const freeGb = () => {
|
||||
const out = spawnSync('powershell', ['-NoProfile', '-Command', '[math]::Round((Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory/1MB,1)'], { encoding: 'utf8' }).stdout.trim();
|
||||
return Number(out);
|
||||
};
|
||||
const rows = [];
|
||||
const note = (id, ok, detail) => { rows.push({ id, ok, detail }); console.log(`${ok ? 'PASS' : 'FAIL'} ${id} ${detail}`); };
|
||||
|
||||
if (!process.env.STRATA_HARNESS_DIR) { console.error('STRATA_HARNESS_DIR is not set'); process.exit(2); }
|
||||
if (!EXISTING) for (const f of [ENGINE, GGUF]) if (!existsSync(f)) { console.error(`missing: ${f}`); process.exit(2); }
|
||||
if (!EXISTING && freeGb() < 20) { console.error(`only ${freeGb()} GB of RAM are free; wait for the other streams (lock.py status) and run again`); process.exit(2); }
|
||||
|
||||
const scratch = mkdtempSync(join(tmpdir(), 'strata-ctx-e2e-'));
|
||||
const configFile = join(scratch, 'server.json');
|
||||
const logFile = join(scratch, 'server.log');
|
||||
const readyFile = join(scratch, 'ready.json');
|
||||
writeFileSync(configFile, JSON.stringify({
|
||||
exe: ENGINE,
|
||||
args: ['--pack', join(DOCS, 'packs', 'gyro-gyro-s'), '--native', GGUF, '--ple-gguf', GGUF, ...(existsSync(PROFILE) ? ['--expert-profile', PROFILE] : []),
|
||||
'--prefill', 'auto', '--spec-min-p', '0.5', '--mtp', join(DOCS, 'mtp', 'rt'), '--max-context', String(WINDOW), '--kv', 'int8', '--pool-workers', '5',
|
||||
'--expert-cache', 'auto', '--kv-resident', '32768', '--spec', '4', '--vram-reserve-mib', '1536'],
|
||||
cwd: REPO, tokenizer: join(DOCS, 'packs', 'gyro-gyro-s', 'tokenizer'), model_name: 'qwen3.8-flash-next-gyro-gyro-s', log: join(scratch, 'engine.log'), lib_dirs: [], port: PORT, gpu: 0,
|
||||
}, null, 2));
|
||||
|
||||
let server = null, bridge = null, alarm = null;
|
||||
const stopServer = () => {
|
||||
if (server?.pid) spawnSync('taskkill', ['/T', '/F', '/PID', String(server.pid)], { stdio: 'ignore' });
|
||||
server = null;
|
||||
};
|
||||
const finish = async (code) => {
|
||||
clearInterval(alarm);
|
||||
try { await bridge?.close(); } catch { /* going away */ }
|
||||
stopServer();
|
||||
await sleep(1500);
|
||||
console.log(`\nscratch folder: ${scratch}`);
|
||||
process.exit(code);
|
||||
};
|
||||
process.on('SIGINT', () => finish(130));
|
||||
|
||||
if (!EXISTING) server = spawn(PYTHON, ['serve/server.py', '--engine', 'strata', '--config', configFile, '--port', String(PORT), '--no-api-key', '--ready-file', readyFile], {
|
||||
cwd: REPO, env: { ...process.env, PATH: `${CUDA_BIN};${process.env.PATH}`, STRATA_DATA_DIR: join(scratch, 'data') }, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
|
||||
});
|
||||
const sink = [];
|
||||
if (server) { server.stdout.on('data', d => sink.push(String(d))); server.stderr.on('data', d => sink.push(String(d))); server.on('exit', code => { sink.push(`[server exited ${code}]`); server = null; }); }
|
||||
// Memory alarm: this run must never be what freezes the PC. Under the floor the engine is stopped (by PID) and the run ends.
|
||||
if (!EXISTING) alarm = setInterval(() => { const free = freeGb(); if (free < MIN_FREE_GB) { console.error(`ALARM: ${free} GB of RAM free, stopping the engine`); finish(3); } }, 3000);
|
||||
alarm?.unref?.();
|
||||
|
||||
const base = EXISTING || `http://127.0.0.1:${PORT}`;
|
||||
let ready = false;
|
||||
for (let i = 0; i < 120 && !ready; i++) {
|
||||
if (!EXISTING && !server) break;
|
||||
try { ready = (await fetch(`${base}/health`, { signal: AbortSignal.timeout(2000) })).ok; } catch { /* still loading */ }
|
||||
if (!ready) await sleep(2000);
|
||||
}
|
||||
writeFileSync(logFile, sink.join(''));
|
||||
if (!ready) { console.error('the engine did not become ready:\n' + sink.join('').slice(-1500)); await finish(4); }
|
||||
const model = (await (await fetch(`${base}/v1/models`)).json()).data[0].id;
|
||||
console.log(`engine ready: ${model}, free RAM ${freeGb()} GB`);
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------- the pieces
|
||||
|
||||
const line = i => `2026-10-22 13:${String(Math.floor(i / 60) % 60).padStart(2, '0')}:${String(i % 60).padStart(2, '0')} INFO svc-${i % 17} req=${(i * 2654435761 >>> 0).toString(16)} status=${200 + (i % 7) * 3} ms=${(i * 37) % 900} path=/api/v1/items/${(i * 7919) % 100000}`;
|
||||
const logText = (chars, from = 0, needle = '') => {
|
||||
const out = [];
|
||||
let n = 0, i = from;
|
||||
while (n < chars) { const l = line(i++); out.push(l); n += l.length + 1; }
|
||||
if (needle) out.splice(Math.floor(out.length / 2), 0, needle);
|
||||
return out.join('\n');
|
||||
};
|
||||
const count = text => countTokens({ baseUrl: `${base}/v1`, model, text });
|
||||
|
||||
const turnsOf = async (message) => {
|
||||
const frames = [];
|
||||
const t0 = Date.now();
|
||||
await bridge.runTurn({ chatId: 'e2e', message, workspacePath: scratch, mode: 'bypass', thinking: 'Off', onFrame: f => frames.push(f) });
|
||||
return { frames, seconds: (Date.now() - t0) / 1000, last: frames.at(-1) };
|
||||
};
|
||||
const textOf = frames => frames.filter(f => f.type === 'delta' && !f.noteReason).map(f => f.text).join('').trim();
|
||||
const noteOf = frames => frames.find(f => f.type === 'delta' && f.noteReason === 'compacted')?.text?.trim() ?? null;
|
||||
|
||||
bridge = createAgentBridge({
|
||||
dataDir: join(scratch, 'agent'), env: process.env, startTimeoutMs: 120_000, log: l => console.log(' bridge:', l),
|
||||
localInstances: () => [{ id: 'e2e', label: 'Gyro-S', baseUrl: `${base}/v1`, modelId: model, context: WINDOW, slots: 1, key: '' }], connections: () => [],
|
||||
});
|
||||
mkdirSync(join(scratch, 'agent'), { recursive: true });
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------- A
|
||||
{
|
||||
// Grow a log until the engine itself counts about 60K tokens.
|
||||
let chars = 150_000, tokens = await count(logText(chars));
|
||||
while (tokens && tokens < 58_000 && chars < 400_000) { chars = Math.round(chars * (60_000 / tokens)); tokens = await count(logText(chars)); }
|
||||
const t = await turnsOf(logText(chars) + '\n\nWhat is the most common status?');
|
||||
const refused = t.last.type === 'error' && t.last.code === 'CONTEXT_MESSAGE_TOO_LARGE';
|
||||
note('A too large is refused before it is read', refused && t.seconds < 15, `${chars} characters = ${tokens} tokens by the engine; answered in ${t.seconds.toFixed(1)} s; frame ${t.last.type}/${t.last.code ?? '-'}: ${String(t.last.message ?? '').slice(0, 110)}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------- B
|
||||
const needle = 'NOTE FROM OPS: the vault code is COBALT-27. Do not lose it.';
|
||||
const timeline = [];
|
||||
let notes = 0, refusedAny = false, peak = 0;
|
||||
{
|
||||
for (let k = 0; k < 5; k++) {
|
||||
const body = logText(34_000, k * 1000, k === 0 ? needle : '');
|
||||
const t = await turnsOf(`${body}\n\nReply with the single word OK.`);
|
||||
const note = noteOf(t.frames);
|
||||
if (note) notes++;
|
||||
if (t.last.type === 'error') refusedAny = true;
|
||||
const used = t.last.contextTokens ?? 0;
|
||||
peak = Math.max(peak, ...t.frames.filter(f => f.type === 'context').map(f => f.contextTokens), used);
|
||||
timeline.push(`msg${k + 1}: ${t.last.type}${t.last.code ? '/' + t.last.code : ''} ${t.seconds.toFixed(0)} s context ${used} (${Math.round(100 * used / WINDOW)}%)${note ? ' [' + note.slice(0, 80) + ']' : ''}`);
|
||||
console.log(' ' + timeline.at(-1));
|
||||
if (t.last.type === 'error') break;
|
||||
}
|
||||
note('B five dense messages (about 14K tokens each) in a 64K window: no error', !refusedAny, `peak context ${peak} of ${WINDOW}; ${notes} compaction note(s) in the chat`);
|
||||
note('B compaction ran and was announced', notes >= 1, timeline.join(' | '));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------- C
|
||||
{
|
||||
const run = async (line) => { const frames = []; await bridge.compactCommand({ chatId: 'e2e', message: line, workspacePath: scratch, mode: 'bypass', choice: null, onFrame: f => frames.push(f) }); return frames; };
|
||||
await turnsOf(`${logText(30_000, 90_000)}\n\nReply with the single word OK.`);
|
||||
const t0 = Date.now();
|
||||
const first = await run('/compact');
|
||||
const said = first.find(f => f.type === 'delta');
|
||||
const ok = said?.key === 'Older messages were summarized to make room. The chat was {0}% full and is now {1}% full.' && said.values[0] > said.values[1];
|
||||
note('C /compact summarises and states the figures', ok, `${((Date.now() - t0) / 1000).toFixed(0)} s: ${said?.text}`);
|
||||
const second = await run('/compact');
|
||||
const again = second.find(f => f.type === 'delta')?.text ?? '';
|
||||
note('C a second /compact is answered in plain words', /summarized|nothing|could not/i.test(again) && !/\{|400|invalid_request/.test(again), again);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------- D
|
||||
{
|
||||
const t = await turnsOf('What was the vault code in the very first message? Answer with the code only.');
|
||||
const answer = textOf(t.frames);
|
||||
console.log(`D needle after compaction: "${answer.slice(0, 80)}" (${t.last.type})`);
|
||||
rows.push({ id: 'D the needle', ok: /COBALT-27/.test(answer), detail: `reported only: answer "${answer.slice(0, 80)}"` });
|
||||
}
|
||||
|
||||
console.log('\n' + rows.map(r => `${r.ok ? 'PASS' : r.id.startsWith('D') ? 'INFO' : 'FAIL'} ${r.id}${r.id.startsWith('D') ? ` ${r.detail}` : ''}`).join('\n'));
|
||||
const failed = rows.filter(r => !r.ok && !r.id.startsWith('D')).length;
|
||||
writeFileSync(join(scratch, 'result.json'), JSON.stringify(rows, null, 2));
|
||||
await finish(failed ? 1 : 0);
|
||||
@@ -0,0 +1,385 @@
|
||||
"""Writes src/locales/fix-<lang>.json: the interface strings that had no key in the locale files (#3479, #3825) and the strings of the 0.2.4.3 bug fixes.
|
||||
Run from apps/host: python scripts/make-fix-locales.py
|
||||
The keys are fx01.. in this order; fix-en.json is the English source. Placeholders ({0}, {1}) must be kept in every language."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
EN = [
|
||||
"will stop running and be removed.",
|
||||
"Downloaded, not loaded. Open Models to load it.",
|
||||
"{0} lines added, {1} removed",
|
||||
"Edited {0} files",
|
||||
"Drop files to add them",
|
||||
"Pictures and text files",
|
||||
"Keep goal",
|
||||
"Remove goal",
|
||||
"The assistant stops working towards it. Your chat and files stay as they are.",
|
||||
"This model can read pictures, but the engine installed here does not include the picture reader yet, so Read pictures is off. Update the engine in Settings → About to turn it on.",
|
||||
"Showing the values of your tuning '{0}'.",
|
||||
"Your tuning '{0}' sets",
|
||||
"was {0}",
|
||||
"· {0} RAM",
|
||||
"The message was not cancelled",
|
||||
"Extra settings for the program",
|
||||
"Draft it with AI",
|
||||
"Thinking time",
|
||||
"Writing time",
|
||||
"tokens/s",
|
||||
"% of context",
|
||||
"This page asks for files from the internet, which the preview does not load, so it may look incomplete. View code shows exactly what was written.",
|
||||
"Keep A",
|
||||
"Keep B",
|
||||
"Took",
|
||||
"Variation",
|
||||
"Changed",
|
||||
"Nothing",
|
||||
"Both versions are ready. Compare A and B, then keep one.",
|
||||
"The assistant is still answering",
|
||||
"Send your message when it has finished.",
|
||||
"The message was not queued",
|
||||
"Scroll to the latest message",
|
||||
"Remove task",
|
||||
"Start a new conversation; keep this one",
|
||||
"Have the assistant write a plan first; nothing runs until you approve it",
|
||||
"Search the web, read the pages and answer with sources",
|
||||
"Copied",
|
||||
"Response",
|
||||
"Message",
|
||||
"PDF added",
|
||||
"Select the text and copy it with Ctrl+C.",
|
||||
"{0} is in this chat. Ask about it in your next message.",
|
||||
]
|
||||
|
||||
T = {
|
||||
"zh": [
|
||||
"将停止运行并被移除。",
|
||||
"已下载,尚未加载。打开“模型”页面来加载它。",
|
||||
"新增 {0} 行,删除 {1} 行",
|
||||
"已编辑 {0} 个文件",
|
||||
"拖放文件即可添加",
|
||||
"图片和文本文件",
|
||||
"保留目标",
|
||||
"移除目标",
|
||||
"助手将不再朝这个目标工作。你的对话和文件保持不变。",
|
||||
"此模型可以读取图片,但这里安装的引擎还不包含图片读取器,所以“读取图片”已关闭。请在“设置 → 关于”中更新引擎后再开启。",
|
||||
"显示你的调优“{0}”的取值。",
|
||||
"你的调优“{0}”设置了",
|
||||
"原为 {0}",
|
||||
"· {0} 内存",
|
||||
"消息没有被取消",
|
||||
"程序的额外设置",
|
||||
"用 AI 起草",
|
||||
"思考时间",
|
||||
"写作时间",
|
||||
"词元/秒",
|
||||
"% 的上下文",
|
||||
"此页面需要从互联网加载文件,预览不会加载这些文件,所以看起来可能不完整。“查看代码”会显示实际写入的内容。",
|
||||
"保留 A",
|
||||
"保留 B",
|
||||
"用时",
|
||||
"波动",
|
||||
"已更改",
|
||||
"无",
|
||||
"两个版本都已就绪。比较 A 和 B,然后保留其中一个。",
|
||||
"助手仍在回答",
|
||||
"等它回答完再发送你的消息。",
|
||||
"消息没有进入队列",
|
||||
"滚动到最新消息",
|
||||
"移除任务",
|
||||
"开始新对话;保留当前这个",
|
||||
"让助手先写一份计划;在你批准之前不会运行任何内容",
|
||||
"搜索网络、阅读网页,并附上来源作答",
|
||||
"已复制",
|
||||
"回复",
|
||||
"消息",
|
||||
"已添加 PDF",
|
||||
"请选中文字,然后按 Ctrl+C 复制。",
|
||||
"{0} 已在此对话中。请在下一条消息里询问它。",
|
||||
],
|
||||
"ja": [
|
||||
"は停止され、削除されます。",
|
||||
"ダウンロード済みですが、読み込まれていません。「モデル」を開いて読み込んでください。",
|
||||
"{0} 行追加、{1} 行削除",
|
||||
"{0} 個のファイルを編集しました",
|
||||
"ファイルをドロップして追加",
|
||||
"画像とテキストファイル",
|
||||
"目標を残す",
|
||||
"目標を削除",
|
||||
"アシスタントはこの目標に向けた作業をやめます。チャットとファイルはそのまま残ります。",
|
||||
"このモデルは画像を読めますが、ここにインストールされているエンジンにはまだ画像リーダーが含まれていないため、「画像を読む」はオフです。設定 → 情報 でエンジンを更新するとオンにできます。",
|
||||
"チューニング「{0}」の値を表示しています。",
|
||||
"チューニング「{0}」が設定する項目",
|
||||
"変更前: {0}",
|
||||
"· {0} RAM",
|
||||
"メッセージはキャンセルされませんでした",
|
||||
"プログラムの追加設定",
|
||||
"AI で下書きを作成",
|
||||
"思考時間",
|
||||
"執筆時間",
|
||||
"トークン/秒",
|
||||
"% のコンテキスト",
|
||||
"このページはインターネット上のファイルを必要としますが、プレビューでは読み込まれないため、表示が不完全になることがあります。「コードを表示」で書かれた内容をそのまま確認できます。",
|
||||
"A を残す",
|
||||
"B を残す",
|
||||
"所要時間",
|
||||
"ばらつき",
|
||||
"変更点",
|
||||
"なし",
|
||||
"両方のバージョンの準備ができました。A と B を比べて、どちらかを残してください。",
|
||||
"アシスタントはまだ回答中です",
|
||||
"回答が終わってからメッセージを送ってください。",
|
||||
"メッセージは待機列に入りませんでした",
|
||||
"最新のメッセージへスクロール",
|
||||
"タスクを削除",
|
||||
"新しい会話を始めます。今の会話は残ります",
|
||||
"アシスタントにまず計画を書かせます。承認するまで何も実行されません",
|
||||
"ウェブを検索し、ページを読み、出典付きで回答します",
|
||||
"コピーしました",
|
||||
"返信",
|
||||
"メッセージ",
|
||||
"PDF を追加しました",
|
||||
"テキストを選択して Ctrl+C でコピーしてください。",
|
||||
"{0} をこのチャットに追加しました。次のメッセージで質問してください。",
|
||||
],
|
||||
"de": [
|
||||
"wird nicht mehr ausgeführt und entfernt.",
|
||||
"Heruntergeladen, aber nicht geladen. Öffne „Modelle“, um es zu laden.",
|
||||
"{0} Zeilen hinzugefügt, {1} entfernt",
|
||||
"{0} Dateien bearbeitet",
|
||||
"Dateien hier ablegen, um sie hinzuzufügen",
|
||||
"Bilder und Textdateien",
|
||||
"Ziel behalten",
|
||||
"Ziel entfernen",
|
||||
"Der Assistent arbeitet nicht mehr darauf hin. Dein Chat und deine Dateien bleiben unverändert.",
|
||||
"Dieses Modell kann Bilder lesen, aber die hier installierte Engine enthält den Bildleser noch nicht, deshalb ist „Bilder lesen“ aus. Aktualisiere die Engine unter Einstellungen → Info, um es einzuschalten.",
|
||||
"Zeigt die Werte deiner Abstimmung „{0}“.",
|
||||
"Deine Abstimmung „{0}“ setzt",
|
||||
"war {0}",
|
||||
"· {0} RAM",
|
||||
"Die Nachricht wurde nicht abgebrochen",
|
||||
"Zusätzliche Einstellungen für das Programm",
|
||||
"Mit KI entwerfen",
|
||||
"Denkzeit",
|
||||
"Schreibzeit",
|
||||
"Token/s",
|
||||
"% des Kontexts",
|
||||
"Diese Seite lädt Dateien aus dem Internet, was die Vorschau nicht tut. Sie kann deshalb unvollständig aussehen. „Code anzeigen“ zeigt genau, was geschrieben wurde.",
|
||||
"A behalten",
|
||||
"B behalten",
|
||||
"Dauer",
|
||||
"Schwankung",
|
||||
"Geändert",
|
||||
"Nichts",
|
||||
"Beide Versionen sind fertig. Vergleiche A und B und behalte eine.",
|
||||
"Der Assistent antwortet noch",
|
||||
"Sende deine Nachricht, wenn er fertig ist.",
|
||||
"Die Nachricht wurde nicht eingereiht",
|
||||
"Zur neuesten Nachricht scrollen",
|
||||
"Aufgabe entfernen",
|
||||
"Neue Unterhaltung beginnen; diese bleibt erhalten",
|
||||
"Der Assistent schreibt zuerst einen Plan; nichts läuft, bis du ihn freigibst",
|
||||
"Im Web suchen, die Seiten lesen und mit Quellen antworten",
|
||||
"Kopiert",
|
||||
"Antwort",
|
||||
"Nachricht",
|
||||
"PDF hinzugefügt",
|
||||
"Markiere den Text und kopiere ihn mit Strg+C.",
|
||||
"{0} ist in diesem Chat. Frag in deiner nächsten Nachricht danach.",
|
||||
],
|
||||
"es": [
|
||||
"dejará de ejecutarse y se eliminará.",
|
||||
"Descargado, pero no cargado. Abre Modelos para cargarlo.",
|
||||
"{0} líneas añadidas, {1} eliminadas",
|
||||
"{0} archivos editados",
|
||||
"Suelta los archivos para añadirlos",
|
||||
"Imágenes y archivos de texto",
|
||||
"Mantener el objetivo",
|
||||
"Quitar el objetivo",
|
||||
"El asistente deja de trabajar en ello. Tu chat y tus archivos se quedan como están.",
|
||||
"Este modelo puede leer imágenes, pero el motor instalado aquí aún no incluye el lector de imágenes, así que Leer imágenes está desactivado. Actualiza el motor en Ajustes → Acerca de para activarlo.",
|
||||
"Se muestran los valores de tu ajuste «{0}».",
|
||||
"Tu ajuste «{0}» establece",
|
||||
"antes {0}",
|
||||
"· {0} de RAM",
|
||||
"El mensaje no se canceló",
|
||||
"Ajustes adicionales del programa",
|
||||
"Redactar con IA",
|
||||
"Tiempo de pensar",
|
||||
"Tiempo de escribir",
|
||||
"tokens/s",
|
||||
"% del contexto",
|
||||
"Esta página pide archivos de internet, que la vista previa no carga, así que puede verse incompleta. Ver código muestra exactamente lo que se escribió.",
|
||||
"Mantener A",
|
||||
"Mantener B",
|
||||
"Tardó",
|
||||
"Variación",
|
||||
"Cambiado",
|
||||
"Nada",
|
||||
"Las dos versiones están listas. Compara A y B y quédate con una.",
|
||||
"El asistente sigue respondiendo",
|
||||
"Envía tu mensaje cuando termine.",
|
||||
"El mensaje no se puso en cola",
|
||||
"Ir al último mensaje",
|
||||
"Quitar tarea",
|
||||
"Empieza una conversación nueva; esta se conserva",
|
||||
"El asistente escribe primero un plan; no se ejecuta nada hasta que lo apruebes",
|
||||
"Busca en la web, lee las páginas y responde con fuentes",
|
||||
"Copiado",
|
||||
"Respuesta",
|
||||
"Mensaje",
|
||||
"PDF añadido",
|
||||
"Selecciona el texto y cópialo con Ctrl+C.",
|
||||
"{0} está en este chat. Pregunta por él en tu próximo mensaje.",
|
||||
],
|
||||
"ru": [
|
||||
"перестанет работать и будет удалена.",
|
||||
"Скачана, но не загружена. Откройте «Модели», чтобы загрузить её.",
|
||||
"Добавлено строк: {0}, удалено: {1}",
|
||||
"Изменено файлов: {0}",
|
||||
"Перетащите файлы сюда, чтобы добавить",
|
||||
"Картинки и текстовые файлы",
|
||||
"Оставить цель",
|
||||
"Удалить цель",
|
||||
"Ассистент перестанет работать над ней. Ваш чат и файлы останутся как есть.",
|
||||
"Эта модель умеет читать картинки, но установленный здесь движок пока не содержит модуль чтения картинок, поэтому «Читать картинки» выключено. Обновите движок в разделе «Настройки → О программе», чтобы включить.",
|
||||
"Показаны значения вашей настройки «{0}».",
|
||||
"Ваша настройка «{0}» задаёт",
|
||||
"было {0}",
|
||||
"· {0} ОЗУ",
|
||||
"Сообщение не было отменено",
|
||||
"Дополнительные настройки программы",
|
||||
"Набросать с помощью ИИ",
|
||||
"Время размышления",
|
||||
"Время написания",
|
||||
"токенов/с",
|
||||
"% контекста",
|
||||
"Эта страница запрашивает файлы из интернета, а предпросмотр их не загружает, поэтому она может выглядеть неполной. «Показать код» показывает, что именно было написано.",
|
||||
"Оставить A",
|
||||
"Оставить B",
|
||||
"Заняло",
|
||||
"Разброс",
|
||||
"Изменено",
|
||||
"Ничего",
|
||||
"Обе версии готовы. Сравните A и B и оставьте одну.",
|
||||
"Ассистент ещё отвечает",
|
||||
"Отправьте сообщение, когда он закончит.",
|
||||
"Сообщение не поставлено в очередь",
|
||||
"К последнему сообщению",
|
||||
"Удалить задачу",
|
||||
"Начать новый разговор; этот сохраняется",
|
||||
"Ассистент сначала напишет план; ничего не запустится, пока вы его не одобрите",
|
||||
"Искать в интернете, читать страницы и отвечать со ссылками на источники",
|
||||
"Скопировано",
|
||||
"Ответ",
|
||||
"Сообщение",
|
||||
"PDF добавлен",
|
||||
"Выделите текст и скопируйте его клавишами Ctrl+C.",
|
||||
"{0} добавлен в этот чат. Спросите о нём в следующем сообщении.",
|
||||
],
|
||||
"pt": [
|
||||
"vai parar de funcionar e será removido.",
|
||||
"Baixado, mas não carregado. Abra Modelos para carregá-lo.",
|
||||
"{0} linhas adicionadas, {1} removidas",
|
||||
"{0} arquivos editados",
|
||||
"Solte os arquivos para adicioná-los",
|
||||
"Imagens e arquivos de texto",
|
||||
"Manter a meta",
|
||||
"Remover a meta",
|
||||
"O assistente deixa de trabalhar nela. Seu chat e seus arquivos continuam como estão.",
|
||||
"Este modelo consegue ler imagens, mas o motor instalado aqui ainda não inclui o leitor de imagens, então Ler imagens está desligado. Atualize o motor em Configurações → Sobre para ligar.",
|
||||
"Mostrando os valores do seu ajuste \"{0}\".",
|
||||
"Seu ajuste \"{0}\" define",
|
||||
"era {0}",
|
||||
"· {0} de RAM",
|
||||
"A mensagem não foi cancelada",
|
||||
"Configurações extras do programa",
|
||||
"Rascunhar com IA",
|
||||
"Tempo de raciocínio",
|
||||
"Tempo de escrita",
|
||||
"tokens/s",
|
||||
"% do contexto",
|
||||
"Esta página pede arquivos da internet, que a visualização não carrega, então pode parecer incompleta. Ver código mostra exatamente o que foi escrito.",
|
||||
"Manter A",
|
||||
"Manter B",
|
||||
"Levou",
|
||||
"Variação",
|
||||
"Alterado",
|
||||
"Nada",
|
||||
"As duas versões estão prontas. Compare A e B e mantenha uma.",
|
||||
"O assistente ainda está respondendo",
|
||||
"Envie sua mensagem quando ele terminar.",
|
||||
"A mensagem não entrou na fila",
|
||||
"Ir para a última mensagem",
|
||||
"Remover tarefa",
|
||||
"Começar uma nova conversa; esta continua salva",
|
||||
"O assistente escreve um plano primeiro; nada roda até você aprovar",
|
||||
"Pesquisar na web, ler as páginas e responder com fontes",
|
||||
"Copiado",
|
||||
"Resposta",
|
||||
"Mensagem",
|
||||
"PDF adicionado",
|
||||
"Selecione o texto e copie com Ctrl+C.",
|
||||
"{0} está neste chat. Pergunte sobre ele na próxima mensagem.",
|
||||
],
|
||||
"ko": [
|
||||
"실행이 중지되고 제거됩니다.",
|
||||
"다운로드했지만 아직 불러오지 않았습니다. 모델 화면을 열어 불러오세요.",
|
||||
"{0}줄 추가, {1}줄 삭제",
|
||||
"파일 {0}개 편집함",
|
||||
"파일을 놓아서 추가하세요",
|
||||
"그림과 텍스트 파일",
|
||||
"목표 유지",
|
||||
"목표 제거",
|
||||
"어시스턴트가 이 목표를 향한 작업을 멈춥니다. 대화와 파일은 그대로 남습니다.",
|
||||
"이 모델은 그림을 읽을 수 있지만, 여기에 설치된 엔진에는 아직 그림 읽기 기능이 없어서 '그림 읽기'가 꺼져 있습니다. 설정 → 정보에서 엔진을 업데이트하면 켤 수 있습니다.",
|
||||
"튜닝 '{0}'의 값을 보여 줍니다.",
|
||||
"튜닝 '{0}'이(가) 설정하는 항목",
|
||||
"이전 값 {0}",
|
||||
"· RAM {0}",
|
||||
"메시지가 취소되지 않았습니다",
|
||||
"프로그램 추가 설정",
|
||||
"AI로 초안 작성",
|
||||
"생각한 시간",
|
||||
"작성한 시간",
|
||||
"토큰/초",
|
||||
"컨텍스트 %",
|
||||
"이 페이지는 인터넷의 파일을 불러와야 하는데 미리보기는 이를 불러오지 않으므로 불완전하게 보일 수 있습니다. '코드 보기'에서 작성된 내용을 그대로 볼 수 있습니다.",
|
||||
"A 유지",
|
||||
"B 유지",
|
||||
"걸린 시간",
|
||||
"편차",
|
||||
"변경됨",
|
||||
"없음",
|
||||
"두 버전이 모두 준비되었습니다. A와 B를 비교한 뒤 하나를 유지하세요.",
|
||||
"어시스턴트가 아직 답하는 중입니다",
|
||||
"답이 끝나면 메시지를 보내세요.",
|
||||
"메시지가 대기열에 들어가지 않았습니다",
|
||||
"최신 메시지로 이동",
|
||||
"작업 제거",
|
||||
"새 대화를 시작합니다. 이 대화는 그대로 둡니다",
|
||||
"어시스턴트가 먼저 계획을 작성합니다. 승인하기 전에는 아무것도 실행되지 않습니다",
|
||||
"웹을 검색하고 페이지를 읽어 출처와 함께 답합니다",
|
||||
"복사했습니다",
|
||||
"답변",
|
||||
"메시지",
|
||||
"PDF를 추가했습니다",
|
||||
"텍스트를 선택한 뒤 Ctrl+C로 복사하세요.",
|
||||
"{0}이(가) 이 채팅에 있습니다. 다음 메시지에서 물어보세요.",
|
||||
],
|
||||
}
|
||||
|
||||
here = Path(__file__).resolve().parent.parent / "locales" if False else Path(__file__).resolve().parent.parent / "src" / "locales"
|
||||
|
||||
|
||||
def write(lang, values):
|
||||
assert len(values) == len(EN), (lang, len(values), len(EN))
|
||||
for source, text in zip(EN, values):
|
||||
assert sorted(__import__("re").findall(r"\{\d+\}", source)) == sorted(__import__("re").findall(r"\{\d+\}", text)), (lang, source, text)
|
||||
data = {f"fx{i + 1:02d}": text for i, text in enumerate(values)}
|
||||
(here / f"fix-{lang}.json").write_text(json.dumps(data, ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
write("en", EN)
|
||||
for lang, values in T.items():
|
||||
write(lang, values)
|
||||
print("written", len(EN), "strings in", len(T) + 1, "languages")
|
||||
@@ -5,6 +5,12 @@ import {spawn} from 'node:child_process';
|
||||
import {resolve} from 'node:path';
|
||||
const root=resolve(import.meta.dirname,'..');
|
||||
const files=readdirSync(resolve(root,'tests')).filter(name=>name.endsWith('.test.mjs')).sort().map(name=>resolve(root,'tests',name));
|
||||
const child=spawn(process.execPath,['--test',...files],{cwd:root,stdio:'inherit'});
|
||||
/* Every test file runs against scratch copies of the per-user places the host writes to (the owner's real profile is never touched). */
|
||||
import {mkdtempSync} from 'node:fs';
|
||||
import {tmpdir} from 'node:os';
|
||||
import {join} from 'node:path';
|
||||
const scratch=mkdtempSync(join(tmpdir(),'strata-host-suite-profile-'));
|
||||
const env={...process.env,STRATA_SETTINGS_FILE:join(scratch,'app-settings.json'),STRATA_COMPONENTS_DIR:join(scratch,'components'),LOCALAPPDATA:join(scratch,'Local'),APPDATA:join(scratch,'Roaming'),XDG_DATA_HOME:join(scratch,'xdg-data'),XDG_STATE_HOME:join(scratch,'xdg-state'),XDG_CONFIG_HOME:join(scratch,'xdg-config')};
|
||||
const child=spawn(process.execPath,['--test',...files],{cwd:root,stdio:'inherit',env});
|
||||
child.on('error',error=>{console.error(error.message);process.exitCode=1;});
|
||||
child.on('exit',(code,signal)=>{process.exitCode=code??(signal?1:0);});
|
||||
@@ -14,6 +14,7 @@ and talks to its Gateway. Nothing here downloads or builds anything.
|
||||
| `children.mjs` | pure projection of one helper agent's events into its row and its approval card |
|
||||
| `preset.mjs` | the agent preset (which tools the chat has) |
|
||||
| `plugins/strata-gate.mjs` | the Manual-mode approval gate (a Harness plugin, loaded by path) |
|
||||
| `plugins/strata-context.mjs`, `context-budget.mjs`, `context-fit.mjs` | measured context pressure, summaries that fit the window, the check before a message goes out |
|
||||
| `agent-bridge.mjs` | the chat-to-session map, turns, approvals, Stop, status |
|
||||
|
||||
## Frames sent on `/api/chat/stream` for an agent turn
|
||||
@@ -132,3 +133,14 @@ A turn that keeps failing the same way is stopped by the app, never left to the
|
||||
- **Compaction:** after a guard stop (and after a turn the model server refused because the context was full) the bridge asks the Harness for `/compact` on that session, so the next message has room. Best effort and logged.
|
||||
- **Never silent:** a turn that ends (`completed` or `max-tokens`) with no answer text gets "I ran out of room in this chat..." when the last request used 85% of the window or more, otherwise "I stopped without writing an answer..." (not when tool cards were shown, not after Stop, not after an error, which says why by itself). Goal runs have their own ending lines and are not guarded.
|
||||
- Tests: `tests/agent-turn-guard.test.mjs` (counting, always; end to end with the real Harness, the scripted model's `searchloop` and `bloat` scripts and `searchMock: 'limited'` on the bridge, which runs the real search code on a pretend web that refuses, no network).
|
||||
|
||||
## Context: measured pressure, summaries that fit, the check before a message goes out (2026-10-22)
|
||||
|
||||
Found by the owner's hands-on test of the installed 0.2.1 (Gyro-S, 64K): a 118 KB log paste was 66,500 tokens to the engine and about 29,500 to the Harness (4 characters a token; logs and code are about 2); the engine refused it with a 400, the refusal stayed in the history, and then neither the next message nor `/compact` could go through, because the summary request carried the same history and was refused for its size too. `/compact` itself was never a command of the app: it went to the model as a question and the model answered "nothing to compact".
|
||||
|
||||
- **`plugins/strata-context.mjs`** (inserted by the profile, like the gate) wraps `tokenMeter.measure` and `llm.stream` in place, once each. Pressure = the last prompt size the model server reported for an answer, plus what changed since priced at the density that measurement showed (`context-budget.mjs`: `pressureNow`, `densityOf`); never lower than the Harness's own price. Node prices are scaled the same way, so the retained tail is in real tokens. A summary request (`purpose: 'compaction'`) that would not fit the window is asked for in pieces: a message bigger than a piece is cut for the request only, each piece builds on the summary before it, a piece the server refuses is halved and tried again. `/strata-context` (a Harness command, like `/strata-gate`) answers `{tokens, window}` for a session so the host can tell the person true figures.
|
||||
- **`context-fit.mjs`**: before a message is sent the model server counts it (`POST /v1/messages/count_tokens`, its own tokenizer). `decideFit`: send; summarise first and then send (the message fits an otherwise empty chat); or never fits. The last one is not sent: the error frame has `code: 'CONTEXT_MESSAGE_TOO_LARGE'` and the page offers "Attach as a file" (`oversize.mjs`, `/api/chat/oversize`: the text is saved in the chat's folder, the assistant reads it in parts) or "Send a shortened preview". An engine that cannot count lets the message through; the Harness's own compact-and-retry is the net.
|
||||
- **`/compact`** is handled by the host (`compactCommand`): the Harness's `/compact`, then a plain line with the measured figures ("The chat was 90% full and is now 38% full"), or "nothing older to summarize" when the newest message is what fills the chat.
|
||||
- **Visible:** `compaction/start` shows "Summarizing earlier messages..." in the status line; `compaction/end` adds one plain line to the reply. The projector also sends a `context` frame after every model request, so the meter follows the running turn.
|
||||
- **A refusal that is left** (`CONTEXT_WINDOW_EXCEEDED`) is a plain sentence with the engine's text under Details and the buttons "Summarize and continue" and "Start a new chat". "Try sending the message again" is for other errors only.
|
||||
- Tests: `tests/agent-context.test.mjs` (arithmetic; the real Harness against the scripted worker with `window`, `charsPerToken` and `noCount`), `tests/oversize.test.mjs`, `tests/context-ui.test.mjs`. The run against a real engine is `scripts/context-e2e.mjs`.
|
||||
@@ -22,7 +22,7 @@ import { randomUUID } from 'node:crypto';
|
||||
import { GatewayClient } from './gateway-client.mjs';
|
||||
import { HarnessProcess, resolveHarness, redact, stopStaleAgent } from './harness-runtime.mjs';
|
||||
import { StatusReporter } from './status.mjs';
|
||||
import { buildProfile, routeFor, MODE_PRESETS, modeOf, effortOf } from './profile.mjs';
|
||||
import { buildProfile, routeFor, MODE_PRESETS, modeOf, effortOf, localOutputBudget, compactionHeadroom } from './profile.mjs';
|
||||
import { TurnProjector, parseArguments as parseArgs } from './events.mjs';
|
||||
import { ChildProjector } from './children.mjs';
|
||||
import { nearestBudget } from './llm-shape.mjs';
|
||||
@@ -30,6 +30,7 @@ import { DeliverableTracker, ActivityGroups } from './deliverables.mjs';
|
||||
import { SkillsUsed, skillInfoFrom } from './skills-used.mjs';
|
||||
import { turnEngineSpeed } from './engine-speed.mjs';
|
||||
import { TurnGuard, guardNote, GUARD_NOTES } from './turn-guard.mjs';
|
||||
import { BASE_TOKENS, COUNT_ABOVE_CHARS, FIT_TEXT, countTokens, decideFit, fill, grouped } from './context-fit.mjs';
|
||||
import { PlanStore, planPrompt, parsePlan, parsePoints, stepObjective, stepLine, firstSentence, stepSummary, planAsPrompt, editedSteps, planView, researchPrompt, researchWritePrompt, RESEARCH_NUDGE, parseSources } from './plan.mjs';
|
||||
|
||||
/**
|
||||
@@ -111,11 +112,12 @@ const deferred = () => { let resolve; const promise = new Promise(r => { resolve
|
||||
* @param {string|null} [options.componentsDir] where installed components live
|
||||
* @param {() => Array} options.localInstances ready local instances (see profile.mjs)
|
||||
* @param {() => Array} options.connections saved API connections WITH their keys (host-internal)
|
||||
* @param {() => (string|null)} [options.defaultLocalId] the local instance the chat runs on: its tool servers are the agent's (T5)
|
||||
* @param {NodeJS.ProcessEnv} [options.env]
|
||||
* @param {string} [options.nodePath] the Node runtime that runs the Harness
|
||||
* @param {(line:string)=>void} [options.log]
|
||||
*/
|
||||
export function createAgentBridge({ dataDir, componentsDir = null, bundledDir = null, gate = true, localInstances, connections, tools = () => undefined, skillsDir = () => null, onGoal = () => {}, onPlan = () => {}, onQueue = () => {}, proxyBase = null, searchMock = null, expertSampling = () => null, stallMs = 4 * 60_000, stallToolMs = 20 * 60_000, env = process.env, nodePath = process.execPath, log = () => {}, startTimeoutMs = 120_000, sessionReadyMs = 60_000, sessionRetryMs = 1_500 }) {
|
||||
export function createAgentBridge({ dataDir, componentsDir = null, bundledDir = null, gate = true, localInstances, connections, tools = () => undefined, defaultLocalId = () => null, skillsDir = () => null, onGoal = () => {}, onPlan = () => {}, onQueue = () => {}, proxyBase = null, searchMock = null, expertSampling = () => null, engineProgress = () => null, engineGraceMs = 30 * 60_000, stallMs = 4 * 60_000, stallToolMs = 20 * 60_000, env = process.env, nodePath = process.execPath, log = () => {}, startTimeoutMs = 120_000, sessionReadyMs = 60_000, sessionRetryMs = 1_500 }) {
|
||||
const root = join(dataDir, 'agent');
|
||||
const home = join(root, 'home');
|
||||
const sessionsFile = join(root, 'sessions.json');
|
||||
@@ -171,7 +173,7 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
|
||||
/** The agent's profile now, plus where its shaped routes really lead (read by the forwarding endpoint). */
|
||||
let shapeTargets = {};
|
||||
function buildProfileNow() {
|
||||
const profile = buildProfile({ local: localInstances(), connections: connections(), gate, tools: tools(), skillsDir: skillsDir(), proxyBase, searchMock });
|
||||
const profile = buildProfile({ local: localInstances(), connections: connections(), gate, tools: tools(), skillsDir: skillsDir(), proxyBase, searchMock, defaultLocalId: defaultLocalId() });
|
||||
shapeTargets = profile.shapeTargets ?? {};
|
||||
return profile;
|
||||
}
|
||||
@@ -193,6 +195,8 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
|
||||
harness: { version: h.version },
|
||||
runtime: rt?.hp.running ? { pid: rt.hp.pid, generation: rt.generation } : null,
|
||||
routes: profile.routes.map(({ route, kind, modelId, name, contextWindow }) => ({ route, kind, modelId, name, contextWindow })),
|
||||
// tool servers that could not be wired (a name the rule does not allow, no program or address): the chat says so
|
||||
skippedServers: profile.mcpSkipped ?? [],
|
||||
activeTurns: activeTurns(),
|
||||
pendingApprovals: pending.size,
|
||||
};
|
||||
@@ -406,7 +410,27 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
|
||||
const appliedMode = planTask ? 'plan' : modeOf(mode);
|
||||
await applyMode(runtime, sessionId, appliedMode);
|
||||
|
||||
const projector = new TurnProjector({ policy: MODE_PRESETS[appliedMode].approval });
|
||||
// Does the message fit the model's window? Asked before anything is sent, with the model server's own count (context-fit.mjs). A message
|
||||
// that fits once the older messages are a summary is sent after a summary; one that can never fit is not sent, and the person is told
|
||||
// plainly and offered the two ways out. Whatever cannot be counted goes through: the Harness recovers from a refusal by itself.
|
||||
if (!goal && route.kind === 'local') {
|
||||
const fit = await fitCheck({ runtime, route, sessionId, fresh, history, message }).catch(() => null);
|
||||
if (fit?.action === 'too-large') {
|
||||
reporter.clear();
|
||||
const values = [grouped(fit.tokens), grouped(fit.window)];
|
||||
const text = fill(FIT_TEXT.tooLarge, values);
|
||||
emit({ type: 'error', message: text, key: FIT_TEXT.tooLarge, values, code: 'CONTEXT_MESSAGE_TOO_LARGE', tokens: fit.tokens, window: fit.window });
|
||||
return { error: text };
|
||||
}
|
||||
if (fit?.action === 'summarize') {
|
||||
reporter.set({ phase: 'compacting', code: 'compacting', text: FIT_TEXT.summarizing, key: FIT_TEXT.summarizing, values: [] });
|
||||
const done = await compactChat(runtime, sessionId);
|
||||
reporter.clear();
|
||||
if (done.ok) emit(summarizedNote(done.before, done.after, fit.window));
|
||||
}
|
||||
}
|
||||
|
||||
const projector = new TurnProjector({ policy: MODE_PRESETS[appliedMode].approval, window: route.contextWindow });
|
||||
if (!goal) guard = new TurnGuard({ plan: !!planTask, contextMax: route.kind === 'local' ? route.contextWindow : 0 }); // a saved connection's window is often only assumed: never judged by a guess
|
||||
projector.onStatus = update => { if (!turn?.parentOpen && turn?.children.size && [...turn.children.values()].some(h => !h.done) && update.phase === 'waiting') return; reporter.set(update); };
|
||||
const turn = {
|
||||
@@ -415,7 +439,7 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
|
||||
abortFromRuntime: reason => finishTurn(reason), finish: failure => finishTurn(failure),
|
||||
// Helper agents (subagents) the Harness started for this chat, and the state of the run around them.
|
||||
children: new Map(), subagentCalls: new Map(), parentOpen: false, stopping: false, settleTimer: null, mode: appliedMode,
|
||||
plan: null, advancing: false, goalRun: false, lastActivity: Date.now(), watchdog: null,
|
||||
plan: null, advancing: false, goalRun: false, lastActivity: Date.now(), heard: Date.now(), engineMark: null, watchdog: null,
|
||||
research: researchTopic ? { phase: 'gather', nudged: false, waitingStart: false } : null,
|
||||
};
|
||||
c.turn = turn;
|
||||
@@ -428,6 +452,13 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
|
||||
if (turn.finished || turn.stopping) return;
|
||||
const toolRunning = [...projector.tools.values()].some(x => x.card?.state === 'running' || x.card?.state === 'proposed') || pending.size > 0 && pendingFor(chatId).length > 0;
|
||||
const limit = toolRunning ? stallToolMs : stallMs;
|
||||
// A local engine reading a long prompt sends the Harness nothing for minutes (#3623). Its /metrics progress counts as activity,
|
||||
// for at most engineGraceMs after the Harness last spoke: progress for another chat cannot keep a lost turn alive forever.
|
||||
if (turn.route?.kind === 'local' && Date.now() - turn.heard < engineGraceMs) {
|
||||
let mark = null;
|
||||
try { mark = engineProgress(turn.route.instanceId); } catch { /* no progress known: the Harness alone decides */ }
|
||||
if (mark != null && mark !== turn.engineMark) { turn.engineMark = mark; turn.lastActivity = Date.now(); return; }
|
||||
}
|
||||
if (Date.now() - turn.lastActivity < limit) return;
|
||||
turn.stalledFor = Math.round((Date.now() - turn.lastActivity) / 1000);
|
||||
turn.stopping = true;
|
||||
@@ -501,6 +532,7 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
|
||||
function handle(item) {
|
||||
if (turn.finished) return;
|
||||
turn.lastActivity = Date.now();
|
||||
turn.heard = turn.lastActivity;
|
||||
if (item.type === 'event') {
|
||||
const e = item.event;
|
||||
if (e.seq <= turn.lastSeq) return;
|
||||
@@ -538,7 +570,7 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
|
||||
emitRow(child, true);
|
||||
const fold = item => {
|
||||
if (turn.finished) return;
|
||||
turn.lastActivity = Date.now();
|
||||
turn.lastActivity = turn.heard = Date.now();
|
||||
// A helper that finishes before its throttled "working" row went out would show queued -> completed and never "working": send the held row first.
|
||||
if (item.type === 'event' && item.event.type === 'turn/end' && child.rowPending) emitRow(child, true);
|
||||
const { frames, row } = cp.consume(item);
|
||||
@@ -951,6 +983,16 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
|
||||
|
||||
// -- /plan -----------------------------------------------------------------------------------------------
|
||||
|
||||
/** A short answer in the chat that the page translates (the sentence is in the interface catalog, "values" fill its slots). */
|
||||
function sayKey(options, template, values = []) {
|
||||
const { onFrame } = options;
|
||||
onFrame({ type: 'queued', requestId: 'compact-' + randomUUID() });
|
||||
onFrame({ type: 'start', model: '', contextMax: 0, demo: false, remoteModel: false, agent: true, mode: modeOf(options.mode), skills: [] });
|
||||
onFrame({ type: 'delta', text: fill(template, values), key: template, values });
|
||||
onFrame({ type: 'done', finishReason: 'stop', tokensIn: null, tokensOut: null, usageSource: 'unavailable', durationMs: 0 });
|
||||
return { compact: true };
|
||||
}
|
||||
|
||||
/** A short answer in the chat (no model): the same frames a turn sends. */
|
||||
function sayOnce(options, reply) {
|
||||
const { onFrame } = options;
|
||||
@@ -1150,6 +1192,93 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
|
||||
* Stop one chat's turn. Resolves only once the turn has ended (or the wait times out), because the
|
||||
* Harness acknowledges a cancel as admitted, not as done.
|
||||
*/
|
||||
/** What the chat holds now and the model's window, as the context plugin measures them (null when it cannot be read). */
|
||||
async function contextOf(runtime, sessionId) {
|
||||
try {
|
||||
const r = await runtime.gw.rpc('commands/execute', { agentId: sessionId, line: '/strata-context', submittedAttachments: [] });
|
||||
if (r?.result?.kind !== 'success') return null;
|
||||
const v = JSON.parse(String(r.result.text));
|
||||
return Number.isFinite(v.tokens) ? { tokens: Math.round(v.tokens), window: Number(v.window) > 0 ? Math.round(v.window) : 0 } : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
/** The decision of context-fit.mjs for the message about to be sent, or null when the model server cannot count it. */
|
||||
async function fitCheck({ runtime, route, sessionId, fresh, history, message }) {
|
||||
const inst = localInstances().find(i => i.id === route.instanceId);
|
||||
if (!inst || !(route.contextWindow > 0)) return null;
|
||||
const carried = fresh && history.length ? withEarlierConversation(history, message) : message;
|
||||
if (carried.length < COUNT_ABOVE_CHARS) return null;
|
||||
const count = text => countTokens({ baseUrl: inst.baseUrl, key: inst.key, model: inst.modelId, text });
|
||||
let tokens = await count(carried);
|
||||
if (tokens == null) return null;
|
||||
const now = fresh ? null : await contextOf(runtime, sessionId);
|
||||
const window = now?.window || route.contextWindow;
|
||||
const pressure = now?.tokens ?? 0;
|
||||
// Where the Harness starts summarising by itself on this window (profile.mjs: the reply keeps its room, then the headroom).
|
||||
const reply = localOutputBudget(window);
|
||||
const threshold = Math.floor(Math.min(window * 0.8, window - reply - compactionHeadroom(window, reply)));
|
||||
let verdict = decideFit({ window, pressure, messageTokens: tokens, threshold });
|
||||
// The earlier conversation carried into a fresh session is what pushed it over: send the message alone instead.
|
||||
if (verdict.action === 'too-large' && carried !== message) {
|
||||
const alone = await count(message);
|
||||
if (alone != null) { tokens = alone; verdict = decideFit({ window, pressure, messageTokens: alone, threshold }); }
|
||||
}
|
||||
return { ...verdict, tokens, window, pressure };
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarise the older part of a chat with the Harness's /compact and say what happened. Never throws.
|
||||
* ok:true carries the figures before and after; ok:false says why (nothing to summarise, busy, failed).
|
||||
*/
|
||||
async function compactChat(runtime, sessionId) {
|
||||
const before = await contextOf(runtime, sessionId);
|
||||
let result;
|
||||
try { result = (await runtime.gw.rpc('commands/execute', { agentId: sessionId, line: '/compact', submittedAttachments: [] }))?.result; }
|
||||
catch (e) { return { ok: false, reason: /not.found|no such/i.test(String(e.message)) ? 'nothing' : 'failed', before, text: String(e.message ?? e) }; }
|
||||
const text = String(result?.text ?? '');
|
||||
log('agent · compact: ' + (result?.kind ?? 'none') + ' ' + text.slice(0, 140));
|
||||
if (result?.kind === 'success' && /^Compacted/i.test(text)) return { ok: true, before, after: await contextOf(runtime, sessionId) };
|
||||
if (result?.kind === 'success') return { ok: false, reason: 'nothing', before, text };
|
||||
if (/active compaction|not idle/i.test(text)) return { ok: false, reason: 'busy', before, text };
|
||||
// The Harness cannot make a summary smaller than what is left when the chat holds little beyond its first prompt: that is nothing to summarise, not a failure.
|
||||
const little = before && before.window > 0 && before.tokens - BASE_TOKENS < before.window * 0.25;
|
||||
return { ok: false, reason: little && /useful summary|not smaller/i.test(text) ? 'nothing' : 'failed', before, text };
|
||||
}
|
||||
|
||||
/** The line in the chat that explains why the context meter fell: how full it was and how full it is. */
|
||||
function summarizedNote(before, after, window) {
|
||||
const w = after?.window || before?.window || window || 0;
|
||||
const pct = v => Math.max(0, Math.min(100, Math.round(100 * v.tokens / w)));
|
||||
const known = w > 0 && before && after;
|
||||
const template = known ? FIT_TEXT.summarized : FIT_TEXT.summarizedShort;
|
||||
const values = known ? [pct(before), pct(after)] : [];
|
||||
return { type: 'delta', own: true, text: '\n\n' + fill(template, values), key: template, values, noteReason: 'compacted' };
|
||||
}
|
||||
|
||||
/** "/compact": summarise the older messages of this chat now, and say in plain words what happened. */
|
||||
async function compactCommand(options) {
|
||||
const { chatId, choice, signal } = options;
|
||||
const c = chat(chatId);
|
||||
const say = (template, values = []) => sayKey(options, template, values);
|
||||
if (c.turn || c.starting) return say(FIT_TEXT.compactBusy);
|
||||
const known = sessionMap[chatId]?.sessionId;
|
||||
if (!known) return say(FIT_TEXT.noChatYet);
|
||||
let runtime;
|
||||
try { ({ rt: runtime } = await ensureRuntime(choice, signal)); } catch { return say(FIT_TEXT.compactFailed); }
|
||||
const done = await compactChat(runtime, known);
|
||||
if (done.ok) {
|
||||
const note = summarizedNote(done.before, done.after, 0);
|
||||
return say(note.key, note.values);
|
||||
}
|
||||
if (done.reason === 'busy') return say(FIT_TEXT.compactBusy);
|
||||
if (done.reason === 'nothing') {
|
||||
const w = done.before?.window || 0;
|
||||
const pct = w > 0 && done.before ? Math.round(100 * done.before.tokens / w) : 0;
|
||||
return say(pct >= 60 ? FIT_TEXT.nothingFull : FIT_TEXT.nothingToSummarize, pct >= 60 ? [pct] : []);
|
||||
}
|
||||
return say(FIT_TEXT.compactFailed);
|
||||
}
|
||||
|
||||
/** Summarise the older part of a session with the Harness's own /compact. Best effort: the next message compacts again by itself when it must. */
|
||||
async function compactSession(runtime, sessionId) {
|
||||
try {
|
||||
@@ -1204,7 +1333,7 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
|
||||
/** Let go of a chat's Harness session mapping (a task starts every run in a session of its own). */
|
||||
function forget(chatId) { if (chats.get(chatId)?.turn) return; delete sessionMap[chatId]; chats.delete(chatId); persist(); }
|
||||
|
||||
return { goalState, goalControl, harnessDir, status, runTurn, goalCommand, planCommand, researchCommand, llmTarget, setPlanBriefing, resetSession, reopenPlan, skipStep, planGoal, planControl, planState, queueMessage, cancelQueued, takeQueue, forget, approve, setMode, stop, pendingFor, pendingAll, close, sessionOf: chatId => sessionMap[chatId]?.sessionId ?? null, workingFolder: chatId => sessionMap[chatId]?.cwd ?? null, isRunning: chatId => !!chats.get(chatId)?.turn, _debug: () => ({ rt, sessionMap }) };
|
||||
return { compactCommand, goalState, goalControl, harnessDir, status, runTurn, goalCommand, planCommand, researchCommand, llmTarget, setPlanBriefing, resetSession, reopenPlan, skipStep, planGoal, planControl, planState, queueMessage, cancelQueued, takeQueue, forget, approve, setMode, stop, pendingFor, pendingAll, close, sessionOf: chatId => sessionMap[chatId]?.sessionId ?? null, workingFolder: chatId => sessionMap[chatId]?.cwd ?? null, isRunning: chatId => !!chats.get(chatId)?.turn, _debug: () => ({ rt, sessionMap }) };
|
||||
}
|
||||
|
||||
/** A goal that stopped by itself, in words. */
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Token arithmetic for the agent's context, with no I/O (the Harness plugin `plugins/strata-context.mjs` and the host both use it).
|
||||
*
|
||||
* Why it exists. The Harness prices a conversation with a fixed 4 characters per token. Code, logs and JSON are about twice as dense
|
||||
* (2.09 characters per token in the 2026-10-22 hands-on test), so its pressure read 31,794 tokens while the engine had been sent 59,048,
|
||||
* and compaction waited until the window was already past full. The engine reports the real prompt size with every answer; this module
|
||||
* turns that report into the number compaction decides on, and splits a summarising request that would not fit the window.
|
||||
*/
|
||||
|
||||
/** The Harness's own price of a text: characters over this, plus a fixed cost per block and per message. */
|
||||
export const CHARS_PER_TOKEN = 4;
|
||||
const BLOCK_OVERHEAD = 4;
|
||||
const MESSAGE_OVERHEAD = 4;
|
||||
/** The most a real token can be worth in the Harness's units (one character per token is the worst a tokenizer does). */
|
||||
export const MAX_DENSITY = 6;
|
||||
/** Room kept free in a window for the engine's own framing and the chat template (the engine keeps 64 tokens of slack itself). */
|
||||
export const WINDOW_SLACK = 1024;
|
||||
|
||||
/** The Harness's price (4 characters a token) of one message, the same arithmetic as its token meter. */
|
||||
export function estimateMessage(message) {
|
||||
const content = Array.isArray(message?.content) ? message.content : [];
|
||||
if (message?.role === 'system') {
|
||||
if (!content.length) return 0;
|
||||
let chars = 0;
|
||||
for (const block of content) chars += block?.type === 'text' ? String(block.text ?? '').length : JSON.stringify(block ?? {}).length;
|
||||
return Math.ceil(chars / CHARS_PER_TOKEN) + MESSAGE_OVERHEAD;
|
||||
}
|
||||
let tokens = MESSAGE_OVERHEAD;
|
||||
for (const block of content) {
|
||||
if (block?.type === 'text' || block?.type === 'reasoning') tokens += Math.ceil(String(block.text ?? '').length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
|
||||
else if (block?.type === 'tool-call') tokens += Math.ceil(String(block.name ?? '').length / CHARS_PER_TOKEN) + Math.ceil(String(block.arguments ?? '').length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
|
||||
else tokens += BLOCK_OVERHEAD + Math.ceil(JSON.stringify(block ?? {}).length / CHARS_PER_TOKEN);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/** The Harness's price of the tool list of a request header. */
|
||||
export function estimateTools(header) {
|
||||
const tools = header?.tools;
|
||||
if (!Array.isArray(tools) || !tools.length) return 0;
|
||||
return Math.ceil(JSON.stringify(tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
|
||||
}
|
||||
|
||||
/**
|
||||
* How many real tokens one of the Harness's tokens is worth, from one measured request: the engine said the prompt was `real` tokens,
|
||||
* the Harness priced the same prompt at `estimated`. Never below 1 (a conversation of plain prose is priced about right, and a lower
|
||||
* number would only delay compaction) and never above MAX_DENSITY (a wild sample must not make every step compact).
|
||||
*/
|
||||
export function densityOf(real, estimated) {
|
||||
if (!(real > 0) || !(estimated > 0)) return 1;
|
||||
return Math.min(MAX_DENSITY, Math.max(1, real / estimated));
|
||||
}
|
||||
|
||||
/**
|
||||
* The prompt size compaction decides on. `sample` is the last measured request: `real` tokens for a prompt the Harness priced at
|
||||
* `estimated`. `now` is the Harness's price of the conversation now. What was added or removed since the sample is scaled by the sample's
|
||||
* density; the measured part is exact. Without a sample, or when the measurement is lower than the Harness's own price, the Harness's
|
||||
* price stands (it is never an underestimate that makes this smaller).
|
||||
*/
|
||||
export function pressureNow({ sample = null, now }) {
|
||||
if (!sample || !(sample.real > 0) || !(sample.estimated > 0)) return Math.max(0, now);
|
||||
const density = densityOf(sample.real, sample.estimated);
|
||||
return Math.max(Math.round(now), Math.round(sample.real + density * (now - sample.estimated)));
|
||||
}
|
||||
|
||||
/** The usage a finished model request reported, as the prompt side only: input plus the cache's reads and writes (output is not in the prompt). */
|
||||
export function promptTokensOf(usage) {
|
||||
if (!usage || typeof usage !== 'object') return 0;
|
||||
const n = value => (Number.isFinite(Number(value)) ? Math.max(0, Number(value)) : 0);
|
||||
return n(usage.inputTokens) + n(usage.cacheReadTokens) + n(usage.cacheWriteTokens);
|
||||
}
|
||||
|
||||
/** The usage of a settled assistant event (a `usage` on the message, or the last usage chunk of its stream). */
|
||||
export function usageOfEvent(event) {
|
||||
const data = event?.data;
|
||||
if (!data) return null;
|
||||
if (data.usage && typeof data.usage === 'object') return data.usage;
|
||||
const stream = Array.isArray(data.stream) ? data.stream : [];
|
||||
for (let i = stream.length - 1; i >= 0; i--) if (stream[i]?.type === 'chunk' && stream[i].chunk?.type === 'usage') return stream[i].chunk.usage ?? null;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A summarising request is: the conversation's system prompt and tools, the messages to condense, then the instruction. This is how many
|
||||
* real tokens the messages may take in a window so that all of it, the summary and a safety margin fit.
|
||||
*/
|
||||
export function summaryBudget({ window, summaryTokens, toolsTokens = 0, systemTokens = 0, instructionTokens = 900, slack = WINDOW_SLACK }) {
|
||||
return Math.floor(window - summaryTokens - toolsTokens - systemTokens - instructionTokens - slack);
|
||||
}
|
||||
|
||||
/** Text blocks of a message, longest first: the ones worth shortening when a single message is bigger than the budget. */
|
||||
function textBlocks(message) {
|
||||
return (Array.isArray(message?.content) ? message.content : []).filter(b => (b?.type === 'text' || b?.type === 'reasoning') && typeof b.text === 'string');
|
||||
}
|
||||
|
||||
/**
|
||||
* One message cut down to `maxTokens` (in the Harness's price times `density`): the start and the end of its longest text are kept and
|
||||
* the middle is replaced by a line saying how much was left out. Only the copy sent to the summariser is cut; the conversation keeps
|
||||
* the whole message until the summary replaces it.
|
||||
*/
|
||||
export function clipMessage(message, maxTokens, density = 1) {
|
||||
if (estimateMessage(message) * density <= maxTokens) return message;
|
||||
const blocks = textBlocks(message);
|
||||
if (!blocks.length) return message;
|
||||
const others = estimateMessage({ ...message, content: (message.content ?? []).filter(b => !blocks.includes(b)) }) * density;
|
||||
const room = Math.max(256, maxTokens - others);
|
||||
const share = room / blocks.reduce((n, b) => n + b.text.length, 0) / density * CHARS_PER_TOKEN; // characters kept per character
|
||||
const content = (message.content ?? []).map((block) => {
|
||||
if (!blocks.includes(block)) return block;
|
||||
const keep = Math.max(200, Math.floor(block.text.length * Math.min(1, share) - 120));
|
||||
if (block.text.length <= keep) return block;
|
||||
const head = Math.ceil(keep * 0.6), tail = keep - head;
|
||||
const left = block.text.length - head - tail;
|
||||
return { ...block, text: `${block.text.slice(0, head)}\n\n[… ${left.toLocaleString('en-US')} characters from the middle are left out of this summary request …]\n\n${block.text.slice(block.text.length - tail)}` };
|
||||
});
|
||||
return { ...message, content };
|
||||
}
|
||||
|
||||
/** Whether cutting the list in front of index `i` would separate a tool call from its result. */
|
||||
function balancedBefore(messages, i) {
|
||||
let open = 0;
|
||||
for (let k = 0; k < i; k++) {
|
||||
const m = messages[k];
|
||||
if (m?.role === 'assistant') open += (Array.isArray(m.content) ? m.content : []).filter(b => b?.type === 'tool-call').length;
|
||||
else if (m?.role === 'tool') open -= 1;
|
||||
}
|
||||
return open <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split messages into runs that each fit `budget` real tokens (price `density`), cutting between a tool call's result and the next
|
||||
* message when it can. A message bigger than the budget by itself is clipped first. Order is kept.
|
||||
*/
|
||||
export function splitMessages(messages, budget, density = 1) {
|
||||
const room = Math.max(512, budget);
|
||||
const sized = messages.map(m => clipMessage(m, Math.floor(room * 0.8), density));
|
||||
const cost = sized.map(m => estimateMessage(m) * density);
|
||||
const runs = [];
|
||||
let start = 0, used = 0;
|
||||
for (let i = 0; i < sized.length; i++) {
|
||||
if (i > start && used + cost[i] > room) {
|
||||
// Cut as late as possible, but never between a tool call and its result when a clean place exists inside the run.
|
||||
let cut = i;
|
||||
while (cut > start + 1 && !balancedBefore(sized, cut)) cut--;
|
||||
if (!balancedBefore(sized, cut)) cut = i;
|
||||
runs.push(sized.slice(start, cut));
|
||||
start = cut;
|
||||
used = cost.slice(start, i).reduce((n, x) => n + x, 0);
|
||||
}
|
||||
used += cost[i];
|
||||
}
|
||||
if (start < sized.length) runs.push(sized.slice(start));
|
||||
return runs;
|
||||
}
|
||||
|
||||
/** Whether a failure is the model server saying the prompt does not fit its window. */
|
||||
export function isContextOverflow(error) {
|
||||
if (!error) return false;
|
||||
if (error.code === 'CONTEXT_WINDOW_EXCEEDED') return true;
|
||||
const text = [error.message, error.cause?.message, error.failure?.message, error.failure?.code].filter(Boolean).join(' ');
|
||||
return /request exceeds the context window|leaves no room to answer|context[_ ]length|context window|too many tokens|maximum context/i.test(text);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Does a message fit the model's window? Asked before it is sent, with the model server's own count, so a message the server would refuse
|
||||
* is never read for half a minute first (2026-10-22: a 103 KB paste was 70,141 tokens in a 65,536-token window, and the 400 came back after
|
||||
* the whole prompt had been read). No I/O except the one count request; everything else is arithmetic the tests call directly.
|
||||
*
|
||||
* Three answers: send it; summarise the chat first and then send it (the message fits once the older messages are a summary); or it can
|
||||
* never fit, and the person is offered the two ways out that do (the text as a file the assistant reads in parts, or a shortened preview).
|
||||
*/
|
||||
|
||||
/** What a new chat already holds before the person's first word: the system prompt and the tool list. Measured 10,510 by the Harness's price. */
|
||||
export const BASE_TOKENS = 12000;
|
||||
/** The least room a reply needs after the prompt: an eighth of the window, at most this. */
|
||||
export const REPLY_FLOOR = 4096;
|
||||
/** A message shorter than this many characters cannot matter to a window of 8K or more, so it is not even counted. */
|
||||
export const COUNT_ABOVE_CHARS = 3000;
|
||||
|
||||
export const replyRoom = window => Math.max(512, Math.min(REPLY_FLOOR, Math.floor(window / 8)));
|
||||
|
||||
/**
|
||||
* @param {{window:number, pressure?:number, base?:number, messageTokens:number, threshold?:number}} input
|
||||
* `pressure` is what the chat holds now (measured), `base` what an empty chat holds, `messageTokens` the new message, `threshold` where the
|
||||
* Harness starts summarising by itself. The Harness looks at that only before its NEXT step, so a big message that arrives now would otherwise
|
||||
* be sent on top of a chat already near the threshold (2026-10-22: 54K of 64K held by the time it was looked at): summarise first.
|
||||
* @returns {{action:'send'|'summarize'|'too-large', room:number, needs:number}}
|
||||
*/
|
||||
export function decideFit({ window, pressure = 0, base: given = BASE_TOKENS, messageTokens, threshold = 0 }) {
|
||||
const reply = replyRoom(window);
|
||||
const base = Math.min(given, Math.floor(window * 0.6)); // on a small window the first prompt cannot be what the window is made of
|
||||
// A chat holds at least what an empty one does (the system prompt and the tools), so a new chat is not counted as empty.
|
||||
const needs = Math.max(pressure, base) + messageTokens + reply;
|
||||
if (!(window > 0) || !(messageTokens >= 0)) return { action: 'send', room: Infinity, needs: 0 };
|
||||
if (needs <= window) {
|
||||
const worthIt = pressure > base + window * 0.1; // older messages worth summarising: more than a tenth of the window beyond the first prompt
|
||||
if (threshold > 0 && worthIt && pressure + messageTokens > threshold) return { action: 'summarize', room: window - needs, needs, why: 'threshold' };
|
||||
return { action: 'send', room: window - needs, needs };
|
||||
}
|
||||
// Not as it stands. Does the message fit in an otherwise empty chat (only the system prompt and the tools before it)?
|
||||
if (base + messageTokens + reply <= window) return { action: 'summarize', room: window - (base + messageTokens + reply), needs };
|
||||
return { action: 'too-large', room: window - (base + messageTokens + reply), needs };
|
||||
}
|
||||
|
||||
/**
|
||||
* The model server's count of a text (its tokenizer and chat template), through its Anthropic-style count endpoint.
|
||||
* Returns the number, or null when it cannot be had (an old engine, a server that is busy, a timeout): then nothing is blocked.
|
||||
*/
|
||||
export async function countTokens({ baseUrl, key = '', model = '', text, fetchImpl = fetch, timeoutMs = 8000 }) {
|
||||
try {
|
||||
const root = String(baseUrl ?? '').replace(/\/v1\/?$/, '').replace(/\/$/, '');
|
||||
const res = await fetchImpl(`${root}/v1/messages/count_tokens`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', ...(key ? { authorization: `Bearer ${key}`, 'x-api-key': key } : {}) },
|
||||
body: JSON.stringify({ model, messages: [{ role: 'user', content: String(text) }] }),
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const n = Number((await res.json())?.input_tokens);
|
||||
return Number.isFinite(n) && n >= 0 ? Math.round(n) : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
/** The plain sentences (also in the interface catalog in all eight languages). `{0}` slots are filled by the page. */
|
||||
export const FIT_TEXT = Object.freeze({
|
||||
tooLarge: 'This message is about {0} tokens long, and this model can read about {1} at a time. It is too large to send as it is.',
|
||||
tooLargeNext: 'Attach it as a file for the assistant to read in parts, or send a shortened preview.',
|
||||
full: 'This conversation no longer fits the model’s window, and summarizing the older messages did not make enough room.',
|
||||
fullNext: 'Summarize it now and continue, or start a new chat.',
|
||||
summarizing: 'Summarizing earlier messages…',
|
||||
summarized: 'Older messages were summarized to make room. The chat was {0}% full and is now {1}% full.',
|
||||
summarizedWas: 'Older messages were summarized to make room. The chat was {0}% full.',
|
||||
summarizedShort: 'Older messages were summarized to make room.',
|
||||
nothingFull: 'This chat is {0}% full, but there is nothing older to summarize: the newest message is what fills it. Start a new chat, or send a shorter message.',
|
||||
nothingToSummarize: 'There is nothing older to summarize yet. Everything in this chat is still needed by the assistant.',
|
||||
noChatYet: 'There is nothing to summarize yet. This chat has not started a conversation with the assistant.',
|
||||
compactFailed: 'The older messages could not be summarized right now. Try again in a moment, or start a new chat.',
|
||||
compactBusy: 'This chat is still working. Stop it or wait for it to finish, then try again.',
|
||||
});
|
||||
export const fill = (template, values = []) => String(template).replace(/\{(\d+)\}/g, (_, i) => String(values[Number(i)] ?? ''));
|
||||
|
||||
/** A number with thousands separators, the way the page writes it. */
|
||||
export const grouped = n => Math.round(n).toLocaleString('en-US');
|
||||
|
||||
/**
|
||||
* The two ways to send a message that is too large.
|
||||
* file the whole text goes in a file in the chat's folder; the message names it and shows how it starts
|
||||
* preview the start and the end of the text, cut to what the window can hold, with the number of characters left out
|
||||
* @param {{text:string, mode:'file'|'preview', tokens:number, window:number, pressure?:number, base?:number, path?:string}} input
|
||||
* @returns {{message:string, kept:number}}
|
||||
*/
|
||||
export function oversizedReplacement({ text, mode, tokens, window, pressure = 0, base = BASE_TOKENS, path = '' }) {
|
||||
const t = String(text);
|
||||
const charsPerToken = tokens > 0 ? Math.max(0.8, t.length / tokens) : 3;
|
||||
if (mode === 'file') {
|
||||
const start = t.slice(0, 1200).trimEnd();
|
||||
return { kept: start.length, message: `My message was too long to send in the chat, so I saved it as a file: ${path}\n\nIt starts like this:\n\n${start}\n\n[the rest is in the file]\n\nPlease read the file in parts (it is long) and then do what my message asks.` };
|
||||
}
|
||||
const room = Math.max(1000, Math.floor((window - Math.max(pressure, base) - replyRoom(window)) * 0.7));
|
||||
const keep = Math.max(500, Math.floor(room * charsPerToken * 0.9));
|
||||
const head = Math.ceil(keep * 0.7), tail = keep - head;
|
||||
const left = Math.max(0, t.length - head - tail);
|
||||
if (!left) return { kept: t.length, message: t };
|
||||
return { kept: head + tail, message: `${t.slice(0, head)}\n\n[… ${grouped(left)} characters from the middle are left out because the message was too long to send …]\n\n${t.slice(t.length - tail)}` };
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Runs the command of a command tool the person wrote (Settings -> Tools). Split from custom-tools-mcp.mjs so it can be tested.
|
||||
*
|
||||
* T7 (tools and images review, 2026-10-22). It used to run every command with `shell: true` (cmd.exe on Windows, so `Get-Content x` failed with
|
||||
* "not recognized"), in whatever folder the host happened to run in (so `python tools/foo.py` could not find its file), and ended it after two
|
||||
* minutes with 20,000 characters of output, silently cut.
|
||||
*
|
||||
* shell 'auto' (default): PowerShell for a Windows command that reads as PowerShell (Get-Content, Start-Process, $env:X, $(...)), cmd.exe for
|
||||
* any other Windows command (so every command that worked keeps working), /bin/sh elsewhere. 'cmd', 'powershell', 'bash' and 'sh' name one.
|
||||
* cwd the folder to run in: the tool's own, or the one the assistant names in the call (it knows the chat's folder); else the person's home.
|
||||
* timeoutSec the time allowed, 1 to 3600 (default 120). A command that runs over is ended together with the programs it started.
|
||||
*/
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { existsSync, statSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
export const DEFAULT_TIMEOUT_SEC = 120;
|
||||
export const MAX_TIMEOUT_SEC = 3600;
|
||||
export const OUT_MAX = 20000;
|
||||
export const SHELLS = ['auto', 'cmd', 'powershell', 'bash', 'sh'];
|
||||
|
||||
/** A Windows command that is PowerShell and would fail in cmd.exe. */
|
||||
const POWERSHELL_LOOKS = /(^|[\s;|(&])[A-Z][A-Za-z]+-[A-Z][A-Za-z]+\b|\$\w|\$\(|\$env:|@\(|\|\s*(?:%|\?)\s*\{/;
|
||||
export const looksLikePowerShell = command => POWERSHELL_LOOKS.test(String(command ?? ''));
|
||||
|
||||
let powershellFound;
|
||||
/** pwsh when it is installed (PowerShell 7), else the Windows PowerShell every Windows has. */
|
||||
function powershellExe(platform) {
|
||||
if (powershellFound) return powershellFound;
|
||||
if (platform !== 'win32') return (powershellFound = 'pwsh');
|
||||
try { if (spawnSync('where', ['pwsh'], { windowsHide: true, timeout: 5000 }).status === 0) return (powershellFound = 'pwsh'); } catch { /* none */ }
|
||||
return (powershellFound = 'powershell.exe');
|
||||
}
|
||||
|
||||
/**
|
||||
* -> { file, args, shell } for spawn: which program runs `command`.
|
||||
* @param {string} command
|
||||
* @param {{shell?: string, platform?: string}} [options]
|
||||
*/
|
||||
export function planShell(command, { shell = 'auto', platform = process.platform } = {}) {
|
||||
const windows = platform === 'win32';
|
||||
let choice = SHELLS.includes(shell) ? shell : 'auto';
|
||||
if (choice === 'auto') choice = windows ? (looksLikePowerShell(command) ? 'powershell' : 'cmd') : 'sh';
|
||||
if (choice === 'powershell') return { file: powershellExe(platform), args: ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', command], shell: false, name: 'PowerShell' };
|
||||
if (choice === 'bash') return { file: 'bash', args: ['-c', command], shell: false, name: 'bash' };
|
||||
if (choice === 'sh') return windows ? { file: 'sh', args: ['-c', command], shell: false, name: 'sh' } : { file: command, args: [], shell: true, name: 'sh' };
|
||||
return { file: command, args: [], shell: true, name: windows ? 'cmd.exe' : 'sh' }; // cmd: spawn's shell:true is cmd.exe on Windows
|
||||
}
|
||||
|
||||
/** The folder a command runs in: -> { cwd } or { error } with a plain sentence. */
|
||||
export function resolveCwd(...candidates) {
|
||||
for (const candidate of candidates) {
|
||||
const text = typeof candidate === 'string' ? candidate.trim() : '';
|
||||
if (!text) continue;
|
||||
const expanded = text.replace(/^~(?=$|[\\/])/, homedir());
|
||||
try {
|
||||
if (!existsSync(expanded) || !statSync(expanded).isDirectory()) return { error: `The folder “${text}” does not exist, so the command was not run.` };
|
||||
} catch { return { error: `The folder “${text}” could not be opened, so the command was not run.` }; }
|
||||
return { cwd: expanded };
|
||||
}
|
||||
return { cwd: homedir() };
|
||||
}
|
||||
|
||||
export const clampTimeoutSec = value => {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) && n >= 1 ? Math.min(Math.floor(n), MAX_TIMEOUT_SEC) : DEFAULT_TIMEOUT_SEC;
|
||||
};
|
||||
|
||||
function endTree(child) {
|
||||
try {
|
||||
if (process.platform === 'win32' && child.pid) spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, timeout: 10000 });
|
||||
else child.kill('SIGKILL');
|
||||
} catch { /* gone */ }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{command: string, input?: string, cwd?: string, callCwd?: string, shell?: string, timeoutSec?: number, platform?: string}} job
|
||||
* @returns {Promise<{ok: boolean, text: string}>}
|
||||
*/
|
||||
export function runCommand({ command, input = '', cwd, callCwd, shell = 'auto', timeoutSec, platform = process.platform }) {
|
||||
return new Promise(resolve => {
|
||||
const where = resolveCwd(callCwd, cwd);
|
||||
if (where.error) return resolve({ ok: false, text: where.error });
|
||||
const plan = planShell(command, { shell, platform });
|
||||
const limit = clampTimeoutSec(timeoutSec);
|
||||
let child;
|
||||
try {
|
||||
child = spawn(plan.file, plan.args, { shell: plan.shell, windowsHide: true, cwd: where.cwd, env: { ...process.env, STRATA_INPUT: String(input ?? '') }, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
} catch (e) { return resolve({ ok: false, text: `The command could not start in ${plan.name}: ${e.message}` }); }
|
||||
let text = '', cut = false, timedOut = false;
|
||||
const take = chunk => { if (text.length < OUT_MAX + 1) text += chunk.toString(); else cut = true; };
|
||||
child.stdout.on('data', take); child.stderr.on('data', take);
|
||||
const timer = setTimeout(() => { timedOut = true; endTree(child); }, limit * 1000);
|
||||
child.on('error', e => { clearTimeout(timer); resolve({ ok: false, text: `The command could not start in ${plan.name}: ${e.message}` }); });
|
||||
child.on('close', code => {
|
||||
clearTimeout(timer);
|
||||
if (text.length > OUT_MAX) { cut = true; text = text.slice(0, OUT_MAX); }
|
||||
const out = text.trim() + (cut ? `\n\n(The output was cut after ${OUT_MAX.toLocaleString('en-US')} characters.)` : '');
|
||||
if (timedOut) return resolve({ ok: false, text: `The command was stopped after ${limit} seconds.${out ? '\n' + out : ''}` });
|
||||
resolve({ ok: code === 0, text: out || (code === 0 ? 'The command finished without any error. It printed nothing.' : `The command failed (exit code ${code}) and printed nothing.`) });
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -8,49 +8,35 @@
|
||||
* It speaks the Model Context Protocol over stdio (newline-delimited JSON-RPC), the transport the Harness's MCP client uses for a program
|
||||
* it starts. The file is re-read on every request, so a change in Settings is seen without restarting anything. The assistant's text for
|
||||
* a command tool reaches the command in the STRATA_INPUT environment variable (never spliced into the command line, so it cannot add
|
||||
* commands of its own). A command gets 2 minutes and 20,000 characters of output.
|
||||
* commands of its own). A command runs in the shell that suits it (PowerShell for a Windows command that reads as PowerShell, cmd.exe for
|
||||
* the rest, sh elsewhere), in its own folder or the one the assistant names, gets 2 minutes unless its tool allows more (up to an hour) and
|
||||
* 20,000 characters of output (custom-command-run.mjs).
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createInterface } from 'node:readline';
|
||||
import { runCommand } from './custom-command-run.mjs';
|
||||
|
||||
const [mode, file] = process.argv.slice(2);
|
||||
if (!['instructions', 'commands'].includes(mode) || !file) { console.error('usage: custom-tools-mcp.mjs instructions|commands <tools.json>'); process.exit(2); }
|
||||
|
||||
const load = () => { try { return JSON.parse(readFileSync(file, 'utf8'))[mode] ?? {}; } catch { return {}; } };
|
||||
const OUT_MAX = 20000;
|
||||
const TIME_MS = 120000;
|
||||
|
||||
const schema = mode === 'commands'
|
||||
? { type: 'object', properties: { input: { type: 'string', description: 'Text to give the command (optional).' } }, additionalProperties: false }
|
||||
? { type: 'object', properties: {
|
||||
input: { type: 'string', description: 'Text to give the command (optional).' },
|
||||
cwd: { type: 'string', description: 'The folder to run the command in (optional). Give the working folder of this chat when the command uses relative paths.' },
|
||||
}, additionalProperties: false }
|
||||
: { type: 'object', properties: {}, additionalProperties: false };
|
||||
|
||||
function listTools() {
|
||||
return Object.entries(load()).map(([name, t]) => ({ name, description: String(t.description ?? ''), inputSchema: schema, annotations: mode === 'instructions' ? { readOnlyHint: true } : {} }));
|
||||
}
|
||||
|
||||
function runCommand(command, input) {
|
||||
return new Promise(resolve => {
|
||||
const child = spawn(command, { shell: true, windowsHide: true, env: { ...process.env, STRATA_INPUT: String(input ?? '') }, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let text = '';
|
||||
const take = chunk => { if (text.length < OUT_MAX) text += chunk.toString(); };
|
||||
child.stdout.on('data', take); child.stderr.on('data', take);
|
||||
const timer = setTimeout(() => { try { child.kill(); } catch { /* gone */ } }, TIME_MS);
|
||||
child.on('error', e => { clearTimeout(timer); resolve({ ok: false, text: `The command could not start: ${e.message}` }); });
|
||||
child.on('close', (code, signal) => {
|
||||
clearTimeout(timer);
|
||||
const out = text.slice(0, OUT_MAX).trim();
|
||||
if (signal) resolve({ ok: false, text: `The command was stopped after ${TIME_MS / 1000} seconds.${out ? '\n' + out : ''}` });
|
||||
else resolve({ ok: code === 0, text: out || (code === 0 ? 'The command finished without any error. It printed nothing.' : `The command failed (exit code ${code}) and printed nothing.`) });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function call(name, args) {
|
||||
const t = load()[name];
|
||||
if (!t) return { isError: true, content: [{ type: 'text', text: 'That tool is no longer available.' }] };
|
||||
if (mode === 'instructions') return { content: [{ type: 'text', text: String(t.text ?? '') }] };
|
||||
const r = await runCommand(String(t.command ?? ''), args?.input);
|
||||
const r = await runCommand({ command: String(t.command ?? ''), input: args?.input, callCwd: args?.cwd, cwd: t.cwd, shell: t.shell, timeoutSec: t.timeoutSec });
|
||||
return { isError: !r.ok, content: [{ type: 'text', text: r.text }] };
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* How far a local engine has got, as a short text that changes whenever it makes progress. The agent's stall watchdog
|
||||
* compares it with the last one it saw: while a long prompt is read the Harness hears nothing for minutes, but the
|
||||
* engine's /metrics (polled every 2 s) shows `live.prompt_read` moving (#3623). Null when the engine is idle or says nothing.
|
||||
*/
|
||||
export function engineProgressMark(instance) {
|
||||
const live = instance?.metrics?.live;
|
||||
if (!live || live.state === 'idle' || live.state === 'unloaded') return null;
|
||||
const slots = Array.isArray(live.slots) ? live.slots.map(s => `${s?.state}:${s?.prompt_read ?? ''}:${s?.generated ?? ''}`).join(',') : '';
|
||||
return `${live.state}|${live.prompt_read ?? ''}|${live.generated ?? ''}|${slots}`;
|
||||
}
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
import { toolUpdate, TokenCount, fmtTokens } from './status.mjs';
|
||||
import { askFor } from './ask.mjs';
|
||||
import { FIT_TEXT, fill } from './context-fit.mjs';
|
||||
import { isContextOverflow } from './context-budget.mjs';
|
||||
|
||||
const SHELL = new Set(['pwsh', 'bash', 'job_kill', 'job_output', 'job_list']);
|
||||
const WRITE = new Set(['write', 'edit']);
|
||||
@@ -157,6 +159,9 @@ export function withPlain(raw) {
|
||||
return plain ? `${plain}\n\nDetails: ${raw.replace(/^Error:\s*/, '')}` : raw;
|
||||
}
|
||||
|
||||
/** What a launch that printed nothing proves, and no more. */
|
||||
export const LAUNCHED_TEXT = 'The command finished without an error, so the program was started. Strata cannot confirm that a window or page actually opened.';
|
||||
|
||||
/** A command whose whole point is to open something: it has no output to show when it worked. */
|
||||
const LAUNCH = /(^|[\s;|&(])(start-process|start|invoke-item|ii|explorer(?:\.exe)?|xdg-open|open|notepad(?:\.exe)?|code)(?=[\s;|&)]|$)/i;
|
||||
|
||||
@@ -172,9 +177,10 @@ export function outcomeText(tool, text, args = {}) {
|
||||
text = text.replace(/\r?\n?\[stderr\]\r?\n(?:Cannot create type\. Only core types are supported in this language mode\.[\s\S]*?CannotCreateTypeConstrainedLanguage\s*)+$/, '');
|
||||
if (!text.trim()) text = '(no output)';
|
||||
}
|
||||
// Starting a program or opening a folder prints nothing when it worked. The model used to read "finished, nothing to show" as "cannot tell
|
||||
// whether it opened" and told the person so; a launch that returned without an error did start, and the result says that plainly.
|
||||
if ((tool === 'pwsh' || tool === 'bash') && /^\(no output\)$/.test(text.trim()) && LAUNCH.test(String(args?.command ?? ''))) return 'Started. The program, folder or file was opened.';
|
||||
// Starting a program or opening a folder prints nothing when it worked, and also when it did not (a browser given a file:// address, a
|
||||
// program that closed at once). T9 / #3576: the card used to say "was opened", and the model repeated it ("I previewed it in a browser to
|
||||
// confirm it renders"). It says what is known: the command returned without an error.
|
||||
if ((tool === 'pwsh' || tool === 'bash') && /^\(no output\)$/.test(text.trim()) && LAUNCH.test(String(args?.command ?? ''))) return LAUNCHED_TEXT;
|
||||
if ((tool === 'pwsh' || tool === 'bash') && /^\(no output\)$/.test(text.trim())) return 'The command finished without any error. It printed nothing, so there is nothing more to show.';
|
||||
return text;
|
||||
}
|
||||
@@ -196,8 +202,10 @@ export class TurnProjector {
|
||||
* no question ever reaches the person: the card goes straight from running to denied, without a moment where it
|
||||
* offers Approve and Deny buttons that would do nothing.
|
||||
*/
|
||||
constructor({ startedAt = Date.now(), policy = 'ask' } = {}) {
|
||||
constructor({ startedAt = Date.now(), policy = 'ask', window = 0 } = {}) {
|
||||
this.startedAt = startedAt;
|
||||
this.window = Number(window) || 0; // the model's window, for the figures in the line that says the chat was summarised
|
||||
this.summarizing = false;
|
||||
this.policy = policy;
|
||||
this.byPolicy = new Set(); // callIds the approval policy rejected without asking
|
||||
this.tools = new Map(); // callId -> { card, calledAt }
|
||||
@@ -303,6 +311,7 @@ export class TurnProjector {
|
||||
this.tokensIn += prompt;
|
||||
this.tokensOut += Number(c.usage.outputTokens) || 0;
|
||||
this.contextTokens = prompt + (Number(c.usage.outputTokens) || 0);
|
||||
out.push({ type: 'context', contextTokens: this.contextTokens, contextMax: this.window || undefined });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
@@ -371,6 +380,22 @@ export class TurnProjector {
|
||||
if (frame) out.push(frame);
|
||||
break;
|
||||
}
|
||||
case 'compaction/start':
|
||||
// The Harness is summarising the older messages to make room: say so while it does, and again (once) when it is done.
|
||||
this.summarizing = true;
|
||||
this.#status({ phase: 'compacting', code: 'compacting', text: FIT_TEXT.summarizing, key: FIT_TEXT.summarizing, values: [] });
|
||||
break;
|
||||
case 'compaction/end':
|
||||
if (!this.summarizing) break;
|
||||
this.summarizing = false;
|
||||
this.#afterTool();
|
||||
if (!d.error) {
|
||||
const before = this.window > 0 && this.contextTokens ? Math.min(100, Math.round(100 * this.contextTokens / this.window)) : null;
|
||||
const text = before === null ? FIT_TEXT.summarizedShort : FIT_TEXT.summarizedWas;
|
||||
const values = before === null ? [] : [before];
|
||||
out.push({ type: 'delta', own: true, text: '\n\n' + fill(text, values), key: text, values, noteReason: 'compacted' });
|
||||
}
|
||||
break;
|
||||
case 'turn/start':
|
||||
// The parent woke up again (a helper finished and reported back): the message goes on, as a new paragraph.
|
||||
this.ended = null;
|
||||
@@ -420,7 +445,7 @@ export class TurnProjector {
|
||||
case 'aborted':
|
||||
return [{ type: 'done', ...base, ...usage, finishReason: 'aborted', cancelled: true }];
|
||||
case 'error':
|
||||
return [{ type: 'error', message: friendlyFailure(reason.error), code: reason.error?.code ?? 'UNKNOWN' }];
|
||||
return [{ type: 'error', message: friendlyFailure(reason.error), code: isContextFailure(reason.error) ? 'CONTEXT_WINDOW_EXCEEDED' : reason.error?.code ?? 'UNKNOWN', ...(isContextFailure(reason.error) ? { key: FIT_TEXT.full, details: String(reason.error?.message ?? '') } : {}) }];
|
||||
case 'blocked':
|
||||
return [{ type: 'error', message: 'The agent stopped because this turn was blocked by policy.', code: 'BLOCKED' }];
|
||||
default:
|
||||
@@ -429,10 +454,15 @@ export class TurnProjector {
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a failure is the model server saying the conversation does not fit its window (its code, or the engine's own words). */
|
||||
export function isContextFailure(failure) {
|
||||
return isContextOverflow(failure) || /context window|too long for|exceeds the context/i.test(String(failure?.message ?? ''));
|
||||
}
|
||||
|
||||
/** A model-route failure in words a user can act on. */
|
||||
export function friendlyFailure(failure) {
|
||||
const message = String(failure?.message ?? 'The model request failed.');
|
||||
if (/context window|too long|exceeds/i.test(message)) return `The conversation no longer fits the model's context window. ${clip(message, 200)}`;
|
||||
if (isContextFailure(failure)) return FIT_TEXT.full;
|
||||
if (/ECONNREFUSED|fetch failed|connect/i.test(message)) return `The model is not reachable. ${clip(message, 200)}`;
|
||||
if (/401|403|credential|api key/i.test(message)) return 'The model rejected the credentials. Check the connection in Settings.';
|
||||
return clip(message, 400);
|
||||
|
||||
@@ -33,7 +33,7 @@ export function agentEnvironment(base = process.env, extra = {}) {
|
||||
}
|
||||
// DSH_PERMISSION_MODE is the composition default for a session no chat mode has touched (a child agent, a
|
||||
// seeded session): the strictest sandbox, which matches the Manual preset, never the Harness's wider default.
|
||||
return { ...out, DSH_TELEMETRY_DISABLED: '1', DSH_PERMISSION_MODE: 'read-only', ...extra };
|
||||
return { ...out, DSH_TELEMETRY_DISABLED: '1', DSH_PERMISSION_MODE: 'read-only', NODE_USE_SYSTEM_CA: '1', ...extra };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -117,7 +117,7 @@ export function stepObjective(plan, index) {
|
||||
`You are on step ${index + 1} of ${n}: ${s.text}${s.detail ? ` (${s.detail})` : ''}.`,
|
||||
done ? `Already done: ${done}.` : '',
|
||||
plan.briefing ? String(plan.briefing) : '',
|
||||
isViewStep(s.text) ? 'This step is about seeing the result in a browser. Never wait for a program to close. If a person is at this computer, start the browser so the command returns at once (Windows: Start-Process; macOS: open; Linux: xdg-open) and say it is open. If there is no screen, check the page without a visible window: use the headless browser tool if you have one, otherwise read the file and its script for mistakes, and say in one plain sentence what you could and could not check. Then finish the step.' : '',
|
||||
isViewStep(s.text) ? 'This step is about seeing the result. To show a local image, SVG, HTML page or PDF, use the present tool (a browser cannot open file:// addresses). Never wait for a program to close. If a person is at this computer and you start a browser, start it so the command returns at once (Windows: Start-Process; macOS: open; Linux: xdg-open) and say only that you started it, not that the page rendered. If there is no screen, check the page without a visible window: use the headless browser tool if you have one, otherwise read the file and its script for mistakes, and say in one plain sentence what you could and could not check. Then finish the step.' : '',
|
||||
'Do only this step, check that it worked, and then finish the goal.',
|
||||
index + 1 < n ? 'The next steps are handled separately: do not start them.' : '',
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Strata's context plugin for the Harness (plain ESM, loaded by absolute path from the profile, like strata-gate and strata-models).
|
||||
*
|
||||
* What it fixes (2026-10-22 hands-on test, Gyro-S at 64K):
|
||||
* 1. Compaction decided from the Harness's own price of the conversation, 4 characters a token. Code, logs and JSON are about twice as
|
||||
* dense, so a chat the engine had read as 59,048 tokens was priced at 31,794 and compaction started far too late. Here the token meter
|
||||
* is calibrated by what the model server reports for each answer (its real prompt size): the measured size is exact, and what was
|
||||
* added since is priced with the density that measurement showed. Automatic compaction and the manual /compact both measure through
|
||||
* the token meter, so both read the same number as the context meter in the app.
|
||||
* 2. The summarising request itself could be bigger than the window ("/compact" sent a 66,507-token prompt to a 65,536 window). The
|
||||
* summary is now asked for in pieces that fit: a message bigger than a piece is cut for the request (the conversation keeps it until
|
||||
* the summary replaces it), the pieces are summarised one after the other, each building on the summary before it, and a piece the
|
||||
* server still refuses is halved and tried again.
|
||||
*
|
||||
* How. It wraps `tokenMeter.measure` and `llm.stream` in place, once each (the same way strata-models wraps the subagent tools). Only a
|
||||
* request the Harness marks `purpose: 'compaction'` is touched in `llm.stream`. Nothing in the Harness is rewritten: a Harness update
|
||||
* that renames either leaves compaction as it was, and the test that runs the real Harness fails.
|
||||
*/
|
||||
import { densityOf, estimateMessage, estimateTools, isContextOverflow, MAX_DENSITY, pressureNow, promptTokensOf, splitMessages, summaryBudget, usageOfEvent, WINDOW_SLACK } from '../context-budget.mjs';
|
||||
|
||||
export const name = 'strata-context';
|
||||
export const inject = ['tokenMeter', 'llm'];
|
||||
|
||||
/** Real prompt measurements per session: { real, estimated } for the last answer the model server reported. */
|
||||
const samples = new WeakMap();
|
||||
const bySessionId = new Map();
|
||||
const wrapped = new WeakSet();
|
||||
/** Densities that made a summary request fail on this server, per session id (the next request starts from a more careful one). */
|
||||
const careful = new Map();
|
||||
|
||||
export function apply(ctx) {
|
||||
const meter = ctx.tokenMeter;
|
||||
const llm = ctx.llm;
|
||||
const log = (level, text) => { try { ctx.logger?.[level]?.(`strata-context: ${text}`); } catch { /* logging never blocks */ } };
|
||||
|
||||
// 1. The measured size of the prompt, from the model server's own report for each answer.
|
||||
if (meter && typeof meter.measure === 'function' && !wrapped.has(meter)) {
|
||||
wrapped.add(meter);
|
||||
const original = meter.measure;
|
||||
meter.measure = function measure(session, requestHeader) {
|
||||
const m = original.call(this, session, requestHeader);
|
||||
const sample = samples.get(session);
|
||||
if (!sample) return m;
|
||||
const now = m.surfaceTokens + estimateTools(requestHeader ?? session.requestHeader?.());
|
||||
const total = Math.max(m.totalTokens, pressureNow({ sample, now }));
|
||||
if (total <= m.totalTokens) return m;
|
||||
const density = densityOf(sample.real, sample.estimated);
|
||||
return { ...m, totalTokens: total, nodes: m.nodes.map(n => ({ ...n, tokens: Math.ceil(n.tokens * density) })) };
|
||||
};
|
||||
ctx.on('session/event', (session, event) => {
|
||||
if (event.type !== 'assistant/message') return;
|
||||
const real = promptTokensOf(usageOfEvent(event));
|
||||
if (!(real > 0)) return;
|
||||
try {
|
||||
// The request that was just answered saw the conversation without this answer: its price is the surface before the answer's node.
|
||||
const m = original.call(meter, session);
|
||||
const heuristic = m.nodes.reduce((n, x) => n + (x.seq === event.seq ? 0 : x.heuristicTokens), 0);
|
||||
const estimated = heuristic + estimateTools(session.requestHeader?.());
|
||||
if (!(estimated > 0)) return;
|
||||
const sample = { real, estimated };
|
||||
samples.set(session, sample);
|
||||
if (session.id) bySessionId.set(String(session.id), sample);
|
||||
} catch (e) { log('warn', `could not record a measurement: ${e.message}`); }
|
||||
});
|
||||
}
|
||||
|
||||
// 2. A summary request that fits the window.
|
||||
if (llm && typeof llm.stream === 'function' && !wrapped.has(llm)) {
|
||||
wrapped.add(llm);
|
||||
const original = llm.stream;
|
||||
llm.stream = function stream(options) {
|
||||
if (options?.purpose !== 'compaction') return original.call(this, options);
|
||||
return summaryStream.call(this, options, original);
|
||||
};
|
||||
}
|
||||
|
||||
// The host asks, with /strata-context, how much of the window the chat holds right now: the same measured number compaction decides on
|
||||
// (JSON in the command's text: { tokens, window }). Before a message goes out, and after /compact, so the person is told true figures.
|
||||
ctx.inject(['commands'], (commandCtx) => {
|
||||
commandCtx.commands.register({
|
||||
definitionId: 'strata-context',
|
||||
name: 'strata-context',
|
||||
description: 'Reports how many tokens the conversation holds now and the size of the model window',
|
||||
handler: async (invocation) => {
|
||||
try {
|
||||
const session = invocation.agent.session;
|
||||
const tokens = meter.measure(session).totalTokens;
|
||||
const config = session.requestHeader?.()?.config;
|
||||
let window = 0;
|
||||
if (config) window = (await llm.resolveModelInfo(config.provider, config.model))?.context?.contextWindow ?? 0;
|
||||
return { kind: 'success', text: JSON.stringify({ tokens, window }) };
|
||||
} catch (e) { return { kind: 'error', text: `strata-context: ${e.message}` }; }
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/** A finished stream: its chunks, the text it wrote and why it ended. */
|
||||
async function drain(iterable) {
|
||||
let text = '', finish = null;
|
||||
const chunks = [];
|
||||
for await (const chunk of iterable) {
|
||||
chunks.push(chunk);
|
||||
if (chunk.type === 'text-delta') text += chunk.text ?? '';
|
||||
else if (chunk.type === 'finish') finish = chunk.reason;
|
||||
}
|
||||
return { text, finish, chunks };
|
||||
}
|
||||
const refusedForSize = finish => finish?.kind === 'error' && isContextOverflow(finish.failure ?? finish);
|
||||
|
||||
async function* summaryStream(options, original) {
|
||||
let window = 0;
|
||||
try { window = (await llm.resolveModelInfo(options.provider, options.model, options.signal))?.context?.contextWindow ?? 0; } catch { window = 0; }
|
||||
const messages = options.messages ?? [];
|
||||
const key = String(options.sessionId ?? '');
|
||||
const summaryTokens = options.maxTokens ?? 4096;
|
||||
if (!(window > 0) || messages.length < 2) { yield* original.call(this, options); return; }
|
||||
const own = bySessionId.get(key);
|
||||
let density = Math.max(careful.get(key) ?? 1, own ? densityOf(own.real, own.estimated) : 1);
|
||||
const price = list => list.reduce((n, m) => n + estimateMessage(m), 0) * density;
|
||||
const limit = window - summaryTokens - WINDOW_SLACK;
|
||||
// The whole request fits by this price: ask as the Harness would (the model server's cache of the conversation is reused).
|
||||
if (price(messages) + estimateTools({ tools: options.tools }) * density <= limit) {
|
||||
const first = await drain(original.call(this, options));
|
||||
if (!refusedForSize(first.finish)) { for (const chunk of first.chunks) yield chunk; return; }
|
||||
density = Math.min(MAX_DENSITY, density * 1.6);
|
||||
careful.set(key, density);
|
||||
log('warn', 'the summary request did not fit the window; asking in pieces');
|
||||
}
|
||||
// In pieces. The instruction is the last message; the system prompt, when there is one, the first.
|
||||
const instruction = messages[messages.length - 1];
|
||||
const system = messages[0]?.role === 'system' ? messages[0] : null;
|
||||
const body = messages.slice(system ? 1 : 0, -1);
|
||||
let tools = options.tools;
|
||||
if (tools && estimateTools({ tools }) * density > window / 3) tools = undefined; // a long tool list only describes tools: the summary does not need it
|
||||
const fixed = (system ? estimateMessage(system) : 0) * density + estimateTools({ tools }) * density + estimateMessage(instruction) * density;
|
||||
let budget = summaryBudget({ window, summaryTokens: summaryTokens * 2, systemTokens: fixed, instructionTokens: 0 });
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
const runs = splitMessages(body, Math.max(1024, budget), density);
|
||||
log('info', `summarising ${body.length} messages in ${runs.length} piece(s) for a ${window}-token window (density ${density.toFixed(2)}, about ${Math.round(budget)} tokens a piece)`);
|
||||
let failed = null, summary = '';
|
||||
for (let i = 0; i < runs.length && !failed; i++) {
|
||||
options.signal?.throwIfAborted?.();
|
||||
const carried = summary ? [{ role: 'user', content: [{ type: 'text', text: `<compacted-summary>\n${summary}\n</compacted-summary>` }] }] : [];
|
||||
const { tools: _drop, ...rest } = options;
|
||||
const piece = { ...rest, ...(tools ? { tools } : {}), messages: [...(system ? [system] : []), ...carried, ...runs[i], instruction] };
|
||||
const { text, finish, chunks } = await drain(original.call(this, piece));
|
||||
if (refusedForSize(finish)) { failed = finish; break; }
|
||||
// The last piece's answer is the summary the Harness reads; a piece that ended in another error ends the request the same way.
|
||||
if (i === runs.length - 1 || finish?.kind === 'error' || finish?.kind === 'aborted') { for (const chunk of chunks) yield chunk; return; }
|
||||
summary = text;
|
||||
}
|
||||
if (!failed || attempt >= 3) throw Object.assign(new Error(failed?.failure?.message ?? 'the summary request does not fit the window'), { code: 'CONTEXT_WINDOW_EXCEEDED' });
|
||||
density = Math.min(MAX_DENSITY, density * 1.6);
|
||||
careful.set(key, density);
|
||||
budget = Math.max(1024, budget / 2);
|
||||
log('warn', `a piece did not fit; asking again with pieces of about ${Math.round(budget)} tokens`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,8 +68,37 @@ const SAFE_VERSION = /^(node|python|python3|py|npm|pnpm|git|dotnet|cargo|rustc|g
|
||||
/** Anything that can chain, redirect, expand or start something else is not read-only. */
|
||||
const UNSAFE_SYNTAX = /[;&<>`${}()]|\r|\n|--%|\bout-file\b|\bset-content\b|\badd-content\b|\binvoke-|\biex\b|\bstart-|\bnew-|\bremove-|\bcopy-|\bmove-|\brename-|\bclear-|\bset-|\bpush-|\bexpand-|\bsave-|\binstall-|\bcurl\b|\bwget\b|\bsudo\b|\bsh\b|\bbash\b|\bpwsh\b|\bpowershell\b|\bcmd\b|-exec\b|-delete\b|-execdir\b/i;
|
||||
|
||||
/**
|
||||
* The command with every quoted text replaced by a plain stand-in, or null when it cannot be read safely. What is inside quotes is data
|
||||
* (a path like "C:\Users\x\Q&A", a pattern like 'a|b'), so an & ; | ( ) < > or { in it must not decide anything. Two things still can:
|
||||
* inside double quotes a $ or a backtick expands (a variable, a sub-command), and an unterminated quote means the text was not understood.
|
||||
* Either one gives null, and the command asks as before.
|
||||
* @param {string} text
|
||||
* @returns {string|null}
|
||||
*/
|
||||
export function maskQuotes(text) {
|
||||
let out = '';
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i];
|
||||
if (c !== '"' && c !== "'") { out += c; continue; }
|
||||
let end = text.indexOf(c, i + 1);
|
||||
// PowerShell writes a quote inside a quoted text by doubling it ('it''s'): the text goes on past the pair.
|
||||
while (end >= 0 && text[end + 1] === c) end = text.indexOf(c, end + 2);
|
||||
if (end < 0) return null;
|
||||
if (c === '"' && /[$`]/.test(text.slice(i + 1, end))) return null;
|
||||
// A quoted text is a whole word. One glued to other characters (--out"put"=f, "a"b) or one that starts with a dash could hide an option from the
|
||||
// word checks below (git diff "--output=f" writes a file), so those are not understood either.
|
||||
const before = text[i - 1], after = text[end + 1];
|
||||
if ((before !== undefined && !/[\s|]/.test(before)) || (after !== undefined && !/[\s|]/.test(after)) || text[i + 1] === '-') return null;
|
||||
out += '"_"';
|
||||
i = end;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a shell command only reads. Conservative by design: one unrecognised word and the answer is no.
|
||||
* Quoted text is data and is looked at after it has been set aside (maskQuotes): Get-ChildItem "C:\Users\x\Q&A" only lists.
|
||||
* @param {string} command
|
||||
* @returns {boolean}
|
||||
*/
|
||||
@@ -77,8 +106,9 @@ export function isReadOnlyShell(command) {
|
||||
const text = String(command ?? '').trim();
|
||||
if (!text || text.length > 600) return false;
|
||||
if (SAFE_VERSION.test(text)) return true;
|
||||
if (UNSAFE_SYNTAX.test(text)) return false;
|
||||
const segments = text.split('|').map(s => s.trim());
|
||||
const masked = maskQuotes(text);
|
||||
if (masked === null || UNSAFE_SYNTAX.test(masked)) return false;
|
||||
const segments = masked.split('|').map(s => s.trim());
|
||||
return segments.every((segment) => {
|
||||
if (!segment) return false;
|
||||
const words = segment.split(/\s+/);
|
||||
@@ -219,8 +249,13 @@ export function apply(ctx, config = {}) {
|
||||
ctx.on('tools/pre-execute', async (exec, next) => {
|
||||
const agent = exec.agent;
|
||||
if (!agent || exec.name === 'run_code') return next();
|
||||
// Fail closed (T4): a session whose preset cannot be read (a reset in progress, a helper whose chat is gone) is treated as Manual, so
|
||||
// anything this file cannot prove is read-only still asks. It used to run unasked.
|
||||
let preset;
|
||||
try { preset = effectivePreset(agent.session); } catch { return next(); }
|
||||
try { preset = effectivePreset(agent.session); } catch (e) {
|
||||
preset = MANUAL_PRESET;
|
||||
try { console.warn(`[strata-gate] could not read the permission mode for this call (${e?.message ?? e}); treating it as Manual`); } catch { /* logging must never decide a permission */ }
|
||||
}
|
||||
// A helper that was set to ask while the chat was in Manual must stop asking the moment the chat leaves Manual,
|
||||
// so a mode change applies to the very next tool call of a running goal and its helpers.
|
||||
if (preset !== MANUAL_PRESET && agent.session.header?.origin === 'subagent' && ctx.approval.overrideOf(agent.session) === 'ask') {
|
||||
|
||||
@@ -42,8 +42,8 @@ export const AGENT_PROMPT = [
|
||||
'You are a helpful assistant that can also work on this computer through your tools. You are running as {{model}}.',
|
||||
'Answer questions directly and briefly; use a tool only when the task needs one. The person reads your replies in a chat: use plain words, not tool names or technical jargon.',
|
||||
'Look before you change: read a file before you edit or overwrite it, and prefer a small edit to a rewrite. Search with the glob and grep tools and read files with the read tool; use the shell for running programs.',
|
||||
'When you start a program or open a folder or file for the person, the command returning without an error means it worked: say plainly that it is open. Do not say you could not confirm it. If a command fails, say what went wrong in one sentence and what you will try next.',
|
||||
'To open a page or a file for the person (a browser, an app, a folder), start it so the command returns at once and never wait for the program to close: on Windows use Start-Process, on macOS open, on Linux xdg-open. Then say plainly that it is open.',
|
||||
'A command that starts a program, folder or page only tells you that it started. Never say a window opened, a page rendered or that you checked how it looks unless a tool confirmed it. If a command fails, say what went wrong in one sentence and what you will try next.',
|
||||
'To show the person an image, SVG, HTML page or PDF from their folders, use the present tool: the app shows it next to the chat. A browser cannot open file:// addresses. If you do start a program, start it so the command returns at once and never wait for it to close: Start-Process on Windows, open on macOS, xdg-open on Linux.',
|
||||
'When the person asks you to ask a subagent, a helper or another model ("ask a subagent", "ask model X"), use the subagent tool and name the model in its model field. Never look for a model with shell commands, and never wait for a helper with sleep: end your turn; its answer arrives as a new message.',
|
||||
'Tokens prefixed with @ are files or folders the person pointed at. A failed or refused action changed nothing; say so, and do not claim work you did not do.',
|
||||
].join('\n\n');
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { standardPreset } from './preset.mjs';
|
||||
import { validMcpName } from '../mcp-names.mjs';
|
||||
import { BUDGETS, isPrivateHost } from './llm-shape.mjs';
|
||||
import { DDG_PROVIDER_ID } from '../web-search-ddg.mjs';
|
||||
|
||||
@@ -55,6 +56,8 @@ export const modeOf = value => (Object.hasOwn(MODE_PRESETS, value) ? value : DEF
|
||||
/** The approval gate plugin (plain ESM, loaded by absolute path). */
|
||||
export const GATE_PLUGIN = fileURLToPath(new URL('./plugins/strata-gate.mjs', import.meta.url));
|
||||
/** The app's web search (DuckDuckGo, or the person's own Exa / Perplexity key), registered with the Harness's web seam. */
|
||||
/** Compaction that measures the real prompt and summarises in pieces that fit the window (plugins/strata-context.mjs). */
|
||||
export const CONTEXT_PLUGIN = fileURLToPath(new URL('./plugins/strata-context.mjs', import.meta.url));
|
||||
export const MODELS_PLUGIN = fileURLToPath(new URL('./plugins/strata-models.mjs', import.meta.url));
|
||||
export const SEARCH_PLUGIN = fileURLToPath(new URL('./plugins/strata-search.mjs', import.meta.url));
|
||||
/** The app's own tool server for the tools a person writes (instructions, commands). */
|
||||
@@ -71,11 +74,17 @@ const NO_TOOLS_SETTINGS = Object.freeze({ off: new Set(), search: { provider: 'b
|
||||
* `mcp__<server>__<tool>`. A command or URL goes into the patch as text; a secret (an env value or a header) never does: the row
|
||||
* carries `!!js process.env.<NAME>` and the value travels in the agent process's environment (returned in `env`).
|
||||
*/
|
||||
export function mcpRows(servers, env) {
|
||||
export function mcpRows(servers, env, skipped = []) {
|
||||
const rows = [];
|
||||
const ids = new Set();
|
||||
let index = 0;
|
||||
for (const [name, server] of Object.entries(servers ?? {})) {
|
||||
if (!server || server.disabled === true || !/^[A-Za-z0-9_-]{1,32}$/.test(name)) continue;
|
||||
if (!server || server.disabled === true) continue; // switched off on purpose: not an error
|
||||
// A server that cannot be wired is REPORTED (T6): the model never sees its tools, and a silent drop looked like "the tool does not work".
|
||||
if (!validMcpName(name)) { skipped.push({ name, why: 'name' }); continue; }
|
||||
if (!server.command && !server.url) { skipped.push({ name, why: 'address' }); continue; }
|
||||
if (ids.has(`mcp-${slug(name)}`)) { skipped.push({ name, why: 'duplicate' }); continue; }
|
||||
ids.add(`mcp-${slug(name)}`);
|
||||
const token = (kind, key, value) => {
|
||||
const variable = `STRATA_MCP_${index}_${kind}_${slug(key).replace(/-/g, '_').toUpperCase()}`;
|
||||
env[variable] = String(value);
|
||||
@@ -91,6 +100,24 @@ export function mcpRows(servers, env) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* What the chat says when a tool server was left out (one notice per reason). The sentences are interface copy: they are in every language's
|
||||
* catalog (src/locales/tools-<lang>.json), and the {0} is the list of names exactly as the person wrote them.
|
||||
*/
|
||||
const SKIPPED_TEXT = {
|
||||
name: 'These tool servers were left out because their names are not allowed: {0}. A name can have up to 32 letters, numbers, hyphens or underscores.',
|
||||
address: 'These tool servers were left out because they have no program or web address: {0}.',
|
||||
duplicate: 'These tool servers were left out because another one has almost the same name: {0}.',
|
||||
};
|
||||
export function mcpSkippedNotices(skipped) {
|
||||
const out = [];
|
||||
for (const why of Object.keys(SKIPPED_TEXT)) {
|
||||
const names = (skipped ?? []).filter(s => s.why === why).map(s => String(s.name).slice(0, 40));
|
||||
if (names.length) out.push({ code: `mcp-skipped-${why}-${names.join(',')}`.slice(0, 160), message: SKIPPED_TEXT[why].replace('{0}', names.join(', ')) });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const slug = value => String(value).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24) || 'x';
|
||||
|
||||
/** Identity of one route's wire configuration, secret included by hash only. */
|
||||
@@ -166,7 +193,7 @@ export function outputBudget(context, maxOutput = null) {
|
||||
/** What a local model, or an OpenAI-style server on this network, takes for the thinking level (see the comment where a local route is built). */
|
||||
const THINKING_COMPAT = { supportsStore: false, thinkingFormat: 'chat-template', chatTemplateKwargs: { enable_thinking: { $var: 'thinking.enabled' }, reasoning_effort: { $var: 'thinking.effort', omitWhenOff: true } } };
|
||||
|
||||
export function buildProfile({ local = [], connections = [], gate = true, tools = NO_TOOLS_SETTINGS, skillsDir = null, proxyBase = null, searchMock = null }) {
|
||||
export function buildProfile({ local = [], connections = [], gate = true, tools = NO_TOOLS_SETTINGS, skillsDir = null, proxyBase = null, searchMock = null, defaultLocalId = null }) {
|
||||
const providers = {};
|
||||
const env = {};
|
||||
const routes = [];
|
||||
@@ -202,6 +229,9 @@ export function buildProfile({ local = [], connections = [], gate = true, tools
|
||||
api: 'openai-completions',
|
||||
baseURL: inst.baseUrl,
|
||||
apiKeyEnv: keyEnv,
|
||||
// A long prompt read sends only SSE comments, which pi-ai's OpenAI client drops, so its 300 s stream-idle timer would cut the
|
||||
// request five minutes in (#3623). 60 minutes is a backstop: serve ends a hung engine and the bridge's watchdog ends a lost turn.
|
||||
streamIdleTimeoutMs: 60 * 60_000,
|
||||
// The Strata server reads the thinking level from `chat_template_kwargs` (enable_thinking, reasoning_effort) or from a top-level
|
||||
// reasoning_effort; a top-level `enable_thinking` (pi-ai's `qwen` format) is ignored, so the model went on thinking with the menu on Off.
|
||||
compat: THINKING_COMPAT,
|
||||
@@ -257,9 +287,13 @@ export function buildProfile({ local = [], connections = [], gate = true, tools
|
||||
for (const m of conn.models) routes.push({ route, fp, kind: 'api', connectionId: conn.id, connectionName: conn.name, modelId: m.id, name: m.name || m.id, contextWindow: m.contextMax ?? ASSUMED_API_CONTEXT, ...(shaped ? { shaped } : {}) });
|
||||
});
|
||||
|
||||
const defaultRoute = routes.find(r => r.kind === 'local') ?? null;
|
||||
// The model the chat really runs on is the default instance, not whichever one was loaded first (T5: with two models loaded, the tool servers of the
|
||||
// first one were wired to a chat on the second).
|
||||
const defaultInstance = local.find(i => i.id === defaultLocalId) ?? local[0] ?? null;
|
||||
const defaultRoute = routes.find(r => r.kind === 'local' && r.instanceId === defaultInstance?.id) ?? routes.find(r => r.kind === 'local') ?? null;
|
||||
// One Harness, one set of tool servers: the default local model's.
|
||||
const mcp = mcpRows({ ...(local[0]?.mcpServers ?? {}), ...(tools.servers ?? {}) }, mcpEnv);
|
||||
const mcpSkipped = [];
|
||||
const mcp = mcpRows({ ...(defaultInstance?.mcpServers ?? {}), ...(tools.servers ?? {}) }, mcpEnv, mcpSkipped);
|
||||
Object.assign(env, mcpEnv);
|
||||
// The person's own instruction and command tools come from the app's tool server; a command tool asks in Manual like any connected tool.
|
||||
const own = (kind, on) => on && tools.toolFile ? [{ id: `mcp-strata-${kind}`, name: '@deepseek-ai/dsh-mcp-client', config: { serverName: `strata-${kind}`, transport: 'stdio', command: process.execPath, args: [CUSTOM_TOOLS_SCRIPT, kind, tools.toolFile] } }] : [];
|
||||
@@ -286,6 +320,8 @@ export function buildProfile({ local = [], connections = [], gate = true, tools
|
||||
// Manual asks before a change runs, so the person sees one Approve/Deny card and nothing has happened yet.
|
||||
// Its web question names the search service the person chose (a keyed one only when its key is saved), and the person's switch skips the question.
|
||||
...(gate ? [{ insert: [{ id: 'strata-gate', name: GATE_PLUGIN, config: { web: { provider: SEARCH_NAMES[keyed ?? 'builtin'], skipAsk: tools.webWithoutAsking === true } } }] }] : []),
|
||||
// Compaction decides from what the model server measured, and its summary requests always fit the window (plugins/strata-context.mjs).
|
||||
{ insert: [{ id: 'strata-context', name: CONTEXT_PLUGIN, config: {} }] },
|
||||
// Helper agents on a model the person names: every model the app can reach is allowed, and the plugin makes the choice forgiving (plugins/strata-models.mjs).
|
||||
...(subagentRoutes.length ? [{ id: 'subagent-model-selection-settings', config: { enabled: true, allowedModels: subagentRoutes.map(r => ({ provider: r.provider, model: r.model })) } }] : []),
|
||||
...(subagentRoutes.length ? [{ insert: [{ id: 'strata-models', name: MODELS_PLUGIN, config: { routes: subagentRoutes } }] }] : []),
|
||||
@@ -309,7 +345,7 @@ export function buildProfile({ local = [], connections = [], gate = true, tools
|
||||
// What the agent is offered (tool groups, web search, tool servers, the person's own tools) decides whether a running agent must be
|
||||
// restarted for the next message; the routes are compared separately (a changed context size alone does not restart it).
|
||||
const toolsSignature = createHash('sha256').update(JSON.stringify([mcp, ownRows, webRows, entries.find(e => e.id === 'preset-standard'), env.STRATA_SEARCH_KEY ?? '', mcpEnv])).digest('hex').slice(0, 16);
|
||||
return { patch, env, routes, defaultRoute, signature, toolsSignature, shapeTargets };
|
||||
return { patch, env, routes, defaultRoute, signature, toolsSignature, shapeTargets, mcpSkipped };
|
||||
}
|
||||
|
||||
/** Find the route for a chat's model choice. */
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import { mkdirSync, renameSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { GROUP_IDS, TOOL_GROUPS } from './tools-catalog.mjs';
|
||||
import { SHELLS, clampTimeoutSec } from './custom-command-run.mjs';
|
||||
|
||||
export const SEARCH_PROVIDERS = ['builtin', 'exa', 'perplexity', 'deepseek'];
|
||||
const KINDS = ['instructions', 'command', 'server'];
|
||||
@@ -38,6 +39,8 @@ function normalize(raw) {
|
||||
id: String(t.id), name: String(t.name), about: clean(t.about, 300), kind: t.kind, enabled: t.enabled !== false,
|
||||
instructions: clean(t.instructions, 20000), command: clean(t.command, 2000), address: clean(t.address, 2000),
|
||||
args: (Array.isArray(t.args) ? t.args : []).slice(0, 50).map(a => clean(a, 1000)),
|
||||
// a command tool's folder, shell and time allowed (custom-command-run.mjs); '' / 'auto' / 120 when not set
|
||||
cwd: clean(t.cwd, 1000).trim(), shell: SHELLS.includes(t.shell) ? t.shell : 'auto', timeoutSec: clampTimeoutSec(t.timeoutSec),
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -49,7 +52,7 @@ function validateCustom(body, existing) {
|
||||
if (!about) throw new Error('Say what the tool does, in a sentence. The assistant reads this to decide when to use it.');
|
||||
const kind = body?.kind;
|
||||
if (!KINDS.includes(kind)) throw new Error('Choose how the tool works.');
|
||||
const out = { name, about, kind, instructions: '', command: '', address: '', args: [] };
|
||||
const out = { name, about, kind, instructions: '', command: '', address: '', args: [], cwd: '', shell: 'auto', timeoutSec: clampTimeoutSec(undefined) };
|
||||
if (kind === 'instructions') {
|
||||
out.instructions = clean(body.instructions, 20000).trim();
|
||||
if (!out.instructions) throw new Error('Write the instructions the assistant should follow when it uses this tool.');
|
||||
@@ -57,6 +60,14 @@ function validateCustom(body, existing) {
|
||||
out.command = clean(body.command, 2000).trim();
|
||||
if (!out.command) throw new Error('Enter the command to run.');
|
||||
if (/[\r\n]/.test(out.command)) throw new Error('Put the command on one line.');
|
||||
// The edit form of Settings does not send these three: an edit keeps what the tool had.
|
||||
const before = existing.find(t => t.id === body.id && t.kind === 'command');
|
||||
out.cwd = body.cwd !== undefined ? clean(body.cwd, 1000).trim() : before?.cwd ?? '';
|
||||
if (/[\r\n]/.test(out.cwd)) throw new Error('Put the folder on one line.');
|
||||
if (body.shell !== undefined && !SHELLS.includes(body.shell)) throw new Error('Choose how the command runs: automatic, cmd, PowerShell, bash or sh.');
|
||||
out.shell = body.shell !== undefined ? body.shell : before?.shell ?? 'auto';
|
||||
if (body.timeoutSec !== undefined && !(Number(body.timeoutSec) >= 1 && Number(body.timeoutSec) <= 3600)) throw new Error('Give the time allowed in seconds, from 1 to 3600.');
|
||||
out.timeoutSec = body.timeoutSec !== undefined ? clampTimeoutSec(body.timeoutSec) : before?.timeoutSec ?? clampTimeoutSec(undefined);
|
||||
} else {
|
||||
out.address = clean(body.address, 2000).trim();
|
||||
if (!out.address) throw new Error('Enter the tool server’s web address, or the program that starts it.');
|
||||
@@ -91,7 +102,7 @@ export function createAgentTools({ read, write, dataDir, browser = () => ({ foun
|
||||
for (const t of s.custom.filter(t => t.enabled)) {
|
||||
const key = t.id;
|
||||
if (t.kind === 'instructions') instructions[key] = { description: t.about, text: t.instructions };
|
||||
else if (t.kind === 'command') commands[key] = { description: t.about, command: t.command };
|
||||
else if (t.kind === 'command') commands[key] = { description: t.about, command: t.command, cwd: t.cwd, shell: t.shell, timeoutSec: t.timeoutSec };
|
||||
}
|
||||
mkdirSync(join(dataDir, 'agent'), { recursive: true });
|
||||
const tmp = `${filePath}.tmp`;
|
||||
@@ -115,6 +126,7 @@ export function createAgentTools({ read, write, dataDir, browser = () => ({ foun
|
||||
id: t.id, name: t.name, about: t.about, kind: t.kind, enabled: t.enabled,
|
||||
summary: t.kind === 'instructions' ? t.instructions.slice(0, 80) : t.kind === 'command' ? t.command : t.address,
|
||||
instructions: t.instructions, command: t.command, address: t.address, args: t.args,
|
||||
...(t.kind === 'command' ? { cwd: t.cwd, shell: t.shell, timeoutSec: t.timeoutSec } : {}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,13 +67,29 @@ export function nvidiaDriverVersion() {
|
||||
/** Where the builds are published; the wrong-make sentence points here. */
|
||||
export const RELEASES_URL = 'https://github.com/Niko1221/Strata/releases';
|
||||
|
||||
/** The PCI vendor ids (`10de`, `1002`, `8086`) of the Windows display adapters; empty off Windows or when the registry cannot be read. */
|
||||
export function windowsDisplayVendors({ platform = process.platform, run = execFileSync } = {}) {
|
||||
const found = new Set();
|
||||
if (platform !== 'win32') return found;
|
||||
try {
|
||||
const out = run('reg', ['query', 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}', '/s', '/v', 'MatchingDeviceId'], { encoding: 'utf8', timeout: 8000, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] });
|
||||
for (const match of String(out).matchAll(/VEN_([0-9A-F]{4})/gi)) found.add(match[1].toLowerCase());
|
||||
} catch { /* no registry access: the tools decide */ }
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* The engine family this PC's graphics card needs ('cuda', 'hip', 'sycl'), or null when it cannot be told. The same order as Control
|
||||
* (NVIDIA first, then AMD, then Intel); STRATA_ENGINE_BACKEND wins. Reads only the driver and (Linux) the PCI vendor of the display adapters.
|
||||
*/
|
||||
export function gpuBackendHere(env = process.env) {
|
||||
export function gpuBackendHere(env = process.env, { vendors = windowsDisplayVendors() } = {}) {
|
||||
const forced = String(env.STRATA_ENGINE_BACKEND ?? '').trim().toLowerCase();
|
||||
if (forced) return ['cuda', 'hip', 'sycl'].includes(forced) ? forced : null;
|
||||
// Windows: the display adapters are the machine's own statement of which cards it has; a leftover nvidia-smi from a card that was
|
||||
// taken out must not outvote them (#3688). Same order as Control: NVIDIA, then AMD, then Intel.
|
||||
if (vendors.size) {
|
||||
for (const [vendor, backend] of [['10de', 'cuda'], ['1002', 'hip'], ['8086', 'sycl']]) if (vendors.has(vendor)) return backend;
|
||||
}
|
||||
if (nvidiaDriverVersion()) return 'cuda';
|
||||
try {
|
||||
const base = '/sys/class/drm';
|
||||
@@ -218,3 +234,55 @@ export async function installBundledEngine({ bundleDir, components, platform = n
|
||||
return { action: 'refused', reason: error?.message ?? String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After an update from an installer that carried an engine (a per-GPU build), its `engine-bundle` folder is still in the install folder
|
||||
* (about 570 MB) although the engine was copied to the engine store long ago. It is removed only when EVERY package in it exists complete in
|
||||
* the store (every file there with the size and SHA-256 its manifest lists); otherwise it stays and one line says why.
|
||||
* Returns { removed: bool, reason?: string, packages?: number }; never throws.
|
||||
*/
|
||||
export async function removeVerifiedBundle({ bundleDir, components, log = () => {} } = {}) {
|
||||
try {
|
||||
if (!bundleDir || !components || !existsSync(bundleDir)) return { removed: false, reason: 'no bundle' };
|
||||
// Files inside a signed macOS app must stay intact after installation.
|
||||
if (process.platform === 'darwin' && resolve(bundleDir).split(sep).some(part => part.toLowerCase().endsWith('.app'))) return keepBundle(log, 'macOS app bundles must remain unchanged after signing');
|
||||
const packages = [];
|
||||
for (const entry of await readdir(bundleDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const folder = join(bundleDir, entry.name);
|
||||
const manifest = await readManifest(folder);
|
||||
packages.push({ folder, manifest, name: entry.name });
|
||||
}
|
||||
if (!packages.length) return { removed: false, reason: 'nothing in the bundle' };
|
||||
const store = join(components, 'engine');
|
||||
const stored = existsSync(store) ? (await readdir(store, { withFileTypes: true })).filter(e => e.isDirectory() && !e.name.startsWith('.')).map(e => join(store, e.name)) : [];
|
||||
for (const pkg of packages) {
|
||||
if (!pkg.manifest) return keepBundle(log, `${pkg.name} has no readable manifest`);
|
||||
let found = false;
|
||||
for (const candidate of stored) {
|
||||
const theirs = await readManifest(candidate);
|
||||
if (!theirs || theirs.version !== pkg.manifest.version || theirs.platform !== pkg.manifest.platform || theirs.backend !== pkg.manifest.backend
|
||||
|| (theirs.variant ?? '') !== (pkg.manifest.variant ?? '') || (theirs.build_id ?? '') !== (pkg.manifest.build_id ?? '')) continue;
|
||||
if (await packageIsWhole(candidate, pkg.manifest, { deep: true })) { found = true; break; }
|
||||
}
|
||||
if (!found) return keepBundle(log, `${pkg.name} is not (or not completely) in the engine store`);
|
||||
}
|
||||
for (const pkg of packages) await rm(pkg.folder, { recursive: true, force: true });
|
||||
log(`engine bundle: the copy that came with the installer (${packages.map(p => p.name).join(', ')}) was removed; the same engine is complete in the engine store`);
|
||||
return { removed: true, packages: packages.length };
|
||||
} catch (error) {
|
||||
return keepBundle(log, `could not be checked (${error?.message ?? error})`);
|
||||
}
|
||||
}
|
||||
function keepBundle(log, why) {
|
||||
log(`engine bundle: kept in the install folder (${why})`);
|
||||
return { removed: false, reason: why };
|
||||
}
|
||||
|
||||
/**
|
||||
* What the host does once its engine step has settled: for an engine that is (or was) there, look at the install folder's leftover bundle.
|
||||
* The lines go to the host's output (console.log), which the desktop shell copies to its log file (`[host] engine bundle: ...`).
|
||||
*/
|
||||
export function cleanupAfterInstall(settled, { bundleDir, components, log = line => console.log(line) } = {}) {
|
||||
return Promise.resolve(settled).then(result => (['installed', 'kept', 'repaired'].includes(result?.action) ? removeVerifiedBundle({ bundleDir, components, log }) : null), () => null);
|
||||
}
|
||||
@@ -183,6 +183,12 @@ export function createControl({ control = findControl(), env = process.env, run
|
||||
if (failed) throw Object.assign(new ControlError(failed.error, { exitCode: failed.exit_code ?? result.code, output: result.json }), { code: result.code === 7 ? 'engine_serving' : 'update_failed' });
|
||||
throw failure(result, 'The update failed.');
|
||||
},
|
||||
/** Install an engine package the person downloaded and chose (the offline path): no network; Control checks the package's own signature. */
|
||||
async installEngineFile(path, { onEvent = null, signal = null } = {}) {
|
||||
const result = await call(['components', 'install-file', path, '--output', 'jsonl'], { timeoutMs: 3600_000, onLine: onEvent ?? (() => {}), signal });
|
||||
if (result.json?.ok) return { ok: true, results: [result.json] };
|
||||
throw failure(result, 'This file could not be installed.');
|
||||
},
|
||||
async rollbackEngine() {
|
||||
const result = await call(['components', 'rollback', '--output', 'json'], { timeoutMs: 120_000 });
|
||||
if (result.json?.ok) return result.json;
|
||||
@@ -244,6 +250,22 @@ export function createControl({ control = findControl(), env = process.env, run
|
||||
if (result.json?.ok) return result.json;
|
||||
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 DFlash2 drafter for a model's run config and the drafting choice (`strata drafting plan`): { needed, gb, line, note, drafter }. Control decides
|
||||
* (the table of strata_control/drafting.py); the app never reads it. Nothing is downloaded.
|
||||
*/
|
||||
async draftingPlan({ configPath = null, mode = null, family = null, dataDir = null } = {}) {
|
||||
const result = await call(['drafting', 'plan', ...(configPath ? ['--config', configPath] : []), ...(mode ? ['--mode', mode] : []), ...(family ? ['--family', family] : []), ...(dataDir ? ['--data-dir', dataDir] : []), '--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, onEvent = null, signal = null } = {}) {
|
||||
const args = ['drafting', 'fetch', '--config', configPath, ...(mode ? ['--mode', mode] : []), '--if-needed', '--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' });
|
||||
},
|
||||
async saveProfile(name, configPath) {
|
||||
const listed = await call(['profile', 'list', '--output', 'json'], { timeoutMs: 20_000 });
|
||||
const known = (listed.json?.profiles ?? []).some(p => p.id === name);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** Physical topology and GPU sensors; unavailable values stay null. */
|
||||
import { readFile, readdir, realpath } from 'node:fs/promises';
|
||||
import { probe, round } from './util.mjs';
|
||||
import { windowsGpuReading } from './windows-gpu.mjs';
|
||||
|
||||
/** Unified GPUs share one system memory pool; dedicated GPUs each have their own. */
|
||||
export function gpuMemoryTotals(devices, readings) {
|
||||
@@ -83,6 +84,10 @@ export async function readGpuTelemetry(gpus) {
|
||||
return Promise.all(gpus.map(async gpu=>{
|
||||
let reading=nvidia.get(gpu.id)??{id:gpu.id??`${gpu.vendor}:${gpu.index}`,temperatureC:null,utilizationPct:null,memoryUsedGb:null,powerW:null,powerLimitW:null,pcieGen:null,pcieGenMax:null,pcieWidth:null,pcieRxMbs:null};
|
||||
if(gpu.vendor==='nvidia')reading={...reading,pcieRxMbs:transfer.get(gpu.sensorIndex??gpu.index)??null};
|
||||
if(gpu.luid&&!nvidia.has(gpu.id)) {
|
||||
const counters=windowsGpuReading(gpu.luid);
|
||||
if(counters)reading={...reading,utilizationPct:counters.utilizationPct,memoryUsedGb:round(counters.memoryUsedGb,2)};
|
||||
}
|
||||
if(gpu.sensorPath&&!nvidia.has(gpu.id)) {
|
||||
let temperature=null,power=null,powerLimit=null;
|
||||
for(const hwmon of await list(`${gpu.sensorPath}/hwmon`)) {
|
||||
|
||||
@@ -62,7 +62,10 @@ export function watchedWindowsDialog(shell, args, { readyMs = 30_000, openMs = 2
|
||||
const later = () => { clearTimeout(timer); ready = true; setTimeout(() => end(resolve, { stdout: '' }), openMs).unref?.(); };
|
||||
child.stdout?.on('data', chunk => { stdout += chunk; if (!ready && /^READY\s*$/m.test(stdout)) { clearTimeout(timer); later(); } });
|
||||
child.on('error', error => end(reject, error));
|
||||
child.on('close', code => { if (settled) return; settled = true; clearTimeout(timer); code === 0 ? resolve({ stdout }) : reject(Object.assign(new Error('exit ' + code), { code })); });
|
||||
child.on('close', code => { if (settled) return; settled = true; clearTimeout(timer);
|
||||
// PowerShell ended before its window opened (security software ended it, #3831): the person types the path instead.
|
||||
if (!ready) return resolve({ noDialog: true });
|
||||
code === 0 ? resolve({ stdout }) : reject(Object.assign(new Error('exit ' + code), { code })); });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { CONTEXT_TOKENS, VARIANTS, footprint } from './catalog.mjs';
|
||||
import { MODELS_DIR, diskFreeGb, diskTotalGb } from './store.mjs';
|
||||
import { probe, round } from './util.mjs';
|
||||
import { utf8Script } from './powershell-utf8.mjs';
|
||||
import { DX_ADAPTERS_SCRIPT, parseDxAdapters, windowsOtherGpus } from './windows-gpu.mjs';
|
||||
|
||||
let cached = null;
|
||||
let cachedAt = 0;
|
||||
@@ -187,11 +188,12 @@ async function detectGpus(platform, totalGb) {
|
||||
}
|
||||
if(platform==='win32') {
|
||||
const raw=await probe('powershell.exe',['-NoProfile','-NonInteractive','-Command',utf8Script('Get-CimInstance Win32_VideoController | Select-Object Name,PNPDeviceID | ConvertTo-Json -Compress')]);
|
||||
try {const parsed=JSON.parse(raw);for(const row of Array.isArray(parsed)?parsed:[parsed]) {
|
||||
if(!row?.Name||(/nvidia/i.test(row.Name)&&gpus.some(g=>g.vendor==='nvidia')))continue;
|
||||
const vendor=/amd|radeon/i.test(row.Name)?'amd':/intel/i.test(row.Name)?'intel':'unknown';
|
||||
gpus.push({id:String(row.PNPDeviceID),index:gpus.length,vendor,name:row.Name,vramGb:0,unified:false,backend:'vulkan'});
|
||||
}}catch{}
|
||||
try {
|
||||
const parsed=JSON.parse(raw);
|
||||
const rows=(Array.isArray(parsed)?parsed:[parsed]).filter(row=>row?.Name&&!(/nvidia/i.test(row.Name)&&gpus.some(g=>g.vendor==='nvidia')));
|
||||
const dx=rows.length?parseDxAdapters(await probe('powershell.exe',['-NoProfile','-NonInteractive','-Command',DX_ADAPTERS_SCRIPT],8000)):[];
|
||||
for(const card of windowsOtherGpus(rows,dx))gpus.push({...card,index:gpus.length,unified:false,backend:'vulkan'});
|
||||
}catch{}
|
||||
}
|
||||
gpus.forEach((g,i)=>{g.index=i;});
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { engineProgressMark } from './agent/engine-progress.mjs';
|
||||
import './system-ca.mjs'; // #3722: the operating system's certificates, before any HTTPS request
|
||||
import { createCloudflare } from './cloudflare.mjs';
|
||||
import { createConnectorInstaller } from './cloudflared-install.mjs';
|
||||
import { findRunningConnector } from './cloudflared-find.mjs';
|
||||
@@ -15,6 +17,7 @@ import { materializeSkills, parseSkillFile, skillSlug, writeCreatedSkill } from
|
||||
import { systemFonts } from './fonts.mjs';
|
||||
import { tuningDir } from './platform-paths.mjs';
|
||||
import { createAgentBridge, plainTurnFailure } from './agent/agent-bridge.mjs';
|
||||
import { mcpSkippedNotices } from './agent/profile.mjs';
|
||||
import { createAgentTools } from './agent/tools-settings.mjs';
|
||||
import { withPlainStatus } from './agent/status.mjs';
|
||||
import { findBrowser, findPlaywrightMcp } from './agent/browser-probe.mjs';
|
||||
@@ -29,7 +32,8 @@ import { findBrowser, findPlaywrightMcp } from './agent/browser-probe.mjs';
|
||||
*/
|
||||
import http from 'node:http';
|
||||
import { homedir } from 'node:os';
|
||||
import { cpSync, createReadStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { statfsSync, cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
@@ -55,9 +59,10 @@ import { RequestQueue } from './request-queue.mjs';
|
||||
import { selectSkills, skillInstructions } from './skill-selection.mjs';
|
||||
import { createModelProviders } from './model-providers.mjs';
|
||||
import { createForkTimeskip } from './timeskip-fork.mjs';
|
||||
import { prepareOversized } from './oversize.mjs';
|
||||
import { createChatFolders, chatRootOf, openFile, resolveChatFile, previewFile, rawFile, createAttachments, withAttachments } from './chat-files.mjs';
|
||||
import { createLlmProxy, samplingFromExpert } from './agent/llm-shape.mjs';
|
||||
import { checkUpdates, checkComponentUpdates } from './updates.mjs';
|
||||
import { checkUpdates, checkComponentUpdates, engineFirstRun } from './updates.mjs';
|
||||
import { plainNoModel } from './no-model-text.mjs';
|
||||
import { createAppUi } from './app-ui.mjs';
|
||||
import { appVersionFrom, appVersionToShow } from './app-version.mjs';
|
||||
@@ -65,7 +70,7 @@ import { createImageModels } from './image-models.mjs';
|
||||
import { createRuntimeModels } from './runtime-models.mjs';
|
||||
import { createNativeControl } from './native-control.mjs';
|
||||
import { nativeUnloadPlan } from './storage-check.mjs';
|
||||
import { installBundledEngine } from './bundled-engine.mjs';
|
||||
import { installBundledEngine, cleanupAfterInstall } from './bundled-engine.mjs';
|
||||
import { findControl, createControl, componentsDirectory } from './control-runtime.mjs';
|
||||
import { forbidden as acquisitionForbidden } from './acquisition.mjs';
|
||||
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
||||
@@ -126,6 +131,9 @@ const appUpdater = appUi.updater;
|
||||
const bundledEngine = control && !process.env.STRATA_NO_BUNDLED_ENGINE
|
||||
? installBundledEngine({ bundleDir: process.env.STRATA_ENGINE_BUNDLE_DIR || resolve(HERE, '..', '..', 'engine-bundle'), components: componentsDirectory(), log: line => console.log(line) })
|
||||
: Promise.resolve({ action: 'none' });
|
||||
const engineBundleDir = process.env.STRATA_ENGINE_BUNDLE_DIR || resolve(HERE, '..', '..', 'engine-bundle');
|
||||
/* An update from a per-GPU installer leaves its engine-bundle behind; it goes only when the same engine is complete in the store (sizes and hashes). */
|
||||
cleanupAfterInstall(bundledEngine, { bundleDir: engineBundleDir, components: componentsDirectory() });
|
||||
let bundledEngineSettled = !(control && !process.env.STRATA_NO_BUNDLED_ENGINE);
|
||||
bundledEngine.then(() => { bundledEngineSettled = true; }, () => { bundledEngineSettled = true; });
|
||||
/* A build made for another make of graphics card is not unpacked; Control's answer then carries the plain sentence (with the right download) instead of its own. */
|
||||
@@ -174,12 +182,15 @@ const agent = createAgentBridge({
|
||||
// The person's Response settings (thinking budget, Expert sampling) reach the agent's model through this host's own forwarding endpoint.
|
||||
proxyBase: `http://127.0.0.1:${PORT}/api/internal/agent-llm`,
|
||||
expertSampling: () => samplingFromExpert(store.get().expert),
|
||||
// How far a local engine has got (the 2 s /metrics poll), for the agent's stall watchdog: it moves while a long prompt is read.
|
||||
engineProgress: instanceId => engineProgressMark(native.readyInstances().find(i => i.id === instanceId)),
|
||||
onPlan: (chatId, event, plan) => hub.send('plan', { chatId, event, plan }),
|
||||
onQueue: (chatId, queue) => hub.send('queue', { chatId, queue }),
|
||||
dataDir: store.DATA_DIR,
|
||||
componentsDir: componentsDirectory(),
|
||||
// The installer ships the Harness next to the host (<install>/host/server -> <install>/harness).
|
||||
bundledDir: resolve(HERE, '..', '..', 'harness'),
|
||||
defaultLocalId: () => native.defaultInstanceId(),
|
||||
localInstances: () => native.readyInstances().map(i => ({
|
||||
id: i.id, label: i.label, baseUrl: `http://127.0.0.1:${i.port}/v1`, modelId: i.apiModelId,
|
||||
context: i.metrics?.engine?.max_context ?? i.context ?? undefined, key: i.key, vision: i.vision,
|
||||
@@ -601,8 +612,11 @@ async function runAgentTurn({ res, controller, chatId, body, message, workspaceI
|
||||
const goalLine = /^\/goal(?:\s+([\s\S]*))?$/i.exec(message.trim());
|
||||
const planLine = /^\/plan(?:\s+([\s\S]*))?$/i.exec(message.trim());
|
||||
const researchLine = /^\/research(?:\s+([\s\S]*))?$/i.exec(message.trim());
|
||||
// "/compact" summarises the older messages of this chat now (it used to go to the model as text, which answered it as a question).
|
||||
const compactLine = /^\/compact\s*$/i.test(message.trim());
|
||||
const turnOptions = { chatId, message, workspacePath: cwd, choice, images, mode: typeof body.mode === 'string' ? body.mode : null, thinking: typeof body.thinking === 'string' ? body.thinking : null, thinkingBudget: Number.isFinite(body.thinkingBudget) ? body.thinkingBudget : 0, history: Array.isArray(body.history) ? body.history : [], onFrame: write, signal: controller.signal };
|
||||
const result = typeof body.planId === 'string' && body.message === '/plan start' ? await agent.planGoal({ ...turnOptions, planId: body.planId })
|
||||
: compactLine ? await agent.compactCommand(turnOptions)
|
||||
: researchLine ? await agent.researchCommand({ ...turnOptions, line: researchLine[1] ?? '' })
|
||||
: planLine ? await agent.planCommand({ ...turnOptions, line: planLine[1] ?? '' })
|
||||
: goalLine ? await agent.goalCommand({ ...turnOptions, line: goalLine[1] ?? '' }) : await agent.runTurn(turnOptions);
|
||||
@@ -1012,11 +1026,11 @@ async function api(req, res, path, method, url) {
|
||||
if(nativeOperation&&method==='POST')return json(res,200,await native.cancel(nativeOperation[1]));
|
||||
const nativeInstance=/^\/api\/native\/instances\/([a-z0-9-]+)\/(unload|default)$/.exec(path);
|
||||
if(nativeInstance&&method==='POST')return json(res,200,nativeInstance[2]==='unload'?await native.unload(nativeInstance[1]):native.setDefault(nativeInstance[1]));
|
||||
const nativeSettings=/^\/api\/native\/models\/([a-z0-9_-]+)\/settings$/.exec(path);
|
||||
const nativeSettings=/^\/api\/native\/models\/([a-z0-9_.-]+)\/settings$/.exec(path);
|
||||
/* Expert mode shows what will actually run: the saved tuning's values come with the settings (null when none applies or unreadable). */
|
||||
if(nativeSettings&&method==='GET'){const settings=native.settings(nativeSettings[1]);let tuned=null;try{const t=await native.tuneEffective({model:nativeSettings[1],resourceProfile:url.searchParams.get('resourceProfile')||undefined,vision:url.searchParams.get('vision')||undefined});tuned=t.applied?t:null;}catch{tuned=null;}return json(res,200,{...settings,tuned});}
|
||||
if(nativeSettings&&method==='PATCH')return json(res,200,native.save(nativeSettings[1],await readBody(req)));
|
||||
const nativeSettingsFile=/^\/api\/native\/models\/([a-z0-9_-]+)\/settings-file$/.exec(path);
|
||||
const nativeSettingsFile=/^\/api\/native\/models\/([a-z0-9_.-]+)\/settings-file$/.exec(path);
|
||||
if(nativeSettingsFile&&method==='GET')return json(res,200,native.settingsFile(nativeSettingsFile[1]));
|
||||
if(nativeSettingsFile&&method==='POST')return json(res,200,await native.openSettingsFile(nativeSettingsFile[1]));
|
||||
|
||||
@@ -1317,6 +1331,27 @@ async function api(req, res, path, method, url) {
|
||||
}
|
||||
|
||||
if (path === '/api/setup' && method === 'GET') return json(res, 200, setupState());
|
||||
if (path === '/api/engine/first-run' && method === 'GET') {
|
||||
if (!control?.updateCheck) return json(res, 200, { status: 'unavailable', gpus: [] });
|
||||
const info = await engineFirstRun({ control, gpus: hardware.cachedHardware()?.gpus ?? [] });
|
||||
try { let at = componentsDirectory(); while (at && !existsSync(at) && dirname(at) !== at) at = dirname(at); const f = statfsSync(at); info.freeBytes = Number(f.bavail) * Number(f.bsize); } catch { /* unknown: no warning */ }
|
||||
return json(res, 200, info);
|
||||
}
|
||||
if (path === '/api/engine/job' && method === 'GET') {
|
||||
// The newest engine download / file install, read without waiting for the native snapshot to connect (the first-run card needs it before that).
|
||||
const kinds = ['update-engine', 'install-engine-file'];
|
||||
return json(res, 200, { job: native.state().jobs.filter(j => kinds.includes(j.kind)).slice(-1)[0] ?? null });
|
||||
}
|
||||
if (path === '/api/engine/from-file' && method === 'POST') {
|
||||
// The person chose an engine package in the page (the offline path). The bytes come here; Control checks and installs them.
|
||||
const name = String(req.headers['x-file-name'] ?? 'engine.tar.gz').replace(/[^A-Za-z0-9._-]/g, '_').slice(-120);
|
||||
if (!/\.(tar\.gz|tgz|tar\.xz|zip)$/i.test(name)) return error(res, 400, 'This is not a Strata engine package. Choose the .tar.gz or .zip file from the release page.');
|
||||
const folder = join(store.DATA_DIR, 'incoming-engine');
|
||||
mkdirSync(folder, { recursive: true, mode: 0o700 });
|
||||
const target = join(folder, name);
|
||||
await pipeline(req, createWriteStream(target, { mode: 0o600 }));
|
||||
return json(res, 202, native.operation('install-engine-file', { path: target }));
|
||||
}
|
||||
if (path === '/api/updates/check' && method === 'GET' && control?.updateCheck) return json(res, 200, await checkComponentUpdates({ control, appUpdater, appVersion: process.env.STRATA_APP_VERSION || null }));
|
||||
if (path === '/api/updates/check' && method === 'GET') return json(res, 200, await checkUpdates({ appVersion:appVersionToShow(setupState().app.version,native.state().version), engineVersion:native.state().available?(native.state().installed?.engine?.version??null):(await engine.config()).engine.version }));
|
||||
|
||||
@@ -1717,6 +1752,16 @@ async function api(req, res, path, method, url) {
|
||||
try { const f = rawFile(chatRoot(url.searchParams.get('sessionId') ?? ''), url.searchParams.get('path')); res.writeHead(200, { 'content-type': f.mime, 'content-length': f.bytes.length, 'cache-control': 'no-store', 'x-content-type-options': 'nosniff' }); res.end(f.bytes); return undefined; }
|
||||
catch (e) { return error(res, e.code === 'outside' ? 403 : 400, e.message); }
|
||||
}
|
||||
/* A message too large for the model's window, made small enough to send: its text as a file in the chat's folder, or a shortened preview (oversize.mjs). */
|
||||
if (path === '/api/chat/oversize' && method === 'POST') {
|
||||
const body = await readBody(req, 12 * 1024 * 1024);
|
||||
const text = String(body.text ?? '');
|
||||
if (!text.trim()) return error(res, 400, 'text is required');
|
||||
let instance = null;
|
||||
try { instance = native.resolveInstance(body.nativeModelId ?? native.state().defaultId); } catch { /* no local model: the length alone decides */ }
|
||||
try { return json(res, 200, await prepareOversized({ mode: body.mode === 'file' ? 'file' : 'preview', text, root: chatRoot(String(body.sessionId ?? '')), instance })); }
|
||||
catch (e) { return error(res, 400, 'The message could not be prepared. Try a shorter message.'); }
|
||||
}
|
||||
/* "Attach as context": the file is named in the next message of the chat, so the assistant reads it again. */
|
||||
if (path === '/api/chat/attachments' && method === 'GET') return json(res, 200, { files: chatAttachments.list(url.searchParams.get('sessionId') ?? '') });
|
||||
if (path === '/api/chat/attachments' && method === 'POST') {
|
||||
@@ -1934,6 +1979,8 @@ async function api(req, res, path, method, url) {
|
||||
if (chatId && !agentSkip) {
|
||||
const capability = agent.status();
|
||||
if (capability.state !== 'unavailable') {
|
||||
const skipped = mcpSkippedNotices(capability.skippedServers);
|
||||
for (const notice of skipped) res.write(`${JSON.stringify({ type: 'notice', ...notice })}\n`);
|
||||
await runAgentTurn({ res, controller, chatId, body, message, workspaceId, images });
|
||||
res.end();
|
||||
return undefined;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// The one rule for the name of a tool server (MCP), shared by the Settings editor (src/lib/mcp-names.ts keeps a copy; tests/mcp-rows.test.mjs
|
||||
// checks they agree), the save check (native-settings.mjs) and the agent profile (agent/profile.mjs).
|
||||
//
|
||||
// The model sees every tool as `mcp__<server>__<tool>`, and an OpenAI-style API takes at most 64 characters for that name, so the server's
|
||||
// part is limited to 32 (T6, tools and images review 2026-10-22). Before, the editor and the save check accepted 64 and the profile quietly
|
||||
// dropped everything over 32 (or with a dot): the model never saw the tool and nothing said why.
|
||||
export const MCP_NAME_MAX = 32;
|
||||
export const MCP_NAME_RE = /^[A-Za-z0-9_-]{1,32}$/;
|
||||
export const validMcpName = name => typeof name === 'string' && MCP_NAME_RE.test(name);
|
||||
export const MCP_NAME_HELP = 'Use a simple server name of up to 32 characters: letters, numbers, hyphens or underscores.';
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Which files of a model are missing, in words a person can act on (#3478). Control's model list says which required parts of the run
|
||||
* config are absent (`missing`: part + file name), and the installer's status says which GGUF files and shards it could not find
|
||||
* (`missing_files`, full paths). Together they give the names; the app shows them instead of only "some files are missing".
|
||||
*/
|
||||
import { basename } from 'node:path';
|
||||
|
||||
const PART_WORDS = { engine: 'the engine program', 'component:strata-engine': 'the engine program', 'tokenizer/vocab.json': 'the tokenizer', 'tokenizer/merges.txt': 'the tokenizer', 'tokenizer/token_type.json': 'the tokenizer' };
|
||||
|
||||
/** Short readable names, no duplicates, most useful first. */
|
||||
export function missingNames(controlParts, installerFiles) {
|
||||
const names = [];
|
||||
const add = text => { const t = String(text ?? '').trim(); if (t && !names.includes(t)) names.push(t); };
|
||||
for (const file of Array.isArray(installerFiles) ? installerFiles : []) { const name = basename(String(file).split(String.fromCharCode(92)).join('/')); add(PART_WORDS[name] ?? name); }
|
||||
for (const item of Array.isArray(controlParts) ? controlParts : []) {
|
||||
add(PART_WORDS[item?.part] ?? PART_WORDS[item?.file] ?? item?.file ?? item?.part);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/** The error a load or a tune raises for a model that is not whole. Names are in the sentence (at most four) and all of them in `details`. */
|
||||
export function missingFilesError(model, names) {
|
||||
const list = names ?? [];
|
||||
const shown = list.slice(0, 4).join(', ') + (list.length > 4 ? ` and ${list.length - 4} more` : '');
|
||||
const error = new Error(list.length
|
||||
? `Some model files are missing: ${shown}. Use Download / repair to restore them.`
|
||||
: 'Some model files are missing. Use Download / repair to restore them.');
|
||||
error.details = [list.length ? `Missing for ${model}: ${list.join(', ')}` : `Missing for ${model}: Strata could not tell which files`];
|
||||
return error;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
/** Native Strata control plane. All user actions are explicit; opening the UI only reads state. */
|
||||
import {missingFilesError,missingNames} from './missing-files.mjs';
|
||||
import { spawn } from 'node:child_process';
|
||||
import {chooseParallel,applyParallel,withoutParallel,FALLBACK_NOTE} from './native-parallel.mjs';
|
||||
import { randomBytes, randomUUID, createHash } from 'node:crypto';
|
||||
@@ -24,7 +25,7 @@ import { friendlyModelName } from './model-names.mjs';
|
||||
import { parseSaved as parseRoles, mirror as mirrorRoles } from './gpu-roles.mjs';
|
||||
|
||||
const ROOT=resolve(dirname(fileURLToPath(import.meta.url)),'..');
|
||||
const safeId=value=>typeof value==='string'&&/^[a-z0-9_-]{1,80}$/i.test(value);
|
||||
const safeId=value=>typeof value==='string'&&/^[a-z0-9](?:[a-z0-9_-]|\.(?!\.)){0,79}$/i.test(value); /* a dot inside the name: deepseek-v4.1-flash-ist-das-2.00bpw */
|
||||
const busy=requests=>requests.some(r=>['queued','generating'].includes(r.state));
|
||||
function readJson(path,fallback={}){try{return JSON.parse(readFileSync(path,'utf8').replace(/^\uFEFF/,''));}catch{return fallback;}}
|
||||
|
||||
@@ -37,7 +38,13 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
let child=null,seq=0,buffer='',pending=new Map(),definitions=[],refreshing=null,lastRead=0,connecting=null;
|
||||
let snapshot={available:false,repo:preferences.repo,summary:'Connect a local Strata installation.',catalog:null,installed:null,hardware:null,error:null};
|
||||
const instances=new Map(),processes=new Map(),jobs=new Map(),logs=[];
|
||||
const savedJobs=readJson(join(directory,'native-jobs.json'),[]);for(const j of (Array.isArray(savedJobs)?savedJobs:[]).slice(-12))jobs.set(j.id,{...j,...(j.state==='running'?{state:'error',error:'The host restarted before completion. Check diagnostics, then retry; completed files are kept.',summary:'Interrupted operation'}:{})});
|
||||
const savedJobs=readJson(join(directory,'native-jobs.json'),[]);/* An app update ends the app on purpose: the installer replaces it and starts it again. The job that asked for it was saved as running together with the app version it started from (appUpdateFrom); a start that finds a different version knows the update finished, instead of calling it an interrupted operation. */
|
||||
const thisAppVersion=()=>process.env.STRATA_APP_VERSION||null;
|
||||
/* A record an OLDER host wrote (no appUpdateFrom): the app's own program file was replaced after the job began, so the installer finished. <install>/host/server is ROOT's parent chain: ROOT = <install>/host. */
|
||||
const appProgramNewerThan=started=>{try{const t=Date.parse(started);if(!Number.isFinite(t))return false;const dir=dirname(ROOT),exe=join(dir,process.platform==='win32'?'strata-desktop.exe':'strata-desktop'),times=[];for(const f of [exe,join(dir,'uninstall.exe')])if(existsSync(f)){const st=statSync(f);times.push(st.mtimeMs,st.ctimeMs);}/* the installer keeps the build time as the program's modified time but writes uninstall.exe and changes the program file's metadata when it installs */return times.some(x=>x>t);}catch{return false;}};
|
||||
const settledByAppUpdate=j=>j.state==='running'&&j.kind==='update-strata'&&((typeof j.appUpdateFrom==='string'&&!!thisAppVersion()&&thisAppVersion()!==j.appUpdateFrom)||(typeof j.appUpdateFrom!=='string'&&appProgramNewerThan(j.startedAt)));
|
||||
const neverFinishedAppUpdate=j=>j.state==='running'&&j.kind==='update-strata'&&typeof j.appUpdateFrom==='string'&&!!thisAppVersion()&&thisAppVersion()===j.appUpdateFrom;
|
||||
for(const j of (Array.isArray(savedJobs)?savedJobs:[]).slice(-12))jobs.set(j.id,{...j,...(settledByAppUpdate(j)?{state:'done',error:null,summary:`Strata is updated to ${thisAppVersion()}.`,finishedAt:new Date().toISOString()}:neverFinishedAppUpdate(j)?{state:'error',error:`The update to ${j.appUpdateTo??'the new version'} did not finish: the app closed for the installer, but the old version ${j.appUpdateFrom} started again. Try Update again, or download the installer from the Strata release page (https://github.com/Niko1221/Strata/releases/latest).`,summary:'The app update did not finish',finishedAt:new Date().toISOString()}:j.state==='running'?{state:'error',error:'The host restarted before completion. Check diagnostics, then retry; completed files are kept.',summary:'Interrupted operation'}:{})});
|
||||
/* Packaged mode: the app ships Strata Control (control-runtime.mjs). Models are run configs in an install folder, the engine
|
||||
comes from the component folder, and a start is `strata runtime start` under the owner runtime. Nothing is installed,
|
||||
updated or fetched from here. Without `control` (a source checkout) every path below is the original one. */
|
||||
@@ -49,12 +56,18 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
The install only runs when the user clicks Download; starting and serving (control.start) keep STRATA_NO_ACQUIRE. */
|
||||
const defaultInstall=()=>process.env.STRATA_INSTALL_DIR?resolve(process.env.STRATA_INSTALL_DIR):join(dirname(componentsDirectory()),'install');
|
||||
let packagedInstall=null;
|
||||
/* The component updates and the rollback go through Control, not through the native snapshot, so they work in the first seconds after the app starts and on a PC that has no engine yet. */
|
||||
const COMPONENT_KINDS=['update-engine','update-strata','rollback','install-engine-file'];
|
||||
const updateLog=line=>{try{console.log(`update · ${line}`);}catch{/* no console */}};
|
||||
const NO_INSTALL='Installing, updating, tuning with calibration or rolling back model files and engines happens from the Terminal (strata install), never while the app runs.';
|
||||
let defaultId=null,timer=null,closing=false,polling=false;
|
||||
const emit=()=>{const value=state();writeFileSync(join(directory,'native-jobs.json'),JSON.stringify(value.jobs),{mode:0o600});changed(value);};
|
||||
const append=(instanceId,line)=>{logs.push({at:new Date().toISOString(),instanceId,line:String(line).slice(0,2000)});if(logs.length>400)logs.shift();};
|
||||
function diskSpace(path){try{const s=statfsSync(path);return {free:Number(s.bavail)*Number(s.bsize),total:Number(s.blocks)*Number(s.bsize)};}catch{return null;}}
|
||||
function state(){const folder=preferences.downloadDirectory??(modelsDirectory?modelsDirectory():snapshot.installed?.data_folder),space=folder?diskSpace(folder):null;return {...snapshot,downloadDirectory:folder,downloadFreeBytes:space?.free??null,downloadTotalBytes:space?.total??null,fields:nativeFields(),instances:[...instances.values()].map(({key,configPath,fingerprint,mcpServers,...i})=>structuredClone(i)),defaultId,jobs:[...jobs.values()].slice(-12).map(({process,abort,engineAbort,rawResult,...j})=>j),capabilities:{responses:sourceText().includes('/v1/responses'),updates:!!snapshot.available,rollback:previousEngine(),componentUpdates:packaged&&typeof control.update==='function',calibration:packaged?false:snapshot.hardware?.gpus?.some(g=>/nvidia/i.test(g.name??''))??false}};}
|
||||
/* Does the engine in this release serve DFlash2? Control knows (strata_control/drafting.py); asked once, false until it answers. The Models screen offers the choice only when true. */
|
||||
let dflash2Served=null;
|
||||
function probeDrafting(){if(!packaged||dflash2Served!==null||typeof control.draftingPlan!=='function')return;dflash2Served=false;control.draftingPlan({mode:'auto'}).then(p=>{dflash2Served=p.serves===true;emit();}).catch(()=>{});}
|
||||
function state(){const folder=preferences.downloadDirectory??(modelsDirectory?modelsDirectory():snapshot.installed?.data_folder),space=folder?diskSpace(folder):null;return {...snapshot,downloadDirectory:folder,downloadFreeBytes:space?.free??null,downloadTotalBytes:space?.total??null,fields:nativeFields(),instances:[...instances.values()].map(({key,configPath,fingerprint,mcpServers,...i})=>structuredClone(i)),defaultId,jobs:[...jobs.values()].slice(-12).map(({process,abort,engineAbort,rawResult,...j})=>j),capabilities:{responses:sourceText().includes('/v1/responses'),updates:!!snapshot.available,rollback:previousEngine(),componentUpdates:packaged&&typeof control.update==='function',calibration:packaged?false:snapshot.hardware?.gpus?.some(g=>/nvidia/i.test(g.name??''))??false,dflash2:dflash2Served===true}};}
|
||||
let sourceCache={path:'',time:0,text:''};
|
||||
function sourceText(){try{const path=join(packaged?control.control.dir:preferences.repo,'serve/server.py'),time=statSync(path).mtimeMs;if(sourceCache.path===path&&sourceCache.time===time)return sourceCache.text;sourceCache={path,time,text:readFileSync(path,'utf8')};return sourceCache.text;}catch{return '';}}
|
||||
function nativeFields(){const source=sourceText();return NATIVE_FIELDS.map(field=>({...field,supported:APP_OWNED_FIELDS.includes(field.id)||field.id.startsWith('vision_')||field.id.startsWith('mcp_')||field.id==='vram_reserve_mib'||source.includes(field.id)}));}
|
||||
@@ -67,13 +80,13 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
app is reinstalled to another folder, or an update moves Control, those absolute paths point at nothing and every installed
|
||||
model breaks. At every read of the installed models, a path that ends up inside a folder called `control` that no longer
|
||||
exists is pointed at the Control this app has now, when the file is there. The config is saved (with a .bak) only then. */
|
||||
function repairControlPaths(dir){if(!packaged||!dir||!existsSync(dir))return;let names=[];try{names=readdirSync(dir).filter(n=>/^strata-[a-z0-9_-]+\.json$/i.test(n));}catch{return;}
|
||||
function repairControlPaths(dir){if(!packaged||!dir||!existsSync(dir))return;let names=[];try{names=readdirSync(dir).filter(n=>/^strata-[a-z0-9_.-]+\.json$/i.test(n));}catch{return;}
|
||||
const here=control.control.dir,inside=/^(.+?[\/]control)(?=[\/]|$)(.*)$/i;
|
||||
const fix=value=>{if(typeof value!=='string')return value;const m=value.match(inside);if(!m||existsSync(value)||resolve(m[1]).toLowerCase()===resolve(here).toLowerCase())return value;const candidate=join(here,m[2]);return existsSync(candidate)?candidate:value;};
|
||||
for(const name of names){const path=join(dir,name),config=readJson(path,null);if(!config||typeof config!=='object')continue;
|
||||
const next={...config,...(config.cwd!==undefined?{cwd:fix(config.cwd)}:{}),...(Array.isArray(config.args)?{args:config.args.map(fix)}:{})};
|
||||
if(JSON.stringify(next)!==JSON.stringify(config)){try{writeModelConfig(path,next);}catch{/* read-only folder: the load reports it */}}}}
|
||||
function installedInfo(info){if(!info)return info;return {...info,models:(info.models??[]).map(entry=>{const cfg=/^strata-[a-z0-9_-]+\.json$/i.test(entry.config??'')?readJson(entry.configPath??join(repo(),entry.config)):{};const at=cfg.args?.indexOf('--native')??-1,nativeFile=at>=0?cfg.args[at+1]:null;const dataFolder=typeof nativeFile==='string'&&basename(dirname(dirname(nativeFile)))==='models'?dirname(dirname(dirname(nativeFile))):info.data_folder;/* A model the person pointed at their own GGUF folder (not <data>/models/<tag>): remembered, so a later load runs setup with that folder again instead of downloading the files (#3199). */const ownGgufDir=typeof nativeFile==='string'&&basename(dirname(dirname(nativeFile)))!=='models'&&existsSync(nativeFile)?dirname(nativeFile):undefined;return {...entry,ownGgufDir,diskGb:entry.diskGb??entry.disk_gb,diskBytes:entry.diskBytes??entry.disk_bytes,sharedBytes:entry.sharedBytes??entry.shared_bytes,removableGb:entry.removableGb??entry.removable_gb,dataFolder,visionPrepared:!!cfg.vision,/* The picture files are prepared when the run config has a vision section: the Models screen then loads straight away instead of running setup again (it used to read 'off' for every model, which sent every load of a picture-capable model through "Apply settings and load", and setup writes the arguments, the draft layer and the reader back each time). */images:cfg.vision?'yes':'off',draftLayer:cfg.mtp!==false,/* Picture reading is ON unless the person turned it off: no saved choice and not loaded is undefined, not false (owner rule: pictures on by default). */visionEnabled:cfg.ui_vision_enabled??(()=>{const running=[...instances.values()].find(i=>i.model===entry.model&&i.status==='ready');return running?!!running.vision:undefined;})(),visionDevice:cfg.ui_vision_device,resourceProfile:cfg.ui_resource_profile,lowRamMode:cfg.args?.includes('--resident-experts')?'resident':cfg.args?.includes('--mmap-experts')?'mmap':'auto'};})};}
|
||||
function installedInfo(info){if(!info)return info;return {...info,models:(info.models??[]).map(entry=>{const cfg=/^strata-[a-z0-9_.-]+\.json$/i.test(entry.config??'')?readJson(entry.configPath??join(repo(),entry.config)):{};const at=cfg.args?.indexOf('--native')??-1,nativeFile=at>=0?cfg.args[at+1]:null;const dataFolder=typeof nativeFile==='string'&&basename(dirname(dirname(nativeFile)))==='models'?dirname(dirname(dirname(nativeFile))):info.data_folder;/* A model the person pointed at their own GGUF folder (not <data>/models/<tag>): remembered, so a later load runs setup with that folder again instead of downloading the files (#3199). */const ownGgufDir=typeof nativeFile==='string'&&basename(dirname(dirname(nativeFile)))!=='models'&&existsSync(nativeFile)?dirname(nativeFile):undefined;return {...entry,ownGgufDir,diskGb:entry.diskGb??entry.disk_gb,diskBytes:entry.diskBytes??entry.disk_bytes,sharedBytes:entry.sharedBytes??entry.shared_bytes,removableGb:entry.removableGb??entry.removable_gb,dataFolder,visionPrepared:!!cfg.vision,/* The picture files are prepared when the run config has a vision section: the Models screen then loads straight away instead of running setup again (it used to read 'off' for every model, which sent every load of a picture-capable model through "Apply settings and load", and setup writes the arguments, the draft layer and the reader back each time). */images:cfg.vision?'yes':'off',draftLayer:cfg.mtp!==false,drafting:cfg.drafting??'auto',/* Picture reading is ON unless the person turned it off: no saved choice and not loaded is undefined, not false (owner rule: pictures on by default). */visionEnabled:cfg.ui_vision_enabled??(()=>{const running=[...instances.values()].find(i=>i.model===entry.model&&i.status==='ready');return running?!!running.vision:undefined;})(),visionDevice:cfg.ui_vision_device,resourceProfile:cfg.ui_resource_profile,lowRamMode:cfg.args?.includes('--resident-experts')?'resident':cfg.args?.includes('--mmap-experts')?'mmap':'auto'};})};}
|
||||
function installRoot(){const found=[preferences.installDir].find(d=>d&&existsSync(d));if(found)return realpathSync(found);mkdirSync(defaultInstall(),{recursive:true});return realpathSync(defaultInstall());}
|
||||
/* The settings file Strata Control and the installer read (control-runtime.mjs sets STRATA_SETTINGS_FILE to it): the app's data folder is written there. */
|
||||
function controlSettingsFile(){return process.env.STRATA_SETTINGS_FILE||join(dirname(componentsDirectory()),'app-settings.json');}
|
||||
@@ -106,8 +119,8 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
packagedInstall=list.install_dir;
|
||||
const engine=await control.engine();
|
||||
const installer=await mcpCall('strata_status',{hardware:true}).catch(error=>({error:error.message}));
|
||||
const disk=Object.fromEntries((installer.installed?.models??[]).map(m=>[m.model,{diskGb:m.disk_gb,diskBytes:m.disk_bytes,sharedBytes:m.shared_bytes,removableGb:m.removable_gb,partsMissing:m.parts_missing??0}]));
|
||||
const models=(list.models??[]).filter(m=>m.component_status!=='invalid'&&m.source_config).map(m=>({...(disk[m.id]??{}),model:m.id,config:basename(m.source_config),configPath:m.source_config,model_name:m.name,context:m.context_tokens??0,backend:m.backend,ready:m.component_status==='present'&&!(disk[m.id]?.partsMissing),images:'off',port:undefined,last_used:null}));
|
||||
const disk=Object.fromEntries((installer.installed?.models??[]).map(m=>[m.model,{diskGb:m.disk_gb,diskBytes:m.disk_bytes,sharedBytes:m.shared_bytes,removableGb:m.removable_gb,partsMissing:m.parts_missing??0,missingFiles:m.missing_files}]));
|
||||
const models=(list.models??[]).filter(m=>m.component_status!=='invalid'&&m.source_config).map(m=>({...(disk[m.id]??{}),model:m.id,config:basename(m.source_config),configPath:m.source_config,model_name:m.name,context:m.context_tokens??0,backend:m.backend,ready:m.component_status==='present'&&!(disk[m.id]?.partsMissing),missing:missingNames(m.missing,disk[m.id]?.missingFiles),images:'off',port:undefined,last_used:null}));
|
||||
const cores=os.cpus().length;
|
||||
return {summary:engine.usable?`Strata Control is ready; engine ${engine.engine.version} (${engine.engine.platform}-${engine.engine.backend}).`:(engine.notice??'No engine for this PC was found.'),
|
||||
installDir:list.install_dir,engine:{usable:engine.usable,notice:engine.notice,wrongMake:engine.wrongMake===true,version:engine.engine?.version??null,backend:engine.backend,componentsDir:engine.components_dir,previous:engine.previous??null,pinned:engine.pinned??null,updateNotice:engine.update_notice??null},
|
||||
@@ -125,21 +138,25 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
/* A read that is not forced never makes the window wait for the (slow) status of Control: it gets the last state at once, and a stale one
|
||||
is refreshed in the background. Only the first read, and a forced one after an operation, waits. */
|
||||
if(refreshing)return !force&&snapshot.available?state():refreshing;if(!force&&Date.now()-lastRead<45000)return state();
|
||||
probeDrafting();
|
||||
refreshing=(async()=>{try{const [catalog,status]=packaged?await (async()=>{const s=await tool('strata_status',{hardware:true});const c=await tool('strata_models').catch(error=>({families:[],contexts:[],recommendation:{family:null,model:null},context_note:'',error:error.message}));return [c,s];})():await Promise.all([tool('strata_models'),tool('strata_status',{hardware:true})]);let visionReader=false;if(packaged){try{const e=await control.engine();const root=e?.engine?.root??(e?.engine?.exe?dirname(e.engine.exe):'');visionReader=!!root&&['strata-vision','strata-vision.exe'].some(n=>existsSync(join(root,n)));}catch{visionReader=false;}}const markImages=c=>c&&Array.isArray(c.families)?{...c,families:c.families.map(f=>({...f,imagesCapable:!!f.images,images:!!f.images&&visionReader,sizes:(f.sizes??[]).map(s=>({...s,imagesCapable:!!s.images,images:!!s.images&&visionReader}))}))}:c;snapshot={available:true,repo:packaged?status.installDir:repo(),version:packaged?(control.version??null):await execute('git',['describe','--tags','--always'],repo()).then(s=>s.trim()).catch(()=>null),summary:status.summary,catalog:markEngine(packaged?markImages(catalog):catalog,status.engine),engineInfo:{version:status.engine?.version??null,source:status.engine?.source??null},visionReader:packaged?visionReader:true,installed:installedInfo(status.installed),hardware:status.hardware,error:null,install:status.install,external:status.running??null,...(packaged?{packaged:true,installDir:status.installDir,engine:status.engine,installerError:status.installerError}:{})};lastRead=Date.now();if(status.install?.running&&![...jobs.values()].some(j=>j.state==='running'))begin('install',j=>watchInstall(j,status.install));}catch(error){snapshot={...snapshot,available:false,error:error.message};lastRead=Date.now();}emit();return state();})().finally(()=>{refreshing=null;});const run=refreshing;if(!force&&snapshot.available){run.catch(()=>{});return state();}return run;
|
||||
}
|
||||
function configure({repository}){if(packaged){if(instances.size||[...jobs.values()].some(j=>j.state==='running'))throw new Error('Unload models and finish operations before changing the installation.');const next=realpathSync(String(repository));if(!statSync(next).isDirectory())throw new Error('Choose the folder that holds your Strata model configurations.');preferences={...preferences,installDir:next};packagedInstall=null;disconnect();writeFileSync(settingsPath,JSON.stringify(preferences,null,2),{mode:0o600});lastRead=0;return refresh(true);}if(instances.size||[...jobs.values()].some(j=>j.state==='running'))throw new Error('Unload models and finish operations before changing the installation.');const next=realpathSync(String(repository));const old=preferences;preferences={...preferences,repo:next};try{repo();}catch(e){preferences=old;throw e;}disconnect();writeFileSync(settingsPath,JSON.stringify(preferences,null,2),{mode:0o600});lastRead=0;return refresh(true);}
|
||||
function setDownloadDirectory(input,{insideJob=false}={}){if(!insideJob&&[...jobs.values()].some(j=>j.state==='running'))throw new Error('Finish the current operation before changing the folder.');if(typeof input!=='string'||!input.trim()||/[\r\n\0]/.test(input))throw new Error('Choose a model folder.');let path;try{path=realpathSync(input.trim());}catch{throw new Error('That folder does not exist. Check the path, or that the drive is connected.');}if(!statSync(path).isDirectory())throw new Error('Choose a folder, not a file.');/* A whole drive (D:\) is not filled with models: D:\Strata is made and used. */if(isDriveRoot(path)){const own=dataFolderFromPick(path).path;makeFolder(own);path=realpathSync(own);}try{accessSync(path,constants.R_OK|constants.W_OK);}catch{throw new Error('Strata cannot write in that folder. Choose a folder you can save files in.');}preferences={...preferences,downloadDirectory:path};/* the same choice for the installer and the Terminal: the Control settings file */if(packaged)rememberDataFolder(controlSettingsFile(),path,Date.now()/1000,true);writeFileSync(settingsPath,JSON.stringify(preferences,null,2),{mode:0o600});shareDataFolder();emit();return state();}
|
||||
function recordFile(model){if(!safeId(model))throw new Error('Choose an installed model.');const entry=snapshot.installed?.models?.find(i=>i.model===model);if(!entry)throw new Error('This model is not installed. Download and prepare it first.');const path=realpathSync(entry.configPath??join(repo(),entry.config));if(dirname(path)!==repo()||!/^strata-[a-z0-9_-]+\.json$/i.test(basename(path)))throw new Error('The model configuration is outside the Strata installation.');return {entry,path};}
|
||||
function recordFile(model){if(!safeId(model))throw new Error('Choose an installed model.');const entry=snapshot.installed?.models?.find(i=>i.model===model);if(!entry)throw new Error('This model is not installed. Download and prepare it first.');const path=realpathSync(entry.configPath??join(repo(),entry.config));if(dirname(path)!==repo()||!/^strata-[a-z0-9_.-]+\.json$/i.test(basename(path)))throw new Error('The model configuration is outside the Strata installation.');return {entry,path};}
|
||||
function record(model){const {entry,path}=recordFile(model);return {entry,path,config:readJson(path)};}
|
||||
/* The text file a power user edits by hand: where it is and what is wrong in it (run-config-check.mjs). */
|
||||
function settingsFile(model){const {path}=recordFile(model);return {model,path,name:basename(path),...checkRunConfig(readFileSync(path,'utf8')),note:'Changes you make in the file apply the next time the model starts.'};}
|
||||
async function openSettingsFile(model,options={}){const info=settingsFile(model);await openTextFile(info.path,options);return info;}
|
||||
/* A mistake in the file holds the start back with the line, instead of an engine that dies on it. Warnings (an unknown key) never do. */
|
||||
function assertSettingsFile(path){const check=checkRunConfig(readFileSync(path,'utf8'));if(!check.ok)throw new Error(`The settings file for this model has a mistake, so the model was not started. ${describeIssues(check.issues,'error').join(' ')}`);}
|
||||
function settings(model){const {config}=record(model);const values=Object.fromEntries(NATIVE_FIELDS.map(f=>[f.id,config[f.id]??null]));if(typeof values.aliases==='string')values.aliases=values.aliases.split(',').map(v=>v.trim()).filter(Boolean);values.sampling=config.sampling??{};values.args=config.args??[];values.env=publicNativeEnv(config.env??{});return {model,values,mcp:publicMcpServers(config.mcp_servers??config.mcpServers??{}),restartNeeded:true};}
|
||||
function save(model,{values,mcp}){if([...jobs.values()].some(j=>j.state==='running'))throw new Error('Finish the current operation before saving native settings.');const {path,config}=record(model);if(busy(requests().filter(r=>instances.get(r.instanceId)?.model===model)))throw new Error('Finish this model’s requests before saving its settings.');const patch=validateNativeSettings(values??{}),next={...config};for(const field of nativeFields())if(field.supported===false&&patch[field.id]!=null&&JSON.stringify(patch[field.id])!==JSON.stringify(config[field.id]))throw new Error(`${field.label} needs a newer native Strata installation. Update it in Settings → About.`);for(const [key,value] of Object.entries(patch)){if(key==='env')next.env=restoreNativeEnv(value,config.env??{});else if(value===null)delete next[key];else next[key]=value;}if('vram_reserve_mib' in patch){const base=captureUserArgs(next);const mine=setUserFlag(base,'--vram-reserve-mib',patch.vram_reserve_mib);if(mine.length)next[USER_KEY]=mine;else delete next[USER_KEY];next.args=mine.length&&patch.vram_reserve_mib!==null?mergeUserArgs(next.args??[],mine):setUserFlag(next.args??[],'--vram-reserve-mib',null);delete next.ui_reserve_applied;if(Array.isArray(next.args_written))next.args_written=[...next.args];}if(mcp!==undefined){next.mcp_servers=validateMcpServers(mcp,config.mcp_servers??config.mcpServers??{});delete next.mcpServers;}copyFileSync(path,path+'.bak');chmodSync(path+'.bak',0o600);const temp=path+'.ui-tmp';writeFileSync(temp,JSON.stringify(next,null,2)+'\n',{mode:0o600});renameSync(temp,path);return settings(model);}
|
||||
function settings(model){const {config}=record(model);const values=Object.fromEntries(NATIVE_FIELDS.map(f=>[f.id,config[f.id]??null]));if(typeof values.aliases==='string')values.aliases=values.aliases.split(',').map(v=>v.trim()).filter(Boolean);values.sampling=config.sampling??{};values.args=config.args??[];values.env=publicNativeEnv(config.env??{});return {model,values,mcp:publicMcpServers(config.mcp_servers??config.mcpServers??{}),restartNeeded:true,capabilities:{dsExperts:ownEngine(config)&&dsExpertsSupported(config)}};}
|
||||
function save(model,{values,mcp}){if([...jobs.values()].some(j=>j.state==='running'))throw new Error('Finish the current operation before saving native settings.');const {path,config}=record(model);if(busy(requests().filter(r=>instances.get(r.instanceId)?.model===model)))throw new Error('Finish this model’s requests before saving its settings.');const patch=validateNativeSettings(values??{}),next={...config};if(patch.ds_experts!=null&&!dsExpertsSupported(config))throw new Error('The Speed choice needs the newer DeepSeek engine. Update the engine in Settings, then choose it.');for(const field of nativeFields())if(field.supported===false&&patch[field.id]!=null&&JSON.stringify(patch[field.id])!==JSON.stringify(config[field.id]))throw new Error(`${field.label} needs a newer native Strata installation. Update it in Settings → About.`);for(const [key,value] of Object.entries(patch)){if(key==='env')next.env=restoreNativeEnv(value,config.env??{});else if(value===null)delete next[key];else next[key]=value;}if('vram_reserve_mib' in patch){const base=captureUserArgs(next);const mine=setUserFlag(base,'--vram-reserve-mib',patch.vram_reserve_mib);if(mine.length)next[USER_KEY]=mine;else delete next[USER_KEY];next.args=mine.length&&patch.vram_reserve_mib!==null?mergeUserArgs(next.args??[],mine):setUserFlag(next.args??[],'--vram-reserve-mib',null);delete next.ui_reserve_applied;if(Array.isArray(next.args_written))next.args_written=[...next.args];}if(mcp!==undefined){next.mcp_servers=validateMcpServers(mcp,config.mcp_servers??config.mcpServers??{});delete next.mcpServers;}copyFileSync(path,path+'.bak');chmodSync(path+'.bak',0o600);const temp=path+'.ui-tmp';writeFileSync(temp,JSON.stringify(next,null,2)+'\n',{mode:0o600});renameSync(temp,path);return settings(model);}
|
||||
function previousEngine(){if(packaged)return !!snapshot.engine?.previous;try{const data=snapshot.installed?.data_dir??snapshot.installed?.data_folder;return [join(preferences.repo,'engine/.previous'),data&&join(data,'engine/.previous')].filter(Boolean).some(existsSync)&&readFileSync(join(preferences.repo,'setup.py'),'utf8').includes('--rollback-engine');}catch{return false;}}
|
||||
async function plan(args){await refresh();if(!snapshot.available)throw new Error(snapshot.error);return tool('strata_install',installArgs(args,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;if(packaged&&typeof choice==='string'&&choice!=='auto'&&typeof control.draftingPlan==='function'){try{const d=await control.draftingPlan({mode:choice,family:args.family??null,dataDir:snapshot.installed?.data_folder??null});if(d.needed)return {...result,drafting:{needed:true,gb:d.gb,line:d.line}};}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;}
|
||||
function installArgs(args,confirm){const allowed=['family','model','context','vision','kv','gpu','gpus','backend','low_ram','port','gguf_dir'];const size=snapshot.catalog?.families?.find(f=>f.family===args.family)?.sizes.find(s=>s.model===args.model);const existing=snapshot.installed?.models?.find(m=>m.model===size?.id);/* A model adopted from the person's own GGUF folder reports that folder's parent as its data folder: it is theirs, not Strata's, so a second preparation never uses it for the packs. */if(args.gguf_dir===undefined&&existing?.ownGgufDir)args={...args,gguf_dir:existing.ownGgufDir};const data=nativeDataDirectory(ownFilesDataFolder(args.data_dir??(args.gguf_dir?undefined:existing?.dataFolder)??preferences.downloadDirectory??modelsDirectory?.(),args));if(data&&packaged&&!args.data_dir)rememberDataFolder(controlSettingsFile(),data);return {...(data?{data_dir:data}:{}),...Object.fromEntries(allowed.filter(k=>args[k]!==undefined).map(k=>[k,args[k]])),/* One card is "gpu", never "gpus" (the installer wants two or more there: "Gpus='0' is not valid", #3203). */...(/^\d{1,2}$/.test(String(args.gpus??''))?{gpu:Number(args.gpus),gpus:undefined}:{}),/* Picture reading is only downloaded when it was asked for (Read pictures); the default stays the smaller download. */...(packaged&&args.vision===undefined?{vision:'no'}:{}),confirm};}
|
||||
@@ -176,7 +193,7 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
const note=engineStep?.summaryNote();
|
||||
const finalResult=engineStep?job.result:result;
|
||||
if(note)return {...finalResult,summary:note};
|
||||
if(args.loadAfter){const model=(result.install.family??args.family??'qwen')==='qwen'?String(args.model).toLowerCase():`${args.family}-${String(args.model).toLowerCase()}`;return loadEngine(job,{model,multiple:!!args.multiple,resourceProfile:args.resourceProfile,vision:args.loadVision});}
|
||||
if(args.loadAfter){const model=(result.install.family??args.family??'qwen')==='qwen'?String(args.model).toLowerCase():`${args.family}-${String(args.model).toLowerCase()}`;return loadEngine(job,{model,multiple:!!args.multiple,resourceProfile:args.resourceProfile,vision:args.loadVision,...(typeof args.drafting==='string'?{values:{drafting:args.drafting}}:{})});}
|
||||
return finalResult;
|
||||
}
|
||||
}
|
||||
@@ -199,6 +216,12 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
if(still.length)throw new Error(`Some files could not be removed: ${still.slice(0,3).map(p=>basename(p)).join('; ')}`);
|
||||
return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
/* The configuration a load starts the engine with (before its port and key): the saved model config, the user's choices and the resource profile. The tuner measures exactly this, so a saved tuning belongs to the same choices a load makes. */
|
||||
/* DeepSeek-V4.1-Flash runs on its own engine (strata-dsv41) that takes none of the Flash-Next options the app adds on its own (request slots, the RAM budget, the output clamp, the resource profile): a flag it does not know ends the start with "unknown option". */
|
||||
const ownEngine=config=>config?.arch==='deepseek41'||/strata-dsv41/i.test(String(config?.exe??''));
|
||||
/* The DeepSeek engine's version, read from the folder the run config names (engine-dsv41/<version>-<platform>-...). --experts exists from engine 0.1.50: an older engine ends the start with "unknown option", so the choice is offered and passed only for 0.1.50 or newer. */
|
||||
const DS_EXPERTS_MIN=[0,1,50];
|
||||
function dsEngineVersion(config){const m=/engine-dsv41[\\/](\d+(?:\.\d+)+)-/.exec(String(config?.exe??''))||/engine-dsv41[\\/](\d+(?:\.\d+)+)-/.exec(String((config?.lib_dirs??[])[0]??''));return m?m[1].split('.').map(Number):null;}
|
||||
function dsExpertsSupported(config){const v=dsEngineVersion(config);if(!v)return false;for(let i=0;i<DS_EXPERTS_MIN.length;i++){const a=v[i]??0,b=DS_EXPERTS_MIN[i];if(a!==b)return a>b;}return true;}
|
||||
function buildConfig(args,config){
|
||||
const vision=runtimeVision(config,args.vision);
|
||||
const visionPreferences=args.vision!==undefined?{ui_vision_enabled:args.vision!=='no',ui_vision_device:args.vision==='no'?config.ui_vision_device??'yes':args.vision}:{};
|
||||
@@ -207,7 +230,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
const values0=args.values??{},cleared='vram_reserve_mib' in values0&&(values0.vram_reserve_mib===null||values0.vram_reserve_mib===''),base0=cleared?{...config,vram_reserve_mib:undefined,[USER_KEY]:setUserFlag(config[USER_KEY],'--vram-reserve-mib',null)}:config;
|
||||
let mine=captureUserArgs(base0);const asked=cleared?null:reserveChoice({...base0,[USER_KEY]:mine},snapshot.hardware,values0).asked;
|
||||
if(asked!==null)mine=setUserFlag(mine,'--vram-reserve-mib',asked);
|
||||
const resources=args.resourceProfile?resourceSettings({...base0,[USER_KEY]:mine,vision},snapshot.hardware,args.resourceProfile,modelArena(args.model),values0):null;
|
||||
const resources=args.resourceProfile&&!ownEngine(config)?resourceSettings({...base0,[USER_KEY]:mine,vision},snapshot.hardware,args.resourceProfile,modelArena(args.model),values0):null;
|
||||
const cfg={...config,...visionPreferences,...(resources?{...resources,ui_resource_profile:args.resourceProfile}:{}),host:'127.0.0.1',open_browser:false,...validateNativeSettings(args.values??{}),vision};
|
||||
/* The app's suggested cards only fill a gap: a model whose settings file already names its GPUs ("gpu": [1, 0], with "gpu_order": "as_given") keeps them (#3256). A card the person picked in the screen always wins. */
|
||||
const namedInFile=(Array.isArray(config.gpu)?config.gpu.length>0:config.gpu!==undefined&&config.gpu!==null&&config.gpu!=='')||!!config.gpu_roles;
|
||||
@@ -218,23 +241,46 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
if(mine.length){cfg.args=mergeUserArgs(cfg.args??[],mine);cfg[USER_KEY]=mine;}else delete cfg[USER_KEY];
|
||||
if(cfg.vision){if(cfg.vision_tokens!=null)cfg.vision={...cfg.vision,max_tokens:cfg.vision_tokens};if(cfg.vision_min_tokens!=null)cfg.vision={...cfg.vision,min_tokens:cfg.vision_min_tokens};}cfg.mcp={...(cfg.mcp??{}),...(cfg.mcp_timeout_s!=null?{timeout_s:cfg.mcp_timeout_s}:{}),...(cfg.mcp_max_rounds!=null?{max_rounds:cfg.mcp_max_rounds}:{}),...(cfg.mcp_max_result_chars!=null?{max_result_chars:cfg.mcp_max_result_chars}:{})};
|
||||
/* The model's own choices win over what setup wrote into the arguments: "Draft layer (MTP)" off leaves --mtp out of every start, and a picture reading that is off leaves --vision out (setup writes both again whenever it runs; the saved settings are applied here, on every load). */
|
||||
if(cfg.mtp===false&&Array.isArray(cfg.args))cfg.args=withoutDraftLayer(cfg.args);
|
||||
if((cfg.mtp===false||cfg.drafting==='off')&&Array.isArray(cfg.args))cfg.args=withoutDraftLayer(cfg.args);
|
||||
if(!cfg.vision&&Array.isArray(cfg.args))cfg.args=cfg.args.filter(a=>a!=='--vision');
|
||||
for(const arg of cfg.args??[])if(arg.endsWith('.gguf')&&!existsSync(resolve(repo(),arg)))throw new Error('An engine argument names a missing GGUF. Restore the saved arguments or repair the files.');
|
||||
/* The app's own choice of request slots (4, fewer where the card has no room) is part of the configuration, so a tune measures the engine a load will start: slots take VRAM from the expert cache. */
|
||||
const parallelChoice=packaged?chooseParallel({config:cfg,values:args.values??{},hardware:snapshot.hardware?.gpus?.length?snapshot.hardware:{gpus:hardwareProbe()??[]}}):null;
|
||||
const parallelChoice=packaged&&!ownEngine(config)?chooseParallel({config:cfg,values:args.values??{},hardware:snapshot.hardware?.gpus?.length?snapshot.hardware:{gpus:hardwareProbe()??[]}}):null;
|
||||
let parallelNote=null;
|
||||
/* The agent asks for long answers (a thinking model can use most of the window): let the engine clamp the output to what is left of the context instead of refusing the request. */
|
||||
if(packaged&&cfg.fit_max_tokens===undefined)cfg.fit_max_tokens=true;
|
||||
if(packaged&&!ownEngine(config)&&cfg.fit_max_tokens===undefined)cfg.fit_max_tokens=true;
|
||||
if(parallelChoice){const applied=applyParallel(cfg,parallelChoice);for(const k of Object.keys(cfg))if(!(k in applied.cfg))delete cfg[k];Object.assign(cfg,applied.cfg);parallelNote=applied.note;}
|
||||
/* DeepSeek's Speed choice: the engine decides by itself (--experts auto: 5 when the cards total 16 GB or less, else 6), so nothing is passed unless the person chose 5 or 6. */
|
||||
if(ownEngine(config)){const list=[];const old=Array.isArray(cfg.args)?cfg.args:[];for(let i=0;i<old.length;i++){if(old[i]==='--experts'){i++;continue;}list.push(old[i]);}if(dsExpertsSupported(config)&&(cfg.ds_experts==='5'||cfg.ds_experts==='6'))list.push('--experts',cfg.ds_experts);else if(!dsExpertsSupported(config)&&old.includes('--experts'))list.splice(0,list.length,...old);cfg.args=list;delete cfg.ds_experts;}
|
||||
else delete cfg.ds_experts;
|
||||
return {cfg,resources,visionPreferences,parallelChoice,parallelNote};
|
||||
}
|
||||
/* The DFlash2 drafter, before a load that uses it: Control says whether the choice needs it (and the file is not here), downloads it pinned and checked, and packs it, with plain progress in the job.
|
||||
Never stops the load: a drafter that cannot be had leaves the MTP running, and the job says so. The choice itself is remembered in the model's settings file. */
|
||||
async function prepareDrafter(job,args,cfg,configFile){
|
||||
const asked=args.values?.drafting;
|
||||
if(asked!==undefined)rememberSetting(configFile,'drafting',asked===null||asked==='auto'?null:asked);
|
||||
if(!packaged||typeof control.draftingFetch!=='function'||!cfg.drafting||cfg.drafting==='auto'||cfg.drafting==='mtp'||cfg.drafting==='off')return;
|
||||
const work=join(directory,'native-instances');mkdirSync(work,{recursive:true});const probe=join(work,'drafting-'+randomUUID()+'.json');
|
||||
try{
|
||||
writeFileSync(probe,JSON.stringify(cfg),{mode:0o600});
|
||||
job.engineAbort??=new AbortController();
|
||||
await control.draftingFetch({configPath:probe,mode:cfg.drafting,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'){job.summary='Loading the model…';job.details=undefined;emit();}
|
||||
}catch(error){if(job.state==='running'){job.drafterNote=error.message;job.summary='The DFlash2 drafter could not be downloaded, so the model starts with the MTP. Loading the model…';emit();}}
|
||||
finally{try{rmSync(probe,{force:true});}catch{/* best effort */}}
|
||||
}
|
||||
/* One key of a model's settings file, set or removed (the Drafting choice): written like the Settings screen writes it, to be there for the next load. */
|
||||
function rememberSetting(configFile,key,value){try{const current=readJson(configFile);if(value===null?!(key in current):current[key]===value)return;const next={...current};if(value===null)delete next[key];else next[key]=value;const temp=configFile+'.ui-tmp';writeFileSync(temp,JSON.stringify(next,null,2)+'\n',{mode:0o600});renameSync(temp,configFile);}catch{/* the load goes on with the choice for this start */}}
|
||||
/* Which drafter runs, when it is not the one that was asked for (the engine's own words, "Drafting: MTP (DFlash2 didn't fit)"): under Details of the load. */
|
||||
async function noteDrafting(job,instance,key){let note='';try{const r=await fetcher(`http://127.0.0.1:${instance.port}/metrics`,{headers:{authorization:`Bearer ${key}`},signal:AbortSignal.timeout(2000)});if(r.ok)note=String((await r.json())?.engine?.drafting_note??'');}catch{/* a convenience */}const lines=[job.drafterNote,note].filter(Boolean);if(lines.length)job.details=[job.details,...lines].filter(Boolean).join('\n');}
|
||||
async function loadEngine(job,args){
|
||||
await refresh(true);if(job.state!=='running')return;const {config,entry,path:configFile}=record(args.model);assertSettingsFile(configFile);if(packaged){const engine=await control.engine();if(!engine.usable)throw new Error(ENGINE_NOT_RECOGNISED+(engine.notice?` ${engine.notice}`:''));}if(!entry.ready)throw new Error('Some model files are missing. Use Download / repair to restore them.');
|
||||
await refresh(true);if(job.state!=='running')return;const {config,entry,path:configFile}=record(args.model);assertSettingsFile(configFile);if(packaged){const engine=await control.engine();if(!engine.usable)throw new Error(ENGINE_NOT_RECOGNISED+(engine.notice?` ${engine.notice}`:''));}if(!entry.ready){const error=missingFilesError(args.model,entry.missing);append(args.model,error.details[0]);throw error;}
|
||||
if(externalRunning().length)throw new Error('A Strata server is already running outside this UI. Stop it before loading here, or connect its API in Settings → Models.');
|
||||
if(args.multiple&&instances.size>=8)throw new Error('Unload an instance before adding another. Up to eight native instances can run here.');
|
||||
const {cfg,resources,visionPreferences,parallelChoice,parallelNote:chosenNote}=buildConfig(args,config);
|
||||
let parallelNote=chosenNote;
|
||||
await prepareDrafter(job,args,cfg,configFile);if(job.state!=='running')return;
|
||||
const fingerprint=createHash('sha256').update(JSON.stringify({...cfg,api_key:undefined,port:undefined})).digest('hex');
|
||||
const placement=Array.isArray(cfg.gpu)?cfg.gpu:cfg.gpu===undefined?[]:[cfg.gpu];const matching=[...instances.values()].find(i=>i.model===args.model&&JSON.stringify(i.gpus)===JSON.stringify(placement));
|
||||
if(matching?.status==='ready'&&matching.fingerprint===fingerprint&&(args.multiple||instances.size===1))return {summary:`${matching.label} is already ready.`,instanceId:matching.id};
|
||||
@@ -258,7 +304,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
/* The engine starts under Control's owner runtime, not as our child: a saved profile per slot, then `runtime start`. */
|
||||
const used=new Set([...instances.values()].map(i=>i.profile));let slot=1;while(used.has('app-'+slot))slot++;instance.profile='app-'+slot;
|
||||
instances.set(id,instance);if(!defaultId||!args.multiple||wasDefault)defaultId=id;emit();
|
||||
const startWith=value=>{writeFileSync(configPath,JSON.stringify(value,null,2),{mode:0o600});return control.stop(instance.profile).catch(()=>{}).then(()=>control.saveProfile(instance.profile,configPath)).then(()=>control.start(instance.profile,{waitSeconds:300}));};
|
||||
const startWith=value=>{writeFileSync(configPath,JSON.stringify(value,null,2),{mode:0o600});return control.stop(instance.profile).catch(()=>{}).then(()=>control.saveProfile(instance.profile,configPath)).then(()=>control.start(instance.profile,{waitSeconds:ownEngine(config)?Math.min(600,Math.max(300,Number(config.engine_ready_s)||300)):300}));};
|
||||
started=startWith(cfg);
|
||||
started.then(()=>append(id,'Strata Control reports the engine is ready.'),error=>{append(id,error.message);});
|
||||
try{await started;}catch(firstError){let error=firstError;
|
||||
@@ -271,7 +317,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
/* ONE automatic second try in all, never saved, never repeated (args.retrySafer false turns it off): with a safer memory setting when the
|
||||
failure looks like a memory stop, and without several-requests-at-once when that was the app's own default (not a number the user chose).
|
||||
Both changes ride on the same single try, with the plain sentence of the memory change when there is one. */
|
||||
const allowed=args.retrySafer!==false&&job.state==='running';
|
||||
const allowed=args.retrySafer!==false&&job.state==='running'&&!ownEngine(config);
|
||||
const safer=allowed?saferStart(cfg,snapshot.hardware,modelArena(args.model),said):null;
|
||||
const dropParallel=allowed&&cfg.parallel>1&&!parallelChoice?.explicit;
|
||||
if(!safer&&!dropParallel){instance.status='error';instance.error=said;emit();throw new Error(said);}
|
||||
@@ -289,7 +335,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
}
|
||||
const deadline=Date.now()+300000;while(Date.now()<deadline&&job.state==='running'){
|
||||
if(instance.status==='error')throw new Error(instance.error);
|
||||
try{const response=await fetcher(`http://127.0.0.1:${instance.port}/health`,{headers:{authorization:`Bearer ${key}`},signal:AbortSignal.timeout(2000)});if(job.state!=='running'){await unload(id);return;}if(response.ok){instance.status='ready';instance.phase='idle';if(resources||args.vision!==undefined||JSON.stringify(cfg[USER_KEY]??null)!==JSON.stringify(config[USER_KEY]??null))writeModelConfig(record(args.model).path,{...config,...(resources?{...resources,ui_resource_profile:args.resourceProfile}:{}),...visionPreferences,...(cfg[USER_KEY]?{[USER_KEY]:cfg[USER_KEY]}:{})});emit();ensurePoll();instance.parallelNote=parallelNote;return {summary:retried?`${instance.label} is ready, after a second try: ${retried.words}`:`${instance.label} is ready.${parallelNote?' '+parallelNote:''}`,instanceId:id};}}catch{}
|
||||
try{const response=await fetcher(`http://127.0.0.1:${instance.port}/health`,{headers:{authorization:`Bearer ${key}`},signal:AbortSignal.timeout(2000)});if(job.state!=='running'){await unload(id);return;}if(response.ok){instance.status='ready';instance.phase='idle';await noteDrafting(job,instance,key);if(resources||args.vision!==undefined||JSON.stringify(cfg[USER_KEY]??null)!==JSON.stringify(config[USER_KEY]??null))writeModelConfig(record(args.model).path,{...config,...(resources?{...resources,ui_resource_profile:args.resourceProfile}:{}),...visionPreferences,...(cfg[USER_KEY]?{[USER_KEY]:cfg[USER_KEY]}:{})});emit();ensurePoll();instance.parallelNote=parallelNote;return {summary:retried?`${instance.label} is ready, after a second try: ${retried.words}`:`${instance.label} is ready.${parallelNote?' '+parallelNote:''}`,instanceId:id};}}catch{}
|
||||
await new Promise(resolve=>setTimeout(resolve,1200));
|
||||
}
|
||||
if(job.state!=='running'){await unload(id);return;}
|
||||
@@ -303,7 +349,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
function ensurePoll(){if(!timer){timer=setInterval(()=>void poll(),2000);timer.unref?.();}void poll();}
|
||||
async function benchmark(job,args){const instance=resolveInstance(args.instanceId);job.instanceId=instance.id;if(busy(requests().filter(r=>r.instanceId===instance.id)))throw new Error('Wait for this model to finish before benchmarking.');const started=Date.now();job.abort=new AbortController();const response=await fetcher(`http://127.0.0.1:${instance.port}/v1/chat/completions`,{method:'POST',headers:{'content-type':'application/json',authorization:`Bearer ${instance.key}`},signal:AbortSignal.any([job.abort.signal,AbortSignal.timeout(180000)]),body:JSON.stringify({model:instance.apiModelId,messages:[{role:'user',content:'Explain how a rainbow forms in clear, simple language.'}],temperature:0,reasoning_effort:'none',max_tokens:200,stream:false})});const body=await response.json();if(!response.ok)throw new Error(body.error?.message??'Benchmark failed.');if(job.state!=='running')return;const result={summary:'Measured benchmark complete.',at:new Date().toISOString(),model:instance.apiModelId,durationMs:Date.now()-started,tokensIn:body.usage?.prompt_tokens??null,tokensOut:body.usage?.completion_tokens??null,outputTps:body.timings?.predicted_per_second??null,promptTps:body.timings?.prompt_per_second??null,source:'engine',settings:'Greedy · thinking off · up to 200 output tokens'};writeFileSync(join(directory,'native-benchmark.json'),JSON.stringify(result,null,2),{mode:0o600});return result;}
|
||||
function resolveInstance(id){const aliases=[...instances.values()].filter(i=>i.aliases?.includes(id));if(aliases.length>1&&!instances.has(id)&&![...instances.values()].some(i=>i.apiModelId===id))throw new Error('This alias matches more than one instance. Choose its exact model ID.');const i=instances.get(id)??[...instances.values()].find(i=>i.apiModelId===id)??aliases[0]??(!id&&instances.size===1?[...instances.values()][0]:undefined);if(!i)throw new Error('Choose a loaded model ID.');if(i.status!=='ready')throw new Error('This model is not ready.');return i;}
|
||||
async function program(job,args){if(packaged)return componentProgram(job);if(externalRunning().length)throw new Error('Stop the native server running outside this UI before changing engine files.');if(instances.size)throw new Error('Unload native models before changing engine files.');const folder=repo();let {command,args:prefix}=python(),argv;
|
||||
async function program(job,args){if(packaged)return componentProgram(job,args);if(externalRunning().length)throw new Error('Stop the native server running outside this UI before changing engine files.');if(instances.size)throw new Error('Unload native models before changing engine files.');const folder=repo();let {command,args:prefix}=python(),argv;
|
||||
if(job.kind==='calibrate'||job.kind==='tune'){if(!state().capabilities.calibration)throw new Error('This installation calibrates NVIDIA GPUs only. Use the short benchmark on other devices.');const model=args.model??snapshot.installed?.models?.[0]?.model;const chosen=record(model);if(!chosen.entry.ready)throw new Error('Prepare this model before calibrating.');command=venvPython(folder);prefix=[];if(!existsSync(command))throw new Error('Prepare the Python environment first.');argv=['-c','import sys; from pathlib import Path; import setup; sys.exit(0 if setup.calibrate_config(Path(sys.argv[1])) else 1)',chosen.path];}
|
||||
else if(job.kind==='rollback'){if(!previousEngine())throw new Error('No saved previous engine is available.');argv=[join(folder,'setup.py'),'--rollback-engine'];}
|
||||
else if(job.kind==='update-engine'){if(!snapshot.installed?.models?.length)throw new Error('Prepare a model first. Its installer downloads the matching engine.');argv=[join(folder,'setup.py'),'--update'];}
|
||||
@@ -314,29 +360,33 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
/* Packaged app: engine and Control updates and the engine rollback go through Control's explicit update step (`strata update`,
|
||||
`strata components rollback`), never through git, setup.py or a download in this process. The app itself is updated by the
|
||||
shell's signed Tauri updater. Models are not part of any of it. */
|
||||
async function componentProgram(job){
|
||||
if(!['update-engine','update-strata','rollback'].includes(job.kind)||typeof control.update!=='function')throw new Error(NO_INSTALL);
|
||||
async function componentProgram(job,args={}){
|
||||
if(!COMPONENT_KINDS.includes(job.kind)||typeof control.update!=='function')throw new Error(NO_INSTALL);
|
||||
if(instances.size)throw new Error('Unload native models before changing engine files.');
|
||||
job.abort=new AbortController();
|
||||
const subject=job.kind==='update-strata'?'Strata':'engine';
|
||||
try{return await componentSteps(job,subject);}catch(error){throw error?.details!==undefined&&error.message?error:plainError(error,subject);}
|
||||
try{return await componentSteps(job,subject,args);}catch(error){updateLog(`${job.kind} failed: ${String(error?.message??error).slice(0,400)}`);throw error?.details!==undefined&&error.message?error:plainError(error,subject);}
|
||||
}
|
||||
async function componentSteps(job,subject){
|
||||
async function componentSteps(job,subject,args={}){
|
||||
const onEvent=event=>{
|
||||
if(event.event==='progress'){job.summary=`Downloading: ${event.percent}%`;job.result={install:{step:job.summary,download:{file:event.label,percent:event.percent}}};}
|
||||
else if(event.event==='phase'&&PHASE_TEXT[event.phase])job.summary=PHASE_TEXT[event.phase];
|
||||
else if(event.event==='waiting')job.summary='Waiting for the running model to stop…';
|
||||
emit();
|
||||
};
|
||||
updateLog(`${job.kind} started (app ${thisAppVersion()??'unknown'})`);
|
||||
if(job.kind==='install-engine-file'){if(typeof args.path!=='string'||!args.path)throw new Error('Choose the engine file first.');let done;try{done=await control.installEngineFile(args.path,{onEvent,signal:job.abort.signal});}finally{try{const own=resolve(directory,'incoming-engine')+sep;if(resolve(args.path).startsWith(own))rmSync(args.path,{force:true});}catch{/* the copy in the host's own folder; a leftover is harmless */}}return {summary:updateSummary(done)==='Everything is already up to date.'?(done.results?.[0]?.note??'Nothing was changed.'):updateSummary(done),result:done};}
|
||||
if(job.kind==='rollback'){const done=await control.rollbackEngine();return {summary:'The previous engine is back. Your models and settings are unchanged.',result:done};}
|
||||
if(job.kind==='update-engine'){const done=await control.update({engine:true,control:false,onEvent,signal:job.abort.signal});return {summary:updateSummary(done),result:done};}
|
||||
const done=await control.update({engine:false,control:true,onEvent,signal:job.abort.signal});
|
||||
updateLog(`Control step finished: ${updateSummary(done)}`);
|
||||
const notes=[updateSummary(done)];
|
||||
if(appUpdater){
|
||||
job.summary='Looking for a new version of the app…';emit();
|
||||
let verdict;
|
||||
try{verdict=await appUpdater.check();}catch(error){if(error?.code!=='app-update-unavailable')throw error;verdict={available:false};notes.push(appUpdater.unavailable||error.message);}
|
||||
if(verdict.available){await appUpdater.install({signal:job.abort.signal,onProgress:p=>{if(p.total){job.summary=`Downloading the app update: ${Math.round(100*p.downloaded/p.total)}%`;job.result={install:{step:job.summary,download:{file:'Strata app',percent:Math.round(100*p.downloaded/p.total)}}};emit();}}});notes.push('The app update is installed. Strata will restart.');}
|
||||
updateLog(`app check: ${verdict.available?`version ${verdict.version} is available`:'no newer app version'}`);
|
||||
if(verdict.available){job.appUpdateFrom=thisAppVersion();job.appUpdateTo=verdict.version??null;emit();updateLog(`app update to ${verdict.version??'?'} starting; the installer will end this app`);await appUpdater.install({signal:job.abort.signal,onProgress:p=>{if(p.total){job.summary=`Downloading the app update: ${Math.round(100*p.downloaded/p.total)}%`;job.result={install:{step:job.summary,download:{file:'Strata app',percent:Math.round(100*p.downloaded/p.total)}}};emit();}}});notes.push('The app update is installed. Strata will restart.');}
|
||||
}
|
||||
return {summary:notes.filter(Boolean).join(' '),result:done};
|
||||
}
|
||||
@@ -382,7 +432,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
const room=args.resourceProfile==='work';
|
||||
return autotuner.start(job,job.kind==='recheck'?'recheck':'tune',{configPath:path,presetId:args.presetId,mode:args.mode,use:args.use,goal:room?'room':'speed',leaveRam:room?20:0,allowSmallDifferences:args.allowSmallDifferences===true,name:args.name},{onProgress:p=>{job.summary=p.text??job.summary;emit();}});
|
||||
}
|
||||
function operation(kind,args={}){if(!['install','load','benchmark','tune','autotune','recheck','calibrate','update-engine','update-strata','rollback','remove'].includes(kind))throw new Error('Choose a supported native operation.');if(!snapshot.available)throw new Error(snapshot.error??'Connect the native Strata installation first.');if(kind==='install'||kind==='load'){const refusal=engineRefusal(snapshot.catalog,args.model,snapshot.engineInfo);if(refusal)throw new Error(refusal);}return begin(kind,job=>kind==='install'?install(job,args):kind==='load'?loadEngine(job,args):kind==='benchmark'?benchmark(job,args):kind==='tune'?tune(job,args):kind==='autotune'||kind==='recheck'?autotune(job,args):kind==='remove'?removeModel(job,args):program(job,args),{guided:args.guided===true,model:args.model,...(args.family?{family:args.family}:{})});}
|
||||
function operation(kind,args={}){if(!['install','load','benchmark','tune','autotune','recheck','calibrate','update-engine','update-strata','rollback','install-engine-file','remove'].includes(kind))throw new Error('Choose a supported native operation.');if(!snapshot.available&&!(packaged&&COMPONENT_KINDS.includes(kind)))throw new Error(snapshot.error??'Connect the native Strata installation first.');if(kind==='install'||kind==='load'){const refusal=engineRefusal(snapshot.catalog,args.model,snapshot.engineInfo);if(refusal)throw new Error(refusal);}return begin(kind,job=>kind==='install'?install(job,args):kind==='load'?loadEngine(job,args):kind==='benchmark'?benchmark(job,args):kind==='tune'?tune(job,args):kind==='autotune'||kind==='recheck'?autotune(job,args):kind==='remove'?removeModel(job,args):program(job,args),{guided:args.guided===true,model:args.model,...(args.family?{family:args.family}:{})});}
|
||||
/* ---- The models folder card (0.2.2): what a picked folder means, and the three choices. The copying is Control's (strata models move). ---- */
|
||||
const currentModelsFolder=()=>preferences.downloadDirectory??(modelsDirectory?modelsDirectory():snapshot.installed?.data_folder)??null;
|
||||
const moveFile=()=>join(directory,'models-move.cancel');
|
||||
@@ -405,7 +455,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
const under=value=>typeof value==='string'&&[ 'models','packs','mtp' ].some(item=>{const base=join(old,item).toLowerCase();const v=value.toLowerCase();return v===base||v.startsWith(base+sep)||v.startsWith(base+'/');});
|
||||
const scan=value=>Array.isArray(value)?value.some(scan):value&&typeof value==='object'?Object.values(value).some(scan):under(value);
|
||||
for(const name of readdirSync(folder)){
|
||||
if(!/^strata-[a-z0-9_-]+\.json$/i.test(name)||name.endsWith('.shared-settings.json'))continue;
|
||||
if(!/^strata-[a-z0-9_.-]+\.json$/i.test(name)||name.endsWith('.shared-settings.json'))continue;
|
||||
if(!scan(readJson(join(folder,name))))continue;
|
||||
mkdirSync(kept,{recursive:true});renameSync(join(folder,name),join(kept,name));aside.push(name);
|
||||
}
|
||||
@@ -482,5 +532,5 @@ 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,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'),close};
|
||||
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,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};
|
||||
}
|
||||
@@ -28,12 +28,16 @@ export function defaultReserves(config,hardware,baselineReserve=0){
|
||||
const placement=Array.isArray(config.gpu)?config.gpu:[config.gpu??0];
|
||||
const placed=(hardware?.gpus??[]).filter(g=>placement.includes(g.index)),gpuMemory=placed.map(g=>g.vram_gb).filter(n=>Number.isFinite(n)&&n>0);
|
||||
const headless=placed.length>0&&placed.every(computeOnly); // no display on the card (the desktop is on an integrated GPU): no desktop to keep room for
|
||||
const floor=headless?(config.vision?ENGINE_DEFAULT_RESERVE_MIB:COMPUTE_ONLY_RESERVE_MIB):gpuMemory.length&&Math.min(...gpuMemory)>16?1024:1536;
|
||||
const safety=headless?floor:Math.max(config.vision?2048:floor,baselineReserve);
|
||||
/* Only a picture reader that runs on the GPU needs VRAM of its own: the CPU reader (vision.gpu false) took the same 2048 floor and cost a 12 GB card a quarter of its expert cache (0.2.3 tuning, notes-023-tune.md). */
|
||||
const pictures=config.vision?(typeof config.vision==='object'?Boolean(config.vision.gpu):true):false;
|
||||
const floor=headless?(pictures?ENGINE_DEFAULT_RESERVE_MIB:COMPUTE_ONLY_RESERVE_MIB):gpuMemory.length&&Math.min(...gpuMemory)>16?1024:1536;
|
||||
const safety=headless?floor:Math.max(pictures?2048:floor,baselineReserve);
|
||||
/* A card that shares the PC's memory (Strix Halo, other APUs) has no memory of its own to keep a fifth of: that fifth is RAM, and "Leave 20% for my work" already keeps it
|
||||
* through the RAM budget. A fifth of the shared pool here held back 16 GB of expert cache for nothing (#3510); the display floor stays. */
|
||||
const shared=placed.length>0&&placed.some(g=>g.uma===true||g.unified===true);
|
||||
return {work:gpuMemory.length&&!headless&&!shared?Math.max(safety,Math.floor(Math.min(...gpuMemory)*1024*.2)):safety,performance:safety};
|
||||
/* "work" keeps a fifth of the card, but on a card of 16 GB or less no more than 1536 MiB: a fifth of 12 GB (2457) cost 20% of the speed against 1536, which already leaves 1.1 GiB of VRAM free after the load (RTX 5070, Gyro-S 64K, 5 pairs; notes-023-tune.md). */
|
||||
const fifth=gpuMemory.length?Math.floor(Math.min(...gpuMemory)*1024*.2):0,cap=gpuMemory.length&&Math.min(...gpuMemory)<=16?1536:Infinity;
|
||||
return {work:gpuMemory.length&&!headless&&!shared?Math.max(safety,Math.min(fifth,cap)):safety,performance:safety};
|
||||
}
|
||||
|
||||
/** The reserve the person asked for (null: nothing) and the default for each profile. A 0.2.0-beta file has the key and the same number in "args" with no note that the
|
||||
@@ -63,7 +67,8 @@ export function resourceSettings(config,hardware,profile,model={},values={}){
|
||||
const cores=Number(hardware?.cpu?.cores)||0;
|
||||
if(profile==='work'&&cores>1){
|
||||
const at=args.indexOf('--pool-workers'),previous=at>=0?Number(args[at+1]):0;
|
||||
const workers=Math.max(1,Math.floor(cores*.8));remove('--pool-workers');
|
||||
/* 80% of the physical cores rounded UP, never more than the cores less the host thread (the engine's own count): floor(0.8 x 6) = 4 ran 8-10% slower than the engine's 5 on a 6-core Ryzen (notes-023-tune.md); 12 logical threads gave 9 in 0.2.1. */
|
||||
const workers=Math.max(1,Math.min(cores-1,Math.ceil(cores*.8)));remove('--pool-workers');
|
||||
args.push('--pool-workers',String(previous>0?Math.min(previous,workers):workers));
|
||||
}
|
||||
// Preserve calibrated worker counts and the installation's safety/vision reserve.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { validMcpName, MCP_NAME_HELP } from './mcp-names.mjs';
|
||||
/** Editable native keys. Unknown config keys and program paths stay intact. */
|
||||
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.'},
|
||||
@@ -8,7 +9,9 @@ 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',label:'Draft layer (MTP)',group:'Serving',type:'boolean',hint:'The draft layer guesses several words ahead, which makes answers faster, and costs about 1 GB of graphics memory and some RAM. Turn it off if the model stops while it starts, or the card is small: the expert cache gets that memory back and answers are slower. Default is on.'},
|
||||
{id:'drafting',label:'Drafting',group:'Serving',type:'enum',choices:['auto','mtp','dflash2','off'],hint:'Which drafter guesses words ahead to make answers faster. Auto (recommended) lets Strata pick from what it measured on cards like yours; today that is the MTP. DFlash2 is for Qwen3.8-Flash-Next only and downloads a 1.2 GB drafter once. If DFlash2 cannot run, the MTP is used and Details says why. Off turns the draft layer off.'},
|
||||
{id:'batchMtp',label:'MTP per request',group:'Serving',type:'boolean',hint:'Each concurrent request uses the draft layer. Left alone, Strata turns it on for one graphics card with 24 GB or more and several requests at once, and off otherwise; choose on or off here to decide yourself.'},
|
||||
{id:'ds_experts',label:'Speed (DeepSeek)',group:'Serving',type:'enum',choices:['5','6'],forModel:'deepseek',hint:'Faster uses 5 of 6 experts per word: a small quality change, usually not noticeable. Full quality uses all 6. Left alone, the engine picks Faster on a PC whose graphics cards total 16 GB or less.'},
|
||||
{id:'parallel',label:'Concurrent requests',group:'Serving',type:'number',min:1,max:8,hint:'More slots need more memory and can slow each response.'},
|
||||
{id:'engine_silence_s',label:'Engine stall timeout (seconds)',group:'Recovery',type:'number',min:10,max:3600},
|
||||
{id:'api_monitor',label:'Keep API request metadata',group:'Recovery',type:'boolean'},
|
||||
@@ -24,7 +27,7 @@ export const NATIVE_FIELDS=[
|
||||
{id:'mcp_max_result_chars',label:'Maximum tool result characters',group:'Tools',type:'number',min:100,max:1000000},
|
||||
];
|
||||
/** Settings the app itself applies when it starts a model (they are not read from the engine's source, so the engine's text never says it supports them). */
|
||||
export const APP_OWNED_FIELDS=['mtp','batchMtp'];
|
||||
export const APP_OWNED_FIELDS=['mtp','batchMtp','drafting','ds_experts'];
|
||||
/** The engine arguments without the draft layer: --mtp <folder or count>, --spec-min-p <p> (its confidence threshold) and --batch-mtp. --spec stays: prompt lookup still uses its windows. Same rule as strata_control/draft_layer.py. */
|
||||
export function withoutDraftLayer(args){
|
||||
const out=[];const list=(args??[]).map(String);
|
||||
@@ -56,7 +59,9 @@ export function publicMcpServers(servers){return Object.fromEntries(Object.entri
|
||||
export function validateMcpServers(servers,previous={}){
|
||||
if(!servers||typeof servers!=='object'||Array.isArray(servers)||Object.keys(servers).length>30)throw new Error('MCP servers must be an object with at most 30 entries.');
|
||||
const out={};for(const [name,s] of Object.entries(servers)){
|
||||
if(!/^[a-z0-9_-]{1,64}$/i.test(name)||!s||typeof s!=='object'||Array.isArray(s))throw new Error('Give each tool server a simple, unique name.');
|
||||
if(!s||typeof s!=='object'||Array.isArray(s))throw new Error('Give each tool server a simple, unique name.');
|
||||
// a name that was saved before the 32-character rule stays saveable (unrelated settings must not be blocked); the agent reports it as left out
|
||||
if(!validMcpName(name)&&!Object.hasOwn(previous??{},name))throw new Error(MCP_NAME_HELP);
|
||||
if(!!s.command===!!s.url)throw new Error(`${name}: choose a command or HTTP URL.`);
|
||||
for(const key of Object.keys(s))if(!['command','args','env','cwd','url','headers','disabled'].includes(key))throw new Error(`${name}: unsupported property ${key}.`);
|
||||
if(s.command&&(typeof s.command!=='string'||s.command.length>1000||/[\n\r\0]/.test(s.command)))throw new Error(`${name}: choose a valid program.`);
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* A message too large for the model's window, made small enough to send (agent/context-fit.mjs decides that it is too large).
|
||||
* The person picks how, on the error card:
|
||||
* file the whole text is saved in the chat's folder and the message names the file; the assistant reads it in parts
|
||||
* preview the start and the end of the text, cut to what the window can hold
|
||||
* Nothing is sent from here: the page sends the returned message as the person's next message.
|
||||
*/
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { countTokens, oversizedReplacement } from './agent/context-fit.mjs';
|
||||
|
||||
const stamp = d => `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}-${String(d.getHours()).padStart(2, '0')}${String(d.getMinutes()).padStart(2, '0')}${String(d.getSeconds()).padStart(2, '0')}`;
|
||||
|
||||
/**
|
||||
* @param {{mode:'file'|'preview', text:string, root:string|null, instance?:{port:number, key?:string, apiModelId?:string, context?:number, metrics?:any}|null, now?:Date, fetchImpl?:typeof fetch}} input
|
||||
* @returns {Promise<{message:string, mode:string, tokens:number|null, kept:number, path?:string}>}
|
||||
*/
|
||||
export async function prepareOversized({ mode, text, root, instance = null, now = new Date(), fetchImpl = fetch }) {
|
||||
const window = instance?.metrics?.engine?.max_context ?? instance?.context ?? 32768;
|
||||
const tokens = instance ? await countTokens({ baseUrl: `http://127.0.0.1:${instance.port}/v1`, key: instance.key ?? '', model: instance.apiModelId ?? '', text, fetchImpl }) : null;
|
||||
const counted = tokens ?? Math.ceil(text.length / 3); // not countable: three characters a token is on the safe side for code and logs
|
||||
if (mode === 'file') {
|
||||
if (!root) throw new Error('This chat has no folder yet. Choose one first.');
|
||||
mkdirSync(root, { recursive: true });
|
||||
const path = join(root, `pasted-message-${stamp(now)}.txt`);
|
||||
writeFileSync(path, text, { encoding: 'utf8', flag: 'wx' });
|
||||
return { mode, tokens, path, ...oversizedReplacement({ text, mode, tokens: counted, window, path }) };
|
||||
}
|
||||
return { mode: 'preview', tokens, ...oversizedReplacement({ text, mode: 'preview', tokens: counted, window }) };
|
||||
}
|
||||
@@ -88,10 +88,21 @@ export let MODELS_DIR = MODELS_PICK.directory;
|
||||
|
||||
/** Native setup writes weights to data_dir/models. Keep its data root one level up. */
|
||||
export function nativeDataDirectory(input) {
|
||||
if(!input||process.env.STRATA_MODELS_DIR||!existsSync(DEFAULT_MODELS_DIRECTORY))return input;
|
||||
if(!input||!existsSync(input))return input;
|
||||
if(MODELS_DIR_NOTICE)return input;
|
||||
const root=realpathSync(DEFAULT_MODELS_DIRECTORY);
|
||||
if(resolve(input)!==resolve(DEFAULT_MODELS_DIRECTORY)&&realpathSync(input)!==root)return input;
|
||||
const root=realpathSync(input),legacy=realpathSync(join(HOME,'models'));
|
||||
// Retained legacy libraries are model folders too, not native data roots.
|
||||
// A named data root satisfies the installer's guard; its alias leaves weights in place.
|
||||
if(root===legacy){
|
||||
const data=join(HOME,'Strata-data'),nativeModels=join(data,'models');
|
||||
mkdirSync(data,{recursive:true});
|
||||
// Unlike a fresh default, a retained library must not fall back to an empty folder if linking fails.
|
||||
if(!existsSync(nativeModels))symlinkSync(process.platform==='win32'?root:relative(data,root),nativeModels,process.platform==='win32'?'junction':'dir');
|
||||
const a=statSync(nativeModels),b=statSync(root);
|
||||
if(realpathSync(nativeModels)!==root&&!(a.ino&&a.ino===b.ino&&a.dev===b.dev))throw new Error('The model folder conflicts with another library. Choose a different folder.');
|
||||
return data;
|
||||
}
|
||||
if(process.env.STRATA_MODELS_DIR||!existsSync(DEFAULT_MODELS_DIRECTORY)||root!==realpathSync(DEFAULT_MODELS_DIRECTORY))return input;
|
||||
const data=dirname(root);
|
||||
// `models` and `Models` are one folder on Windows and macOS; where they are not (Linux) a link makes them one, and where no link can be made (exFAT, a share) models go in `models`.
|
||||
alignModels(data,root);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// The host's own HTTPS downloads (engine releases from GitHub, web search, a cloud model's API) check the server against the operating
|
||||
// system's certificates as well as Node's bundled list (#3722). Node carries its own list of roots; Chrome and curl.exe use the Windows
|
||||
// store, which also holds the roots an antivirus's HTTPS scanning, a company proxy or an OS preview build adds. Nothing is turned off:
|
||||
// the system roots are ADDED to the bundled ones, and a connection that still fails keeps failing.
|
||||
//
|
||||
// Node 24.5 and newer: tls.getCACertificates('system') and tls.setDefaultCACertificates. An older Node (the app bundles 24.x) does nothing here;
|
||||
// the agent process gets NODE_USE_SYSTEM_CA=1 (harness-runtime.mjs) for the same reason.
|
||||
import tlsModule from 'node:tls';
|
||||
|
||||
/** @returns {'system'|'bundled'} what the process trusts after the call */
|
||||
export function useSystemCertificates(tls = tlsModule, env = process.env) {
|
||||
if (env.STRATA_NO_SYSTEM_CA === '1') return 'bundled'; // a way back for a PC where reading the store misbehaves; not a way to skip checks
|
||||
try {
|
||||
if (typeof tls.getCACertificates !== 'function' || typeof tls.setDefaultCACertificates !== 'function') return 'bundled';
|
||||
const system = tls.getCACertificates('system');
|
||||
if (!Array.isArray(system) || !system.length) return 'bundled';
|
||||
tls.setDefaultCACertificates([...new Set([...tls.getCACertificates('bundled'), ...system])]);
|
||||
return 'system';
|
||||
} catch {
|
||||
return 'bundled';
|
||||
}
|
||||
}
|
||||
|
||||
export const trust = useSystemCertificates();
|
||||
@@ -1,5 +1,7 @@
|
||||
/** Host counters. CPU sockets are measured only when their logical CPU IDs are known. */
|
||||
import os from 'node:os';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { createPowerShellLoop, endWithHost } from './windows-poll.mjs';
|
||||
import { readFile, readdir } from 'node:fs/promises';
|
||||
import { availableMemoryGb } from './hardware.mjs';
|
||||
import { probe } from './util.mjs';
|
||||
@@ -18,7 +20,7 @@ export function cpuUtilization(previous, current, ids) {
|
||||
}
|
||||
|
||||
export function diskRate(previous, current, time) {
|
||||
const elapsed=previous?(time-previous.time)/1000:0;
|
||||
const elapsed=previous?((current?.at??time)-(previous.counters?.at??previous.time))/1000:0;
|
||||
if(elapsed<=0||!current||!previous?.counters)return {readMbs:null,writeMbs:null};
|
||||
const read=current.read-previous.counters.read,write=current.write-previous.counters.write;
|
||||
return {readMbs:read>=0?read/1024**2/elapsed:null,writeMbs:write>=0?write/1024**2/elapsed:null};
|
||||
@@ -41,6 +43,33 @@ export function parseMacDiskCounters(raw) {
|
||||
return reads.length&&writes.length?{read:reads.reduce((n,m)=>n+Number(m[1]),0),write:writes.reduce((n,m)=>n+Number(m[1]),0)}:null;
|
||||
}
|
||||
|
||||
export function parseWindowsDiskLine(line) {
|
||||
const m=/^\s*(\d+)\s+(\d+)\s*$/.exec(String(line));
|
||||
return m?{read:Number(m[1]),write:Number(m[2])}:null;
|
||||
}
|
||||
|
||||
/** The disk counters, read by one long-lived PowerShell (windows-poll.mjs; #3939). */
|
||||
export function createWindowsDiskReader({ spawnFn=spawn, intervalSec=4, maxStarts=3, now=Date.now }={}) {
|
||||
let latest=null;
|
||||
const script=`while($true){$d=Get-CimInstance Win32_PerfRawData_PerfDisk_PhysicalDisk -Filter "Name='_Total'";if($d){[Console]::Out.WriteLine(([string]$d.DiskReadBytesPersec)+' '+([string]$d.DiskWriteBytesPersec));[Console]::Out.Flush()};Start-Sleep -Seconds ${intervalSec}}`;
|
||||
const loop=createPowerShellLoop({script,spawnFn,maxStarts,onLine:line=>{
|
||||
const parsed=parseWindowsDiskLine(line);
|
||||
if(parsed)latest={...parsed,at:now()};
|
||||
return !!parsed;
|
||||
}});
|
||||
return {
|
||||
read() { loop.start(); return latest&&now()-latest.at<5*intervalSec*1000?latest:null; },
|
||||
stop() { loop.stop(); },
|
||||
get starts() { return loop.starts; },
|
||||
};
|
||||
}
|
||||
|
||||
let windowsReader=null;
|
||||
function windowsDisks() {
|
||||
windowsReader??=endWithHost(createWindowsDiskReader());
|
||||
return windowsReader.read();
|
||||
}
|
||||
|
||||
async function disks() {
|
||||
try {
|
||||
if(process.platform==='linux') {
|
||||
@@ -49,24 +78,26 @@ async function disks() {
|
||||
}
|
||||
if(process.platform==='darwin')return parseMacDiskCounters(await probe('ioreg',['-r','-c','IOBlockStorageDriver','-l'],1500));
|
||||
if(process.platform==='win32') {
|
||||
const raw=await probe('powershell.exe',['-NoProfile','-NonInteractive','-Command',"Get-CimInstance Win32_PerfRawData_PerfDisk_PhysicalDisk -Filter \"Name='_Total'\" | Select-Object DiskReadBytesPersec,DiskWriteBytesPersec | ConvertTo-Json -Compress"],2000);
|
||||
const value=JSON.parse(raw);
|
||||
if(value.DiskReadBytesPersec!=null&&value.DiskWriteBytesPersec!=null)return {read:Number(value.DiskReadBytesPersec),write:Number(value.DiskWriteBytesPersec)};
|
||||
return windowsDisks();
|
||||
}
|
||||
} catch { /* An unsupported counter stays unavailable. */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
export function createSystemSampler() {
|
||||
let previousCpu=null,previousDisk=null;
|
||||
let previousCpu=null,previousDisk=null,lastRate={readMbs:null,writeMbs:null};
|
||||
return async processors => {
|
||||
const current=os.cpus(),time=Date.now();
|
||||
const cpuLoad=cpuUtilization(previousCpu,current,current.map((_,i)=>i));
|
||||
const cpus=processors.map(p=>({id:p.id,utilizationPct:cpuUtilization(previousCpu,current,p.logicalIds??(processors.length===1?current.map((_,i)=>i):[]))}));
|
||||
previousCpu=current;
|
||||
const [available,counters]=await Promise.all([availableMemoryGb(process.platform),disks()]);
|
||||
const rate=diskRate(previousDisk,counters,time);
|
||||
previousDisk={time,counters};
|
||||
let rate=lastRate;
|
||||
if(!(counters&&counters.at!==undefined&&previousDisk?.counters?.at===counters.at)) {
|
||||
rate=diskRate(previousDisk,counters,time);
|
||||
previousDisk={time,counters};
|
||||
lastRate=rate;
|
||||
}
|
||||
const total=os.totalmem()/1024**3,free=available??os.freemem()/1024**3;
|
||||
return {cpuLoad,cpus,ramUsedGb:Math.max(0,Math.min(total,total-free)),ramTotalGb:total,diskReadGbs:rate.readMbs===null?null:rate.readMbs/1024,diskWriteMbs:rate.writeMbs};
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ export function plainUpdateError(error, subject = 'engine') {
|
||||
} else if (/another update is already running/i.test(raw)) {
|
||||
message = 'An update is already running.';
|
||||
next = `Wait a moment, then press ${button} in Settings.`;
|
||||
} else if (/test start|did not answer a one-token|did not become ready/i.test(raw)) {
|
||||
} else if (/test start|did not start correctly|did not answer a one-token|did not become ready/i.test(raw)) {
|
||||
message = `The new ${subject} didn't start correctly, so nothing was changed. ${keep}`;
|
||||
} else if (looksOffline(error) || looksOffline(raw)) {
|
||||
message = 'No internet connection.';
|
||||
|
||||
@@ -59,7 +59,7 @@ export async function checkComponentUpdates({ control, appUpdater = null, appVer
|
||||
const message = /update server|aren't available|No internet/.test(plain.headline) ? plain.message : `Couldn't check for updates. Try again in a moment.`;
|
||||
return { checkedAt, strata: { installedVersion: appVersion ?? control.version ?? null, status: 'error', message, details: plain.details, canInstall: false }, engine: { installedVersion: null, status: 'error', message, details: plain.details, canInstall: false } };
|
||||
}
|
||||
const mib = bytes => `${Math.round((bytes ?? 0) / (1 << 20))} MB`;
|
||||
const mib = bytes => `${Math.round((bytes ?? 0) / 1_000_000)} MB`; // decimal megabytes, like the Terminal and the release page
|
||||
const engine = report.engine ?? {};
|
||||
const installedEngine = engine.installed?.version ?? null;
|
||||
const offered = engine.offered?.version ?? null;
|
||||
@@ -67,12 +67,14 @@ export async function checkComponentUpdates({ control, appUpdater = null, appVer
|
||||
if (engine.status === 'available') Object.assign(engineView, { status: 'available', message: `A new engine is available (${mib(engine.download_bytes)}).` });
|
||||
else if (engine.status === 'install') Object.assign(engineView, { status: 'available', message: `This PC needs an engine to run models. Press Update engine to download it (${mib(engine.download_bytes)}).` });
|
||||
else if (engine.status === 'current' || engine.status === 'failed-before' || engine.status === 'skipped') Object.assign(engineView, { status: 'current', message: 'The engine is up to date.', details: engine.note });
|
||||
else if (engine.status === 'wrong-make') Object.assign(engineView, { status: 'unavailable', message: engine.note ?? 'Nothing was changed: the update was for another make of graphics card.', details: (engine.reasons ?? []).join('\n') || undefined });
|
||||
else if (engine.status === 'not-in-feed') Object.assign(engineView, { status: 'current', message: 'No engine update is listed right now.' });
|
||||
else Object.assign(engineView, { status: 'unavailable', message: "There isn't an engine for this PC's graphics card yet.", details: (engine.reasons ?? []).join('\n') || undefined });
|
||||
const control_ = report.control ?? {};
|
||||
const app = report.app ?? {};
|
||||
let appNew = app.status === 'available' ? app.offered : null;
|
||||
let appNotes = app.notes ?? '';
|
||||
let appCheckFailed = null; // the shell's own (Tauri) check threw: before this, the Settings check hid it and showed only Control's text
|
||||
let appCannot = null; // browser mode: the app window's updater is not running, so the app itself cannot be updated from here
|
||||
if (appUpdater) {
|
||||
try {
|
||||
@@ -80,12 +82,41 @@ export async function checkComponentUpdates({ control, appUpdater = null, appVer
|
||||
appNew = verdict.available ? verdict.version : null;
|
||||
appNotes = verdict.notes ?? appNotes;
|
||||
} catch (error) {
|
||||
if (error?.code === 'app-update-unavailable' && appUpdater.unavailable) appCannot = appUpdater.unavailable; // otherwise the feed's own view stands
|
||||
if (error?.code === 'app-update-unavailable' && appUpdater.unavailable) appCannot = appUpdater.unavailable;
|
||||
else appCheckFailed = String(error?.message ?? error ?? 'The app update check failed.'); // the app's own check did not work: say so, raw text behind Details
|
||||
}
|
||||
}
|
||||
const controlNew = control_.status === 'available' ? control_.offered : null;
|
||||
const strata = { installedVersion: appVersion ?? control_.installed ?? control.version ?? null, canInstall: !!((appNew && !appCannot) || controlNew) };
|
||||
if (appNew || controlNew) Object.assign(strata, { status: 'available', latestVersion: appNew ?? controlNew, message: `A new version of Strata is available.${appNotes ? ` ${appNotes}` : ''}${appNew && appCannot ? ` ${appCannot}` : ''}` });
|
||||
const appCheckLine = "Strata couldn't check for a new version of the app itself. Try again later.";
|
||||
if (appNew || controlNew) Object.assign(strata, { status: 'available', latestVersion: appNew ?? controlNew, message: `A new version of Strata is available.${appNotes ? ` ${appNotes}` : ''}${appNew && appCannot ? ` ${appCannot}` : ''}${appCheckFailed ? ` ${appCheckLine}` : ''}` });
|
||||
else if (appCheckFailed) Object.assign(strata, { status: 'error', message: appCheckLine });
|
||||
else Object.assign(strata, { status: 'current', message: 'Strata is up to date.' });
|
||||
if (appCheckFailed) strata.details = appCheckFailed;
|
||||
return { checkedAt, feed: report.feed, strata, engine: engineView, serving: report.serving ?? [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* What the first-run card shows: this PC's graphics card(s), the engine package the signed feed offers for it and how big it is.
|
||||
* One feed read, nothing downloaded. `status`: offer (a package fits), none (nothing fits; `reasons` say why), offline (the feed could not be
|
||||
* read), installed (an engine is already here), wrong-make (an engine for another make of card is installed).
|
||||
*/
|
||||
export async function engineFirstRun({ control, gpus = [] }) {
|
||||
const cards = gpus.map(g => ({ name: g.name ?? 'Graphics card', vramGb: g.vramGb ?? null }));
|
||||
let report;
|
||||
try {
|
||||
report = await control.updateCheck({});
|
||||
} catch (error) {
|
||||
const plain = plainUpdateError(error, 'Strata');
|
||||
return { status: looksOffline(error) || /update server|No internet/.test(plain.headline ?? '') ? 'offline' : 'error', gpus: cards, message: plain.message, details: plain.details };
|
||||
}
|
||||
const engine = report.engine ?? {};
|
||||
const hardware = report.hardware ?? {};
|
||||
const base = { gpus: cards, driver: hardware.nvidia_driver ?? null, backend: hardware.backend ?? null };
|
||||
if (engine.installed) return { ...base, status: 'installed', installedVersion: engine.installed.version ?? null };
|
||||
if (engine.status === 'wrong-make') return { ...base, status: 'wrong-make', message: engine.note ?? null, reasons: engine.reasons ?? [] };
|
||||
if (engine.offered && (engine.status === 'install' || engine.status === 'available')) {
|
||||
return { ...base, status: 'offer', offer: { version: engine.offered.version, bytes: engine.download_bytes || engine.offered.size || 0, variant: engine.offered.variant ?? '', backend: engine.offered.backend ?? null } };
|
||||
}
|
||||
return { ...base, status: 'none', message: engine.note ?? null, reasons: engine.reasons ?? [] };
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Windows graphics cards that nvidia-smi does not cover (AMD, Intel): their size and their load.
|
||||
* - size: the DirectX adapter list in the registry (HKLM\SOFTWARE\Microsoft\DirectX) has a 64-bit DedicatedVideoMemory and the adapter
|
||||
* LUID. Win32_VideoController.AdapterRAM is 32 bits and reads 0 or 4 GB for a 16 GB card (#3727, #3836).
|
||||
* - load and memory in use: the Windows GPU performance counters (the ones Task Manager shows), read by one long-lived PowerShell and
|
||||
* matched to the card by LUID. Temperature, power and the PCIe link need the vendor's own library, so they stay "not available".
|
||||
*/
|
||||
import { createPowerShellLoop, endWithHost } from './windows-poll.mjs';
|
||||
import { utf8Script } from './powershell-utf8.mjs';
|
||||
|
||||
export const DX_ADAPTERS_SCRIPT = utf8Script(
|
||||
"Get-ChildItem HKLM:\\SOFTWARE\\Microsoft\\DirectX | ForEach-Object { $p = Get-ItemProperty $_.PSPath; " +
|
||||
"if ($p.Description -and $null -ne $p.AdapterLuid) { [pscustomobject]@{ Description = [string]$p.Description; VendorId = [string]$p.VendorId; " +
|
||||
"Luid = [string]$p.AdapterLuid; Dedicated = [string]$p.DedicatedVideoMemory; Software = [string]$p.SoftwareAdapter } } } | ConvertTo-Json -Compress");
|
||||
|
||||
/** luid_0x<high>_0x<low> in the spelling of the performance counters, from the registry's 64-bit number. */
|
||||
export function luidName(value) {
|
||||
try {
|
||||
const n = BigInt.asUintN(64, BigInt(String(value)));
|
||||
const hex = v => '0x' + v.toString(16).padStart(8, '0');
|
||||
return `luid_${hex(n >> 32n)}_${hex(n & 0xffffffffn)}`;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
/** The adapters the DirectX registry lists: [{ description, vendorId, luid, bytes, software }]. Never throws. */
|
||||
export function parseDxAdapters(raw) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw), rows = Array.isArray(parsed) ? parsed : [parsed];
|
||||
return rows.filter(r => r?.Description).map(r => ({
|
||||
description: String(r.Description), vendorId: Number(r.VendorId) || 0, luid: luidName(r.Luid),
|
||||
bytes: Number(r.Dedicated) || 0, software: String(r.Software) === '1' || String(r.Software).toLowerCase() === 'true',
|
||||
}));
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
const norm = text => String(text ?? '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
const VIRTUAL = /virtual|remote|indirect|mirror|basic render|meta |parsec|displaylink|idd/i;
|
||||
|
||||
/**
|
||||
* Win32_VideoController rows + the DirectX adapter list -> the cards this app can show. A card keeps the size from the registry;
|
||||
* a display driver for a virtual monitor (no size, no known maker) is not a graphics card and is dropped.
|
||||
*/
|
||||
export function windowsOtherGpus(videoRows, dx) {
|
||||
const used = new Set();
|
||||
const out = [];
|
||||
for (const row of videoRows) {
|
||||
if (!row?.Name) continue;
|
||||
const match = dx.find((a, i) => !used.has(i) && !a.software && norm(a.description) === norm(row.Name));
|
||||
if (match) used.add(dx.indexOf(match));
|
||||
const vendor = /amd|radeon/i.test(row.Name) ? 'amd' : /intel/i.test(row.Name) ? 'intel' : 'unknown';
|
||||
const vramGb = match?.bytes ? Math.round(match.bytes / 1024 ** 3 * 10) / 10 : 0;
|
||||
if (vendor === 'unknown' && (!vramGb || VIRTUAL.test(row.Name))) continue;
|
||||
out.push({ id: String(row.PNPDeviceID), vendor, name: row.Name, vramGb, luid: match?.luid ?? null });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** One counter line: "G <luid> <load percent> <dedicated bytes in use>". */
|
||||
export function parseGpuCounterLine(line) {
|
||||
const m = /^G\s+(luid_0x[0-9a-f]+_0x[0-9a-f]+)\s+(\d+(?:\.\d+)?)\s+(\d+)\s*$/i.exec(String(line));
|
||||
return m ? { luid: m[1].toLowerCase(), utilizationPct: Math.max(0, Math.min(100, Number(m[2]))), memoryUsedGb: Number(m[3]) / 1024 ** 3 } : null;
|
||||
}
|
||||
|
||||
export function createWindowsGpuReader({ spawnFn, intervalSec = 4, maxStarts = 3, now = Date.now } = {}) {
|
||||
const readings = new Map();
|
||||
const script = utf8Script(
|
||||
`while($true){` +
|
||||
`$e=@(Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUEngine);$m=@(Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUAdapterMemory);` +
|
||||
`$u=@{};foreach($r in $e){if($r.Name -match '(luid_0x[0-9A-Fa-f]+_0x[0-9A-Fa-f]+)_phys_\\d+_eng_\\d+_engtype_(\\w+)'){$k=$matches[1].ToLower()+'|'+$matches[2];$u[$k]=[double]$u[$k]+[double]$r.UtilizationPercentage}};` +
|
||||
`$b=@{};foreach($k in $u.Keys){$l=$k.Split('|')[0];if(-not $b.ContainsKey($l) -or $u[$k] -gt $b[$l]){$b[$l]=$u[$k]}};` +
|
||||
`foreach($r in $m){if($r.Name -match '(luid_0x[0-9A-Fa-f]+_0x[0-9A-Fa-f]+)'){$l=$matches[1].ToLower();[Console]::Out.WriteLine('G '+$l+' '+[math]::Round([double]$b[$l],0)+' '+$r.DedicatedUsage)}};` +
|
||||
`[Console]::Out.Flush();Start-Sleep -Seconds ${intervalSec}}`);
|
||||
const loop = createPowerShellLoop({ script, spawnFn, maxStarts, onLine: line => {
|
||||
const parsed = parseGpuCounterLine(line);
|
||||
if (parsed) readings.set(parsed.luid, { ...parsed, at: now() });
|
||||
return !!parsed;
|
||||
} });
|
||||
return {
|
||||
read(luid) { loop.start(); const r = readings.get(String(luid ?? '').toLowerCase()); return r && now() - r.at < 6 * intervalSec * 1000 ? r : null; },
|
||||
stop() { loop.stop(); },
|
||||
};
|
||||
}
|
||||
|
||||
let shared = null;
|
||||
/** The card's load and memory in use from the Windows counters; null when there is no reading. */
|
||||
export function windowsGpuReading(luid) {
|
||||
if (process.platform !== 'win32' || !luid) return null;
|
||||
shared ??= endWithHost(createWindowsGpuReader());
|
||||
return shared.read(luid);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* One long-lived PowerShell that prints a line per reading, in a loop. The monitor used to start a cold PowerShell for every sample;
|
||||
* its WMI start cost more than the probe timeout, so every query failed and WmiPrvSE burned a core (#3939).
|
||||
* The loop gives up after `maxStarts` early exits in a row, so a counter that cannot be read stays quiet.
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
export function createPowerShellLoop({ script, onLine, spawnFn = spawn, maxStarts = 3 }) {
|
||||
let child = null, starts = 0, buffer = '';
|
||||
function start() {
|
||||
if (child || starts >= maxStarts) return;
|
||||
starts++;
|
||||
let proc;
|
||||
try { proc = spawnFn('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], { windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] }); }
|
||||
catch { return; }
|
||||
child = proc;
|
||||
buffer = '';
|
||||
proc.on?.('error', () => { if (child === proc) child = null; });
|
||||
proc.on?.('exit', () => { if (child === proc) child = null; });
|
||||
proc.stdout?.on('data', chunk => {
|
||||
buffer += chunk;
|
||||
let i;
|
||||
while ((i = buffer.indexOf('\n')) >= 0) {
|
||||
const line = buffer.slice(0, i);
|
||||
buffer = buffer.slice(i + 1);
|
||||
if (onLine(line)) starts = 0;
|
||||
}
|
||||
});
|
||||
proc.unref?.();
|
||||
proc.stdout?.unref?.();
|
||||
}
|
||||
return {
|
||||
start,
|
||||
stop() { try { child?.kill(); } catch { /* gone */ } child = null; },
|
||||
get starts() { return starts; },
|
||||
};
|
||||
}
|
||||
|
||||
let stoppers = null;
|
||||
/** Every loop is ended with the host. */
|
||||
export function endWithHost(loop) {
|
||||
if (!stoppers) { stoppers = new Set(); process.once('exit', () => stoppers.forEach(l => l.stop())); }
|
||||
stoppers.add(loop);
|
||||
return loop;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { useId, useRef, useState } from 'react';
|
||||
import { FloatingMenu } from './FloatingMenu';
|
||||
import { Bar, Btn, Dialog, ErrorNotice, Icon } from './ui';
|
||||
import { go, useLifecycle, loadVariant, selectRemoteModel, setSettingsSection, useStore } from '../lib/store';
|
||||
import { pickerGroups, pickerVersionState } from '../lib/model-picker';
|
||||
import { pickerGroups, pickerNativeModels, pickerVersionState } from '../lib/model-picker';
|
||||
import { versionFit } from '../lib/model-versions';
|
||||
import type { ModelVariant } from '../lib/types';
|
||||
import { sizeGb } from '../lib/format';
|
||||
@@ -37,6 +37,7 @@ function LegacyChatModelPicker() {
|
||||
const running = library?.variants.find(v => v.id === library.loaded);
|
||||
const version = library?.variants.find(v => v.id === selected);
|
||||
const groups = library ? pickerGroups(library) : [];
|
||||
const installedNative = pickerNativeModels(native?.installed?.models, native?.instances);
|
||||
const status = version ? pickerVersionState(version, library?.loaded ?? null) : null;
|
||||
const tooBig = !!version && library?.mode === 'real' && versionFit(version, hardware, contextTokens(engine?.beginner.contextLength ?? '32K')).zone === 'too-big';
|
||||
async function apply() {
|
||||
@@ -78,7 +79,8 @@ function LegacyChatModelPicker() {
|
||||
<button ref={trigger} className="chat-model-badge chat-model-picker" aria-label={tr("Choose model: {0}", [apiModel?.name ?? (running?.local ? running.name : running?.family) ?? 'No model loaded'])} title={tr(apiModel ? `${connection?.name} · ${apiModel.id}` : 'View and switch models')} disabled={!library && !providers} aria-haspopup="menu" aria-expanded={open} onClick={() => setOpen(v => !v)}><Icon name={apiModel ? 'link' : modelFamilyIcon(running?.family ?? groups[0]?.family)} size={16}/><span>{tr(apiModel?.name ?? (running?.local ? running.name : running?.family) ?? groups[0]?.family ?? 'No model loaded')}</span><Icon name="chevron" size={12}/></button>
|
||||
{tr(open ? <FloatingMenu anchor={trigger.current} title={tr("Models")} width={380} onClose={() => setOpen(false)}>
|
||||
{tr(native?.instances.filter(i => i.status !== 'error').map(i => <button key={i.id} className="menu__item" role="menuitem" disabled={streaming || i.status !== 'ready'} onClick={() => void nativeAction(`instances/${i.id}/default`).then(() => { selectRemoteModel(null); setOpen(false); }).catch(e => setError(e.message))}><Icon name="models"/><span>{tr(i.label)}<small>{tr(i.gpus.map(g => `GPU ${g + 1}`).join(" + ") || "Automatic GPU")}</small></span></button>))}<p className="model-picker-heading">{tr("Local models")}</p>
|
||||
{tr(!groups.length ? <p className="model-picker-empty">{tr("No local models downloaded yet.")}</p> : null)}
|
||||
{tr(installedNative.map(m => <button key={m.config || m.model} className="menu__item model-picker-option model-picker-api" role="menuitem" disabled={streaming} onClick={() => { setOpen(false); go('models'); }}><span><strong>{tr(m.model_name || m.model)}</strong><small>{tr("Downloaded, not loaded. Open Models to load it.")}</small></span></button>))}
|
||||
{tr(!groups.length && !installedNative.length ? <p className="model-picker-empty">{tr("No local models downloaded yet.")}</p> : null)}
|
||||
{tr(groups.map(({ family, versions }, index) => {
|
||||
const main = versions[0], state = pickerVersionState(main, library!.loaded);
|
||||
const others = versions.slice(1), isExpanded = expanded.includes(family), id = `${groupsId}-${index}`;
|
||||
|
||||
@@ -5,10 +5,11 @@ import { FloatingMenu } from './FloatingMenu';
|
||||
import { DictationControl } from './DictationControl';
|
||||
import { Btn, Dialog, Icon, ErrorNotice } from './ui';
|
||||
import { MOD } from '../lib/commands';
|
||||
import { setChatPreferences, useStore } from '../lib/store';
|
||||
import { pushToast, setChatPreferences, useStore } from '../lib/store';
|
||||
import { artifactFromUrl, type PreviewArtifact } from '../lib/artifacts';
|
||||
import type { ChatMode, Session } from '../lib/types';
|
||||
import { attachChatPhoto } from '../lib/chat-photos';
|
||||
import { uploadChatFile, FILE_ACCEPT } from '../lib/chat-attach-file';
|
||||
import { useNative } from '../lib/native';
|
||||
import { pictureStatus } from '../lib/pictures';
|
||||
const modes: {
|
||||
@@ -91,6 +92,12 @@ export function ComposerControls({ session, onPreview, onAttach, onError, onSlas
|
||||
await attachChatPhoto(file, session?.id);
|
||||
continue;
|
||||
}
|
||||
if (!directory && session?.id && /\.pdf$/i.test(file.name) && file.size <= 12 * 1024 * 1024) {
|
||||
// A PDF goes to the chat's folder and is named in the next message; "Open a file" still previews it.
|
||||
const saved = await uploadChatFile(file, session.id);
|
||||
pushToast('success', 'PDF added', `${saved.name} is in this chat. Ask about it in your next message.`);
|
||||
continue;
|
||||
}
|
||||
if (!directory && (photoTypes.test(file.name) || /\.pdf$/i.test(file.name))) {
|
||||
await openFile(file);
|
||||
if (photoTypes.test(file.name))
|
||||
@@ -157,7 +164,7 @@ export function ComposerControls({ session, onPreview, onAttach, onError, onSlas
|
||||
return <>
|
||||
{tr(!remoteModel && nativeModel?.metrics?.mcp?.servers?.length ? <button className="composer-choice" aria-label={tr("Use connected MCP tools")} aria-pressed={session?.preferences?.useTools === true} onClick={() => session?.preferences?.useTools ? setChatPreferences(session.id, { useTools: false }) : setToolsOpen(true)}><Icon name="skills" size={16}/>{tr("Tools")}{tr(session?.preferences?.useTools ? ' on' : '')}</button> : null)}
|
||||
<Dialog open={toolsOpen} compact title={tr("Enable connected tools?")} onClose={() => setToolsOpen(false)} actions={<><Btn onClick={() => setToolsOpen(false)}>{tr("Keep tools off")}</Btn><Btn variant="primary" onClick={() => { setChatPreferences(session?.id, { useTools: true, mode: 'bypass' }); setToolsOpen(false); }}>{tr("Enable tools")}</Btn></>}><p>{tr("Connected servers can act on the resources you configured. Calls run automatically when the model chooses them, and their results appear in chat.")}</p><p className="small muted">{tr("This switches this conversation to Bypass permissions. Servers: ")}{tr((nativeModel?.metrics?.mcp?.servers ?? []).map((s: any) => s.name).join(', '))}.</p></Dialog>
|
||||
<input ref={files} type="file" multiple hidden accept="image/*,.pdf,.txt,.md,.markdown,.csv,.ts,.tsx,.js,.jsx,.py,.json,.css,.rs,.go,.sh,.html,.htm,.yaml,.yml,.toml,.xml,.log" onChange={e => { void addFiles(Array.from(e.target.files ?? [])); e.target.value = ''; }}/>
|
||||
<input ref={files} type="file" multiple hidden accept={FILE_ACCEPT} onChange={e => { void addFiles(Array.from(e.target.files ?? [])); e.target.value = ''; }}/>
|
||||
<input ref={folder} type="file" multiple hidden onChange={e => { void addFiles(Array.from(e.target.files ?? []), true); e.target.value = ''; }}/>
|
||||
<input ref={previewFile} type="file" hidden onChange={e => { void openFile(e.target.files?.[0]); e.target.value = ''; }}/>
|
||||
<button ref={trigger} className="composer-add" aria-label={tr("Add or open")} title={tr("Add or open")} disabled={reading || streaming} aria-haspopup="menu" aria-expanded={menu} onClick={() => { setPreferences(null); setMenu(v => !v); }}><Icon name="plus" size={22}/></button>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { parseDecimal, partialDecimal } from '../lib/decimal';
|
||||
|
||||
/** A number field that lets the person type a decimal point: it keeps the text while it is typed and tells the page the number once it is one (#3736). */
|
||||
export function DecimalInput({ value, onChange, ariaLabel, className = 'st-input', placeholder, disabled }: {
|
||||
value: number | null | undefined;
|
||||
onChange: (value: number | null) => void;
|
||||
ariaLabel: string;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const [text, setText] = useState(value === null || value === undefined ? '' : String(value));
|
||||
// A change that came from elsewhere (Restore saved, another tab) replaces the text, but never while the typed text already means this number.
|
||||
useEffect(() => {
|
||||
const shown = text === '' ? null : parseDecimal(text);
|
||||
if ((value ?? null) !== shown) setText(value === null || value === undefined ? '' : String(value));
|
||||
}, [value]);
|
||||
return <input className={className} aria-label={ariaLabel} inputMode="decimal" disabled={disabled} placeholder={placeholder} value={text}
|
||||
onChange={e => {
|
||||
const next = e.target.value;
|
||||
if (!partialDecimal(next)) return;
|
||||
setText(next);
|
||||
if (next.trim() === '') onChange(null);
|
||||
else { const n = parseDecimal(next); if (n !== null) onChange(n); }
|
||||
}}
|
||||
onBlur={() => { const n = text.trim() === '' ? null : parseDecimal(text); setText(n === null ? '' : String(n)); }}/>;
|
||||
}
|
||||
@@ -6,16 +6,7 @@ import { Icon } from './ui';
|
||||
|
||||
/** Files dropped on the chat go where the attach button would put them: the composer reads the 'strata:drop-files' event. */
|
||||
export const DROP_EVENT = 'strata:drop-files';
|
||||
/** The pictures on the clipboard, named so each one is told apart in the composer (a pasted screenshot arrives as "image.png"). */
|
||||
export function pastedFiles(data: DataTransfer | null | undefined): File[] {
|
||||
const out: File[] = [];
|
||||
for (const file of Array.from(data?.files ?? [])) {
|
||||
if (!file.type.startsWith('image/')) continue;
|
||||
const extension = file.type.split('/')[1]?.replace('jpeg', 'jpg').replace(/[+].*/, '') || 'png';
|
||||
out.push(new File([file], `Pasted picture ${new Date().toLocaleTimeString().replace(/[^0-9]+/g, '-').replace(/-$/, '')}${out.length ? `-${out.length + 1}` : ''}.${extension}`, { type: file.type }));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
export { pastedFiles } from '../lib/pasted-files';
|
||||
const hasFiles = (e: React.DragEvent) => Array.from(e.dataTransfer?.types ?? []).includes('Files');
|
||||
|
||||
/** A file dropped outside the chat must not take the window to that file: the page ignores it (called once at start). */
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import { tr } from '../lib/i18n';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Brand } from './Brand';
|
||||
import { Bar, Btn, Icon } from './ui';
|
||||
import { api } from '../lib/client';
|
||||
import { useDownloadEta } from '../lib/download-eta';
|
||||
import type { NativeJob } from '../lib/native';
|
||||
import { megabytes } from '../lib/format';
|
||||
|
||||
/** What the host says about this PC's engine (GET /api/engine/first-run). */
|
||||
export interface FirstRunInfo {
|
||||
status: 'offer' | 'none' | 'offline' | 'error' | 'installed' | 'wrong-make' | 'unavailable';
|
||||
gpus: { name: string; vramGb: number | null }[];
|
||||
driver?: string | null;
|
||||
offer?: { version: string; bytes: number; variant: string; backend: string | null };
|
||||
message?: string | null;
|
||||
reasons?: string[];
|
||||
details?: string;
|
||||
freeBytes?: number;
|
||||
}
|
||||
|
||||
export const mb = megabytes;
|
||||
|
||||
/**
|
||||
* Whether this PC still needs its engine. Reads only Control's local answer (no network): the engine is missing and the engine that came
|
||||
* with the app is not being unpacked. `null` while that is not known yet.
|
||||
*/
|
||||
export function useEngineNeeded(): boolean | null {
|
||||
const [needed, setNeeded] = useState<boolean | null>(null);
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
const ask = () => api.get<{ engine?: { usable?: boolean } | null; engineSetup?: boolean }>('/api/control/status').then(s => {
|
||||
if (!live) return;
|
||||
if (s.engineSetup) return setNeeded(null);
|
||||
setNeeded(!!s.engine && s.engine.usable === false);
|
||||
}, () => { if (live) setNeeded(false); });
|
||||
void ask();
|
||||
const timer = setInterval(() => void ask(), 3000);
|
||||
return () => { live = false; clearInterval(timer); };
|
||||
}, []);
|
||||
return needed;
|
||||
}
|
||||
|
||||
function gpuLine(info: FirstRunInfo) {
|
||||
// a processor's own tiny graphics (no memory of its own) is not "a card"
|
||||
const first = info.gpus.find(g => (g.vramGb ?? 0) >= 1) ?? info.gpus[0];
|
||||
if (!first) return '';
|
||||
const vram = first.vramGb ? `, ${Math.round(first.vramGb)} GB` : '';
|
||||
const driver = info.driver ? `, driver ${info.driver}` : '';
|
||||
return `${first.name}${vram}${driver}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The first-run card: the engine this PC needs, one button to download it, progress, and the offline way (choose a downloaded file).
|
||||
* Nothing is downloaded before a button is pressed.
|
||||
*/
|
||||
export function EngineFirstRun({ onDone }: { onDone: () => void }) {
|
||||
const [info, setInfo] = useState<FirstRunInfo | null>(null);
|
||||
const [job, setJob] = useState<NativeJob | null>(null);
|
||||
const [busy, setBusy] = useState(false), [problem, setProblem] = useState('');
|
||||
const [finished, setFinished] = useState(false);
|
||||
const file = useRef<HTMLInputElement>(null);
|
||||
const started = useRef(false);
|
||||
|
||||
const look = useCallback(() => {
|
||||
setInfo(null);
|
||||
setProblem('');
|
||||
api.get<FirstRunInfo>('/api/engine/first-run').then(setInfo, e => setInfo({ status: 'error', gpus: [], message: e instanceof Error ? e.message : String(e) }));
|
||||
}, []);
|
||||
useEffect(() => { look(); }, [look]);
|
||||
|
||||
// follow the download / install job
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
if (!started.current) return;
|
||||
api.get<{ job: NativeJob | null }>('/api/engine/job').then(r => {
|
||||
setJob(r.job);
|
||||
if (r.job?.state === 'done') setFinished(true);
|
||||
if (r.job && r.job.state !== 'running') started.current = false;
|
||||
}, () => { });
|
||||
}, 700);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
async function start() {
|
||||
setBusy(true);
|
||||
setProblem('');
|
||||
try {
|
||||
started.current = true;
|
||||
setJob(await api.post<NativeJob>('/api/native/operations', { kind: 'update-engine', args: {} }));
|
||||
}
|
||||
catch (e) {
|
||||
started.current = false;
|
||||
setProblem(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
async function chosen(files: FileList | null) {
|
||||
const picked = files?.[0];
|
||||
if (!picked) return;
|
||||
setBusy(true);
|
||||
setProblem('');
|
||||
try {
|
||||
const response = await fetch('/api/engine/from-file', { method: 'POST', headers: { 'x-file-name': picked.name }, body: picked });
|
||||
const body = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(body?.error ?? `The file could not be used (${response.status}).`);
|
||||
started.current = true;
|
||||
setJob(body as NativeJob);
|
||||
}
|
||||
catch (e) {
|
||||
setProblem(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
finally {
|
||||
setBusy(false);
|
||||
if (file.current) file.current.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
const running = job?.state === 'running';
|
||||
const failed = !!job && (job.state === 'error' || job.state === 'cancelled');
|
||||
const percent = Number(job?.result?.install?.download?.percent ?? 0);
|
||||
const bytes = info?.offer?.bytes ?? 0;
|
||||
const doneMb = Math.round(mb(bytes) * percent / 100);
|
||||
const eta = useDownloadEta(running && bytes ? { done_gb: bytes * percent / 100 / 1e9, remaining_gb: bytes * (100 - percent) / 100 / 1e9 } : undefined, job?.id);
|
||||
const cards = (info?.gpus ?? []).filter(g => (g.vramGb ?? 0) >= 1).length;
|
||||
const hidden = <input ref={file} type="file" accept=".gz,.tgz,.xz,.zip" hidden onChange={e => void chosen(e.target.files)}/>;
|
||||
const pick = (label: string) => <button type="button" className="onboard-continue-anyway" disabled={busy || running} onClick={() => file.current?.click()}>{tr(label)}</button>;
|
||||
|
||||
let body;
|
||||
if (finished) {
|
||||
body = <><div className="onboard-welcome-copy"><h1 tabIndex={-1} data-initial-focus>{tr("The engine is ready.")}</h1><p>{tr("Next: pick a model.")}</p></div>
|
||||
<footer className="onboard-footer onboard-footer--welcome"><Btn variant="primary" onClick={onDone}>{tr("Continue")}<Icon name="chevron" size={16}/></Btn></footer></>;
|
||||
}
|
||||
else if (running) {
|
||||
body = <><div className="onboard-welcome-copy"><h1 tabIndex={-1} data-initial-focus>{tr("Downloading the engine")}</h1>
|
||||
<p>{bytes && job?.result?.install?.download ? `${doneMb} of ${mb(bytes)} MB${eta ? ' · ' + eta : ''}` : tr(job?.summary ?? 'Starting…')}</p></div>
|
||||
<div className="onboard-error" style={{ maxWidth: 520, margin: '0 auto', width: '100%' }}><Bar value={percent} label="Downloading the engine"/><p className="small muted" style={{ textAlign: 'center' }}>{tr("You can close Strata; it continues where it stopped.")}</p></div></>;
|
||||
}
|
||||
else if (!info) {
|
||||
body = <div className="onboard-welcome-copy"><h1 tabIndex={-1} data-initial-focus>{tr("Welcome to Strata")}</h1><p role="status">{tr("Looking at your graphics card…")}</p></div>;
|
||||
}
|
||||
else {
|
||||
const card = gpuLine(info);
|
||||
const found = card ? <p>{tr("Strata found your graphics card:")}<br/><strong>{card}</strong>{cards > 1 ? <><br/><span className="small muted">{tr("{0} cards found. Strata uses the first one for the engine; change it later in Settings.", [cards])}</span></> : null}</p>
|
||||
: <p>{tr("Strata did not recognise a supported graphics card.")}</p>;
|
||||
let main, actions;
|
||||
if (info.status === 'offer' && info.offer) {
|
||||
main = <>{found}<p>{tr("To run models on it, Strata needs one download:")}<br/><strong>{tr("The engine for this card")}: {mb(info.offer.bytes)} MB</strong></p>
|
||||
{info.freeBytes !== undefined && info.freeBytes < info.offer.bytes * 2.5 ? <p className="small inline-error">{tr("The engine needs about {0} GB of free space on this drive and only {1} GB is free. Free some space, then continue.", [Math.ceil(info.offer.bytes * 2.5 / 1e9), (info.freeBytes / 1e9).toFixed(1)])}</p> : null}
|
||||
<p className="small muted">{tr("It comes from Strata’s own update server and is checked before it is used. Your models and chats are never touched by it.")}</p></>;
|
||||
actions = <><Btn variant="primary" disabled={busy} onClick={() => void start()}>{tr(failed ? 'Continue' : 'Download the engine')}</Btn>{pick("I already have the file…")}</>;
|
||||
}
|
||||
else if (info.status === 'offline') {
|
||||
main = <>{found}<p>{tr("Strata could not reach github.com. Check your connection and try again, or download the engine file on another PC and choose it here.")}</p></>;
|
||||
actions = <><Btn variant="primary" disabled={busy} onClick={look}>{tr("Try again")}</Btn>{pick("Choose a downloaded file…")}</>;
|
||||
}
|
||||
else if (info.status === 'wrong-make') {
|
||||
main = <>{found}<p>{info.message ?? tr("The engine installed here is for another make of graphics card.")}</p></>;
|
||||
actions = <><Btn variant="primary" disabled={busy} onClick={look}>{tr("Try again")}</Btn>{pick("Choose a downloaded file…")}</>;
|
||||
}
|
||||
else {
|
||||
main = <>{found}<p>{tr("Strata can’t pick an engine for this PC from its update list.")}</p>{info.message ? <p className="small muted">{info.message}</p> : null}{(info.reasons ?? []).length ? <ul className="small muted" style={{ textAlign: 'left', display: 'inline-block' }}>{(info.reasons ?? []).map(r => <li key={r}>{r}</li>)}</ul> : null}</>;
|
||||
actions = <><Btn variant="primary" disabled={busy} onClick={look}>{tr("Try again")}</Btn>{pick("Choose a downloaded file…")}<button type="button" className="onboard-continue-anyway" onClick={onDone}>{tr("Continue without an engine")}</button></>;
|
||||
}
|
||||
body = <><div className="onboard-welcome-copy"><h1 tabIndex={-1} data-initial-focus>{tr("Welcome to Strata")}</h1>{main}</div>
|
||||
{failed ? <div className="onboard-error" style={{ maxWidth: 560, margin: '0 auto' }}><p role="alert" className="small inline-error">{job?.error ?? job?.summary}</p><p className="small muted">{tr("The download stopped. Continue picks it up where it stopped.")}</p></div> : null}
|
||||
{problem ? <div className="onboard-error" style={{ maxWidth: 560, margin: '0 auto' }}><p role="alert" className="small inline-error">{problem}</p></div> : null}
|
||||
<footer className="onboard-footer onboard-footer--welcome">{actions}</footer></>;
|
||||
}
|
||||
return <div className="setup guided-onboarding" data-step="engine">{hidden}<header className="onboard-header"><Brand variant="lockup" height={22}/></header>{body}</div>;
|
||||
}
|
||||
@@ -22,6 +22,15 @@ type Choices = NonNullable<NonNullable<SetupState['choices']>['onboarding']>;
|
||||
function radioKeys(event: KeyboardEvent<HTMLDivElement>) { if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key))
|
||||
return; event.preventDefault(); const items = Array.from(event.currentTarget.querySelectorAll<HTMLButtonElement>('[role=radio]')); const current = items.indexOf(event.target as HTMLButtonElement); const next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 : (current + (['ArrowLeft', 'ArrowUp'].includes(event.key) ? -1 : 1) + items.length) % items.length; items[next]?.focus(); items[next]?.click(); }
|
||||
const steps = ['Welcome', 'Your model', 'Prepare', 'Make it yours'];
|
||||
/** What a person is told before choosing DeepSeek: the size, where it runs, how fast, what it needs. Every line is a warning or a fact; nothing here stops the download. */
|
||||
function DeepSeekNote({ version, plan }: { version: NonNullable<ReturnType<typeof nativeCatalogModels>[number]['sizes'][number]>; plan: Record<string, any> | null }) {
|
||||
void plan; // the plan's own disk line repeats what the version says in on_this_pc
|
||||
return <div className="engine-warning" role="status"><Icon name="info" size={18}/><div>
|
||||
<p>{tr("This is a very large model. The download is {0} GB and it needs about as much free disk space. It runs on one 12 GB graphics card with 46 to 64 GB of RAM, slowly: about 5 to 7 words a second. What does not fit in memory is read from your SSD, so an NVMe drive matters.", [Math.round(version.download_gb)])}</p>
|
||||
{version.on_this_pc ? <p className="small muted">{version.on_this_pc}</p> : null}
|
||||
<p className="small muted">{tr("You can stop the download and continue later; it picks up where it stopped. Strata warns you here but never blocks you.")}</p>
|
||||
</div></div>;
|
||||
}
|
||||
export function GuidedSetup() {
|
||||
const native = useNative(), setup = useStore(s => s.setup), hardware = useStore(s => s.hardware);
|
||||
const families = nativeCatalogModels(native?.catalog?.families ?? []);
|
||||
@@ -132,7 +141,8 @@ export function GuidedSetup() {
|
||||
setTuneJobId(tuneJob.id);
|
||||
void persist(4, { tuneJobId: tuneJob.id }).catch(e => setError(e.message));
|
||||
} }, [step, installJob?.id, tuneJob?.id]);
|
||||
const values = { guided: true, family: version?.sourceFamily, model: version?.model, context, vision: version?.images ? 'yes' : 'no', kv: 'int8', low_ram: 'auto', loadAfter: false, multiple: false, ...(directory ? { data_dir: directory } : {}) };
|
||||
const fixedContext = version?.multi_file ? 8192 : null; // the one context DeepSeek's engine was checked at
|
||||
const values = { guided: true, family: version?.sourceFamily, model: version?.model, context: fixedContext ?? context, vision: version?.images ? 'yes' : 'no', kv: 'int8', low_ram: 'auto', loadAfter: false, multiple: false, ...(directory ? { data_dir: directory } : {}) };
|
||||
async function review() {
|
||||
await perform(async () => {
|
||||
setInstallJobId('');
|
||||
@@ -154,7 +164,7 @@ export function GuidedSetup() {
|
||||
}
|
||||
async function tune() {
|
||||
await perform(async () => {
|
||||
const result = await nativeAction<NativeJob>('operations', { kind: 'tune', args: { guided: true, model: version?.id, profile, calibrate, vision: version?.images && vision ? 'yes' : 'no', multiple: false } });
|
||||
const result = await nativeAction<NativeJob>('operations', { kind: 'tune', args: { guided: true, model: version?.id, profile, calibrate: calibrate && !version?.multi_file, vision: version?.images && vision ? 'yes' : 'no', multiple: false } });
|
||||
setTuneJobId(result.id);
|
||||
await persist(4, { tuneJobId: result.id });
|
||||
});
|
||||
@@ -201,9 +211,10 @@ export function GuidedSetup() {
|
||||
<div className="onboard-stage-heading"><h1 ref={titleRef} tabIndex={-1} data-initial-focus>{tr(title)}</h1>{tr(step === 2 ? <p>{tr("You can always change your models later.")}</p> : step === 4 && tuneJob?.state === 'running' ? <p>{tr("We\u2019re measuring, applying your preference, and getting ready to chat.")}</p> : null)}</div>
|
||||
{tr(step === 2 ? <div className="onboard-model-stage">
|
||||
{tr(selectedFamily ? <>
|
||||
<div className="onboard-model-top"><div className="onboard-model-picker"><span className="onboard-control-label">{tr("Model")}</span><div><span className="onboard-model-symbol"><Icon name="models" size={23}/></span><Select ariaLabel={tr("Setup model")} value={selectedFamily.family} options={families.map(f => ({ value: f.family, label: f.title }))} onChange={value => chooseNative(value, '')}/></div></div><div className="onboard-context"><span className="onboard-control-label">{tr("Conversation context")}</span><Select ariaLabel={tr("Setup conversation context")} value={String(context)} options={(native?.catalog?.contexts ?? [32768]).map(n => ({ value: String(n), label: `${n / 1024}K` }))} onChange={value => setContext(Number(value))}/></div></div>
|
||||
<div className="onboard-model-top"><div className="onboard-model-picker"><span className="onboard-control-label">{tr("Model")}</span><div><span className="onboard-model-symbol"><Icon name="models" size={23}/></span><Select ariaLabel={tr("Setup model")} value={selectedFamily.family} options={families.map(f => ({ value: f.family, label: f.title }))} onChange={value => chooseNative(value, '')}/></div></div><div className="onboard-context"><span className="onboard-control-label">{tr("Conversation context")}</span><Select ariaLabel={tr("Setup conversation context")} value={String(version?.multi_file ? 8192 : context)} options={(version?.multi_file ? [8192] : native?.catalog?.contexts ?? [32768]).map(n => ({ value: String(n), label: `${n / 1024}K` }))} onChange={value => setContext(Number(value))}/></div></div>
|
||||
<OnboardingMemory hardware={hardware} native={native?.hardware}/>{tr(!version || version.needsNewerEngine ? <p className="small muted">{tr("This model needs a newer engine. Update your engine or import a local GGUF for this model.")}</p> : null)}
|
||||
<div className="onboard-version-heading"><strong>{tr("Choose a version")}</strong><span>{tr(version ? `${catalogVersionLabel(version)} selected` : '')}</span></div>
|
||||
{tr(version?.multi_file ? <DeepSeekNote version={version} plan={plan}/> : null)}
|
||||
<div className="onboard-versions" role="radiogroup" aria-label={tr("Model version")} onKeyDown={radioKeys}>{tr(cards.map(v => versionCard(v)))}</div>
|
||||
{tr(others.length ? <div className="onboard-more-versions"><VersionPicker versions={others} selectedId={version?.id ?? ''} hardware={hardware ? { ram_gb: native?.hardware?.ram_gb ?? hardware.memory.totalGb, vram_gb: Math.max(0, ...(native?.hardware?.gpus ?? []).filter(g => g.usable !== false).map(g => g.vram_gb)) } : null} downloaded={readyIds} showRecommended={false} moreLabel="More versions" onSelect={id => { setSize(id); setPlan(null); }} onOwnGguf={() => void ownModel()}/></div> : null)}
|
||||
<div className="onboard-folder"><Icon name="folder" size={20}/><div><strong>{tr(directory ? 'Your model folder' : 'Already downloaded the model?')}</strong><small>{tr(directory || "Choose the folder and we'll start your models with Strata.")}</small></div><Btn size="sm" onClick={() => setFolderOpen(v => !v)}>{tr(folderOpen ? 'Close' : directory ? 'Change' : 'Choose folder')}</Btn></div>
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
import { tr } from '../lib/i18n';
|
||||
import { copyText } from '../lib/clipboard';
|
||||
/** Safe, dependency-free rendering of the Markdown structures used in answers. */
|
||||
import { Fragment, type ReactNode } from 'react';
|
||||
import { Btn, IconBtn } from './ui';
|
||||
import { SyntaxCode } from './SyntaxCode';
|
||||
import { MathView } from './MathView';
|
||||
import { displayBlock, hasMath, splitMath } from '../lib/math';
|
||||
function inlineMarks(text: string, onOpenLink?: (url: string) => void): ReactNode {
|
||||
return text.split(/(`[^`]+`|\*\*[^*]+\*\*|\*[^*\n]+\*|\[[^\]]+\]\([^\s)]+\))/g).map((part, i) => {
|
||||
import { imagePlan } from '../lib/markdown-images';
|
||||
function inlineMarks(text: string, onOpenLink?: (url: string) => void, imageSession?: string | null): ReactNode {
|
||||
return text.split(/(!\[[^\]]*\]\([^\s)]+\)|`[^`]+`|\*\*[^*]+\*\*|\*[^*\n]+\*|\[[^\]]+\]\([^\s)]+\))/g).map((part, i) => {
|
||||
const image = part.match(/^!\[([^\]]*)\]\(([^\s)]+)\)$/);
|
||||
if (image) {
|
||||
const plan = imagePlan(image[2], imageSession);
|
||||
if (plan.kind === 'img')
|
||||
return <img key={i} src={plan.src} alt={image[1]} className="md-image" loading="lazy" referrerPolicy="no-referrer"/>;
|
||||
if (plan.kind === 'link')
|
||||
return <a key={i} href={plan.href} target="_blank" rel="noreferrer" onClick={e => { if (onOpenLink) {
|
||||
e.preventDefault();
|
||||
onOpenLink(plan.href);
|
||||
} }}>{image[1] || plan.href}</a>;
|
||||
return <Fragment key={i}>{image[1] || part}</Fragment>;
|
||||
}
|
||||
if (part.startsWith('`') && part.endsWith('`'))
|
||||
return <code key={i} className="md-code">{part.slice(1, -1)}</code>;
|
||||
if (part.startsWith('**') && part.endsWith('**'))
|
||||
@@ -23,21 +37,37 @@ function inlineMarks(text: string, onOpenLink?: (url: string) => void): ReactNod
|
||||
});
|
||||
}
|
||||
/** Text with its inline marks and, outside code spans, its math (#3268). Math is cut out first: a * or _ inside a formula is not emphasis. */
|
||||
function inline(text: string, onOpenLink?: (url: string) => void): ReactNode {
|
||||
function inline(text: string, onOpenLink?: (url: string) => void, imageSession?: string | null): ReactNode {
|
||||
if (!hasMath(text))
|
||||
return inlineMarks(text, onOpenLink);
|
||||
return inlineMarks(text, onOpenLink, imageSession);
|
||||
return text.split(/(`[^`]+`)/g).map((chunk, c) => {
|
||||
if (chunk.length > 1 && chunk.startsWith('`') && chunk.endsWith('`'))
|
||||
return <Fragment key={c}>{inlineMarks(chunk, onOpenLink)}</Fragment>;
|
||||
return <Fragment key={c}>{inlineMarks(chunk, onOpenLink, imageSession)}</Fragment>;
|
||||
return <Fragment key={c}>{splitMath(chunk).map((part, p) => part.kind === 'math'
|
||||
? <MathView key={p} tex={part.tex} display={part.display}/>
|
||||
: <Fragment key={p}>{inlineMarks(part.text, onOpenLink)}</Fragment>)}</Fragment>;
|
||||
: <Fragment key={p}>{inlineMarks(part.text, onOpenLink, imageSession)}</Fragment>)}</Fragment>;
|
||||
});
|
||||
}
|
||||
export function Markdown({ text, onOpenLink, onPreviewHtml }: {
|
||||
/** Does this line continue the paragraph above it, or does it start a block of its own? */
|
||||
export function continuesParagraph(lines: string[], at: number): boolean {
|
||||
const line = lines[at];
|
||||
if (!line.trim() || /^\s*```/.test(line) || /^(#{1,6})\s+(.+)$/.test(line) || /^\s*[-*+]\s+\S/.test(line) || /^\s*\d+[.)]\s+\S/.test(line) || /^>\s?/.test(line))
|
||||
return false;
|
||||
if (line.includes('|') && at + 1 < lines.length && /^\s*\|?\s*:?-{3,}/.test(lines[at + 1]))
|
||||
return false;
|
||||
return !displayBlock(lines, at);
|
||||
}
|
||||
/** Copy a code block. The toast store needs a browser, so it is loaded here, when someone copies: the renderer itself must render without one. */
|
||||
async function copyCode(text: string) {
|
||||
const { pushToast } = await import('../lib/store');
|
||||
await copyText(text, 'Code', { toast: pushToast });
|
||||
}
|
||||
export function Markdown({ text, onOpenLink, onPreviewHtml, imageSession }: {
|
||||
text: string;
|
||||
onOpenLink?: (url: string) => void;
|
||||
onPreviewHtml?: (html: string) => void;
|
||||
/** The chat whose folder a picture path in the answer is read from (the host serves only files inside it). */
|
||||
imageSession?: string | null;
|
||||
}) {
|
||||
const nodes: ReactNode[] = [];
|
||||
const lines = text.split('\n');
|
||||
@@ -54,7 +84,7 @@ export function Markdown({ text, onOpenLink, onPreviewHtml }: {
|
||||
while (i < lines.length && !/^\s*```/.test(lines[i]))
|
||||
body.push(lines[i++]);
|
||||
i++;
|
||||
nodes.push(<div className="st-code" key={key}><div className="st-code__head"><span>{language || tr('code')}</span>{tr(/^html$/i.test(language) && onPreviewHtml ? <Btn size="sm" onClick={() => onPreviewHtml(body.join('\n'))}>{tr("Open preview")}</Btn> : null)}<IconBtn name="copy" title={tr("Copy code")} onClick={() => void navigator.clipboard.writeText(body.join('\n'))}/></div><SyntaxCode text={body.join('\n')} language={language}/></div>);
|
||||
nodes.push(<div className="st-code" key={key}><div className="st-code__head"><span>{language || tr('code')}</span>{tr(/^html$/i.test(language) && onPreviewHtml ? <Btn size="sm" onClick={() => onPreviewHtml(body.join('\n'))}>{tr("Open preview")}</Btn> : null)}<IconBtn name="copy" title={tr("Copy code")} onClick={() => void copyCode(body.join('\n'))}/></div><SyntaxCode text={body.join('\n')} language={language}/></div>);
|
||||
continue;
|
||||
}
|
||||
const block = displayBlock(lines, i);
|
||||
@@ -66,7 +96,7 @@ export function Markdown({ text, onOpenLink, onPreviewHtml }: {
|
||||
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
||||
if (heading) {
|
||||
const Tag = heading[1].length <= 2 ? 'h2' : heading[1].length === 3 ? 'h3' : 'h4';
|
||||
nodes.push(<Tag key={i}>{tr(inline(heading[2], onOpenLink))}</Tag>);
|
||||
nodes.push(<Tag key={i}>{tr(inline(heading[2], onOpenLink, imageSession))}</Tag>);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
@@ -90,7 +120,7 @@ export function Markdown({ text, onOpenLink, onPreviewHtml }: {
|
||||
i += 2;
|
||||
while (i < lines.length && lines[i].includes('|'))
|
||||
rows.push(cells(lines[i++]));
|
||||
nodes.push(<div className="md-table-wrap" key={key}><table className="md-table"><thead><tr>{tr(header.map((c, j) => <th key={j}>{tr(inline(c, onOpenLink))}</th>))}</tr></thead><tbody>{tr(rows.map((r, j) => <tr key={j}>{tr(r.map((c, k) => <td key={k}>{tr(inline(c, onOpenLink))}</td>))}</tr>))}</tbody></table></div>);
|
||||
nodes.push(<div className="md-table-wrap" key={key}><table className="md-table"><thead><tr>{tr(header.map((c, j) => <th key={j}>{tr(inline(c, onOpenLink, imageSession))}</th>))}</tr></thead><tbody>{tr(rows.map((r, j) => <tr key={j}>{tr(r.map((c, k) => <td key={k}>{tr(inline(c, onOpenLink, imageSession))}</td>))}</tr>))}</tbody></table></div>);
|
||||
continue;
|
||||
}
|
||||
if (/^>\s?/.test(line)) {
|
||||
@@ -98,8 +128,19 @@ export function Markdown({ text, onOpenLink, onPreviewHtml }: {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
nodes.push(<p key={i}>{tr(inline(line, onOpenLink))}</p>);
|
||||
// One paragraph: a single newline inside it is a soft break (a space); two spaces or a backslash at the end of a line is a hard break (#3818).
|
||||
const key = i, parts: ReactNode[] = [];
|
||||
let current = line;
|
||||
i++;
|
||||
while (true) {
|
||||
const hard = /( {2,}|\\)$/.test(current);
|
||||
parts.push(<Fragment key={parts.length}>{tr(inline(current.replace(/( {2,}|\\)$/, ''), onOpenLink, imageSession))}</Fragment>);
|
||||
if (i >= lines.length || !continuesParagraph(lines, i))
|
||||
break;
|
||||
parts.push(hard ? <br key={`b${parts.length}`}/> : ' ');
|
||||
current = lines[i++].trimStart();
|
||||
}
|
||||
nodes.push(<p key={key}>{tr(parts)}</p>);
|
||||
}
|
||||
return <>{tr(nodes)}</>;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { tr } from '../lib/i18n';
|
||||
import { useState } from 'react';
|
||||
import { Btn, Dialog, ErrorNotice, IconBtn, Select } from './ui';
|
||||
import { MCP_NAME_HELP, validMcpName } from '../lib/mcp-names';
|
||||
export function NativeMcpEditor({ text, onChange }: {
|
||||
text: string;
|
||||
onChange: (text: string) => void;
|
||||
@@ -19,8 +20,8 @@ export function NativeMcpEditor({ text, onChange }: {
|
||||
function open(id: string) { const server = servers[id] ?? {}; setEditing(id); setName(id); setTransport(server.url ? 'url' : 'command'); setAddress(server.url ?? server.command ?? ''); setArgs((server.args ?? []).join('\n')); setAdvanced(JSON.stringify(Object.fromEntries(Object.entries(server).filter(([k]) => !['url', 'command', 'args', 'disabled'].includes(k))), null, 2)); setError(''); }
|
||||
function save() {
|
||||
try {
|
||||
if (!/^[a-z0-9_-]{1,64}$/i.test(name))
|
||||
throw new Error('Use a simple server name: letters, numbers, hyphens or underscores.');
|
||||
if (!validMcpName(name))
|
||||
throw new Error(MCP_NAME_HELP);
|
||||
if (!address.trim())
|
||||
throw new Error('Enter the server URL or program.');
|
||||
if (name !== editing && servers[name])
|
||||
|
||||
@@ -20,6 +20,7 @@ import { go, setSettingsSection, selectRemoteModel, pushToast } from '../lib/sto
|
||||
import { nativeAction, useNative, type NativeJob } from '../lib/native';
|
||||
import { tuneEffective } from '../lib/native-tune';
|
||||
import { tunedFields } from '../lib/tune-values';
|
||||
import { draftingChoices as draftingOptions } from '../lib/drafting';
|
||||
import { NativeSettings } from './NativeSettings';
|
||||
import { NativeSettingsFile } from './NativeSettingsFile';
|
||||
import { RunImageModel } from './RunImageModel';
|
||||
@@ -41,6 +42,7 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
const manual = useStore(s => s.setup?.choices?.onboarding?.manual ?? false);
|
||||
const [expert, setExpert] = useKept('models.expert', manual), [multiple, setMultiple] = useKept('models.multiple', false), [roles, setRoles] = useKept<Roles | null>('models.roles', null), [kv, setKv] = useKept('models.kv', 'int8'), [lowRam, setLowRam] = useKept('models.lowRam', 'auto'), [split, setSplit] = useKept('models.split', 'auto');
|
||||
const [resourceProfile, setResourceProfile] = useKept<'work' | 'performance'>('models.profile', 'work');
|
||||
const [drafting, setDrafting] = useState('auto');
|
||||
const [pending, setPending] = useState(false), [error, setError] = useState(''), [plan, setPlan] = useState<Record<string, any> | null>(null), [loadedError, setLoadedError] = useState(''), [tuneOpen, setTuneOpen] = useState(false), [jobId, setJobId] = useKept('models.job', ''), [manage, setManage] = useState(false);
|
||||
const [toastedJob, setToastedJob] = useKept('models.toasted', ''), [appliedSig, setAppliedSig] = useKept('models.applied', '');
|
||||
const presetPending = useRef(false);
|
||||
@@ -99,8 +101,11 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
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)) };
|
||||
// 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]);
|
||||
// Drafting (which drafter guesses ahead): DFlash2 is for Qwen3.8-Flash-Next only (the qwen and gyro families), so no other model is offered it.
|
||||
const draftingChoices = selectedSize ? draftingOptions(selectedSize.sourceFamily ?? selectedFamily?.family, native?.capabilities?.dflash2) : null;
|
||||
const draftingWords: Record<string, string> = { auto: 'Auto (recommended)', mtp: 'MTP', dflash2: 'DFlash2', off: 'Off' };
|
||||
const values = useMemo(() => ({ family: selectedSize?.sourceFamily, model: selectedSize?.model, context, vision: selectedSize?.images ? vision : 'no', kv, low_ram: lowRam, resourceProfile, ...(cardsGpus.length > 1 ? { gpus: cardsGpus.join(',') } : cardsGpus.length === 1 ? { gpu: cardsGpus[0] } : {}), loadAfter: true, multiple: expert && multiple }), [selectedSize?.sourceFamily, selectedSize?.model, context, vision, kv, lowRam, resourceProfile, cardsGpus.join(','), expert, multiple]);
|
||||
const downloadValues = { ...values, vision: selectedSize?.images && vision !== 'no' ? 'yes' : 'no', loadVision: selectedSize?.images ? vision : 'no' };
|
||||
const downloadValues = { ...values, ...(draftingChoices ? { drafting } : {}), vision: selectedSize?.images && vision !== 'no' ? 'yes' : 'no', loadVision: selectedSize?.images ? vision : 'no' };
|
||||
useEffect(() => { if (!native?.catalog || family)
|
||||
return; const current = native.instances.find(i => i.id === native.defaultId); const rec = native.catalog.recommendation ?? { family: null, model: null }; const loadedFamily = families.find(f => f.sizes.some(s => s.id === current?.model)); const have = new Set((native.installed?.models ?? []).filter(m => m.ready).map(m => m.model)); const installedFamily = families.find(f => f.sizes.some(s => have.has(s.id))); const f = loadedFamily ?? installedFamily ?? families.find(f => f.family === rec.family || f.sizes.some(s => s.sourceFamily === rec.family)) ?? families[0]; if (f) {
|
||||
setFamily(f.family);
|
||||
@@ -117,7 +122,7 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
}
|
||||
if (!selectedSize)
|
||||
return;
|
||||
const sig = installed ? ['have', installed.model, installed.ready, installed.context, installed.kv, installed.images, installed.visionEnabled, installed.visionDevice, installed.lowRamMode, installed.resourceProfile].join('|') : `new|${selectedSize.id}`;
|
||||
const sig = installed ? ['have', installed.model, installed.ready, installed.context, installed.kv, installed.images, installed.visionEnabled, installed.visionDevice, installed.lowRamMode, installed.drafting, installed.resourceProfile].join('|') : `new|${selectedSize.id}`;
|
||||
if (sig === appliedSig)
|
||||
return;
|
||||
setAppliedSig(sig);
|
||||
@@ -128,6 +133,7 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
setVision(installed.visionEnabled === false ? 'no' : installed.visionDevice ?? (installed.images && installed.images !== 'off' ? installed.images : 'yes'));
|
||||
setKv(installed.kv ?? 'int8');
|
||||
setLowRam(installed.lowRamMode ?? 'auto');
|
||||
setDrafting(installed.drafting ?? 'auto');
|
||||
// A model never tuned has no saved choice: keep the page's own (Leave 20%) rather than flipping to Full performance.
|
||||
if (installed.resourceProfile && !loadedSelected?.resourceProfile)
|
||||
setResourceProfile(installed.resourceProfile);
|
||||
@@ -151,7 +157,7 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
} }
|
||||
async function load() { if (!selectedSize)
|
||||
return; await perform(async () => { if (installed?.ready && context === installed.context && (!selectedSize?.images || vision === 'no' || installed.images && installed.images !== 'off') && (!installed.kv || kv === installed.kv || kv === tunedNow?.fields.kv) && (lowRam === (installed.lowRamMode ?? 'auto') || lowRam === tunedNow?.fields.lowRam)) {
|
||||
const result = await nativeAction<NativeJob>('operations', { kind: 'load', args: { model: selectedSize.id, resourceProfile, vision: selectedSize?.images ? vision : 'no', multiple: expert && multiple, ...(roleCards.length > 1 ? (roles && customRoles ? { gpuRoles: effectiveRoles, layerSplit: expert && cardsGpus.length > 1 ? split : 'auto' } : cardsGpus.length ? { gpus: cardsGpus, gpusFromRecommendation: true, layerSplit: expert && cardsGpus.length > 1 ? split : 'auto' } : {}) : {}), values: {} } });
|
||||
const result = await nativeAction<NativeJob>('operations', { kind: 'load', args: { model: selectedSize.id, resourceProfile, vision: selectedSize?.images ? vision : 'no', multiple: expert && multiple, ...(draftingChoices ? { values: { drafting } } : {}), ...(roleCards.length > 1 ? (roles && customRoles ? { gpuRoles: effectiveRoles, layerSplit: expert && cardsGpus.length > 1 ? split : 'auto' } : cardsGpus.length ? { gpus: cardsGpus, gpusFromRecommendation: true, layerSplit: expert && cardsGpus.length > 1 ? split : 'auto' } : {}) : {}) } });
|
||||
setJobId(result.id);
|
||||
}
|
||||
else
|
||||
@@ -179,8 +185,10 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
{tr(noFit ? <p className="small muted native-spill-note"><Icon name="disk" size={16}/>{tr("May run slower \u00B7 Model data can be read from SSD.")}</p> : null)}
|
||||
<div className="native-choice__row">
|
||||
<div className="native-choice__col"><span className="native-choice__label">{tr("Conversation context")}</span><Select ariaLabel={tr("Conversation context")} value={String(context)} options={contextChoices(native?.catalog?.contexts ?? [8192, 32768, 65536, 131072, 262144], context).map(n => ({ value: String(n), label: n % 1024 === 0 ? `${n / 1024}K` : `${n.toLocaleString()} tokens` }))} onChange={n => setContext(Number(n))}/></div>
|
||||
{tr(draftingChoices ? <div className="native-choice__col"><span className="native-choice__label">{tr("Drafting")}</span><Select ariaLabel={tr("Drafting")} value={drafting} disabled={!!active} options={draftingChoices.map(v => ({ value: v, label: tr(draftingWords[v]) }))} onChange={v => setDrafting(String(v))}/></div> : null)}
|
||||
<div className="native-choice__col"><span className="native-choice__label">{tr("Computer use")}</span><div className="native-resource-choice"><button aria-pressed={resourceProfile === 'work'} data-selected={resourceProfile === 'work'} onClick={() => setResourceProfile('work')}><Icon name="computer" size={20}/><span><strong>{tr("Leave 20% for my work")}</strong><small>{tr("Headroom for your other apps")}</small></span>{tr(resourceProfile === 'work' ? <Icon name="check" size={15}/> : null)}</button><button aria-pressed={resourceProfile === 'performance'} data-selected={resourceProfile === 'performance'} onClick={() => setResourceProfile('performance')}><Icon name="bolt" size={20}/><span><strong>{tr("Full performance")}</strong><small>{tr("Give Strata priority")}</small></span>{tr(resourceProfile === 'performance' ? <Icon name="check" size={15}/> : null)}</button></div></div>
|
||||
</div>
|
||||
{tr(loadedSelected?.metrics?.engine?.drafting_note ? <p className="small muted native-spill-note">{tr(String(loadedSelected.metrics.engine.drafting_note))}</p> : null)}
|
||||
{roleCards.length > 1 ? <GpuRoles cards={roleCards} roles={effectiveRoles} custom={customRoles} recommendedRoles={recommended.roles} vendor={engineVendor} disabled={!!active} onChange={changeRoles} onBack={() => setRoles(null)} memoryGb={selectedSize ? memoryNeeded(selectedSize) || null : null} ramGb={pcRam} spillGb={selectedSize ? fitOn(selectedSize, fitHardware).spillGb : 0} usedBy={i => usedBy([i])}/> : null}
|
||||
{tr(roleCards.length > 1 && (native?.hardware?.gpus ?? []).length > roleCards.length ? <p className="small muted native-cards__note">{tr('Not used: {0} (another make of graphics card than this engine drives).', [(native?.hardware?.gpus ?? []).filter(g => !roleCards.some(r => r.index === g.index && r.vendor === g.vendor)).map(g => g.name).join(', ')])}</p> : null)}
|
||||
{tr(selectedSize?.imagesCapable && !selectedSize.images ? <p className="small muted native-spill-note">{tr("This model can read pictures, but the engine installed here does not include the picture reader yet, so Read pictures is off. Update the engine in Settings → About to turn it on.")}</p> : null)}
|
||||
@@ -196,10 +204,10 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
{tr(expert && IMAGE_GENERATION_VISIBLE ? <><details className="native-generation"><summary>{tr("Image generation ")}<span className="muted">{tr("Optional")}</span></summary><RunImageModel library={imageLibrary} disabled={pending || !!active} onChange={() => { }}/></details></> : null)}
|
||||
{tr(expert ? <section id="native-expert-settings" ref={expertSection} className="st-card native-execution"><h2 className="h4">{tr("Expert settings")}</h2>{tr(tunedNow ? <p className="small muted native-tuned-note">{tr("Showing the values of your tuning '{0}'.", [tunedNow.name])}</p> : null)}<label className="native-toggle"><span><strong>{tr("Run multiple models")}</strong><small>{tr("Loading another model keeps the others loaded.")}</small></span><button className="st-toggle" role="switch" aria-label={tr("Run multiple models")} aria-checked={multiple} onClick={() => setMultiple(v => !v)}/></label>{tr(cardsGpus.length > 1 ? <label className="input-field"><span>{tr("Layer split")}</span><input className="st-input" value={split} placeholder={tr("auto or 16,32")} onChange={e => setSplit(e.target.value)}/></label> : null)}<div className="native-field-grid"><label className="input-field"><span>{tr("Context memory")}</span><Select ariaLabel={tr("Native KV format")} value={kv} options={[{ value: 'int8', label: 'INT8' }, { value: 'q4_0', label: 'Q4' }, { value: 'k8v4', label: 'K8 / V4' }]} onChange={setKv}/></label><label className="input-field"><span>{tr("Low-RAM mode")}</span><Select ariaLabel={tr("Native low RAM mode")} value={lowRam} options={['auto', 'resident', 'mmap'].map(v => ({ value: v, label: v === 'auto' ? 'Automatic' : v === 'resident' ? 'Keep experts in RAM' : 'Read experts from disk' }))} onChange={setLowRam}/></label></div>{tr(installed ? <><NativeSettingsFile model={installed.model}/><NativeSettings model={installed.model}/></> : <p className="small muted">{tr("More engine parameters become available after preparing this version.")}</p>)}</section> : null)}
|
||||
</div>
|
||||
<aside className="stack stack--lg"><PcFitCard gpuName={pcGpu?.name} ramGb={pcRam} vramGb={pcVram} inUse={keptModels.length ? { ramGb: Math.round(keptRam), vramGb: Math.round(keptVramOn(chosenCards)) } : null} fit={selectedSize ? fitOn(selectedSize, fitHardware) : null}/><section className="st-card"><h2 className="h4">{tr("Model files")}</h2><p className="small muted">{tr("Strata prepares all shards, the draft layer and shared files. Completed downloads are reused.")}</p><code className="native-path">{installed?.dataFolder ?? native?.downloadDirectory ?? native?.installed?.data_folder ?? 'Choose an installation'}</code><Btn size="sm" variant="ghost" onClick={() => { setSettingsSection('Storage'); go('settings'); }}>{tr("Storage settings")}</Btn></section><Btn size="sm" variant="ghost" onClick={onOtherModels}>{tr("Other local models\u2026")}</Btn></aside>
|
||||
<aside className="stack stack--lg"><PcFitCard gpuName={pcGpu?.name} ramGb={pcRam} vramGb={pcVram} inUse={keptModels.length ? { ramGb: Math.round(keptRam), vramGb: Math.round(keptVramOn(chosenCards)) } : null} fit={selectedSize ? fitOn(selectedSize, fitHardware, true, { context, kv }) : null}/><section className="st-card"><h2 className="h4">{tr("Model files")}</h2><p className="small muted">{tr("Strata prepares all shards, the draft layer and shared files. Completed downloads are reused.")}</p><code className="native-path">{installed?.dataFolder ?? native?.downloadDirectory ?? native?.installed?.data_folder ?? 'Choose an installation'}</code><Btn size="sm" variant="ghost" onClick={() => { setSettingsSection('Storage'); go('settings'); }}>{tr("Storage settings")}</Btn></section><Btn size="sm" variant="ghost" onClick={onOtherModels}>{tr("Other local models\u2026")}</Btn></aside>
|
||||
</div>
|
||||
|
||||
<Dialog open={!!plan} title={tr(installed?.ready ? "Apply settings and load" : "Download and prepare")} compact onClose={() => !pending && setPlan(null)} actions={<><Btn disabled={pending} onClick={() => setPlan(null)}>{tr("Cancel")}</Btn><Btn variant="primary" disabled={pending || !!plan?.plan?.disk_short} onClick={() => void download()}>{tr(installed?.ready ? "Apply & load" : "Download & load")}</Btn></>}><p><strong>{tr(plan?.plan?.title ?? selectedFamily?.title)}</strong>{tr(installed?.ready ? null : <>{tr(" \u00B7 approximately ")}{tr(plan?.plan?.download_gb ?? selectedSize?.download_gb)}{tr(" GB")}</>)}</p>{tr(installed?.ready ? <p className="small muted">{tr("The model files are already on this computer. Nothing will be downloaded; only the settings you chose are applied.")}</p> : null)}{tr(plan?.plan?.disk_short ? <ErrorNotice message={plan.plan.disk_short}/> : null)}{tr(installed?.ready ? <p className="small muted">{tr("Any currently loaded native models will be unloaded first.")}</p> : <><p className="small muted">{tr("Existing files are checked and reused. Any currently loaded native models will be unloaded before preparing files.")}</p><p className="small muted">{tr("Your computer may feel slower while weights load. You can stop the download and resume later.")}</p></>)}{tr(selectedSize?.images ? <p className="small muted">{tr("Vision encoder files are included. Picture reading loads only when enabled.")}</p> : null)}</Dialog>
|
||||
<Dialog open={!!plan} title={tr(installed?.ready ? "Apply settings and load" : "Download and prepare")} compact onClose={() => !pending && setPlan(null)} actions={<><Btn disabled={pending} onClick={() => setPlan(null)}>{tr("Cancel")}</Btn><Btn variant="primary" disabled={pending || !!plan?.plan?.disk_short} onClick={() => void download()}>{tr(installed?.ready ? "Apply & load" : "Download & load")}</Btn></>}><p><strong>{tr(plan?.plan?.title ?? selectedFamily?.title)}</strong>{tr(installed?.ready ? null : <>{tr(" \u00B7 approximately ")}{tr(plan?.plan?.download_gb ?? selectedSize?.download_gb)}{tr(" GB")}</>)}</p>{tr(plan?.drafting?.needed ? <p className="small muted">{tr("Choosing DFlash2 also downloads its drafter: about {0} GB, once. If it does not fit in graphics memory, the model uses the MTP instead.", [plan.drafting.gb])}</p> : null)}{tr(installed?.ready ? <p className="small muted">{tr("The model files are already on this computer. Nothing will be downloaded; only the settings you chose are applied.")}</p> : null)}{tr(plan?.plan?.disk_short ? <ErrorNotice message={plan.plan.disk_short}/> : null)}{tr(installed?.ready ? <p className="small muted">{tr("Any currently loaded native models will be unloaded first.")}</p> : <><p className="small muted">{tr("Existing files are checked and reused. Any currently loaded native models will be unloaded before preparing files.")}</p><p className="small muted">{tr("Your computer may feel slower while weights load. You can stop the download and resume later.")}</p></>)}{tr(selectedSize?.images ? <p className="small muted">{tr("Vision encoder files are included. Picture reading loads only when enabled.")}</p> : null)}</Dialog>
|
||||
<NativeDeleteDialog id={deleting} onClose={() => setDeleting(null)}/>
|
||||
<Dialog open={manage} title={tr("Manage models")} onClose={() => setManage(false)} actions={<Btn onClick={() => setManage(false)}>{tr("Done")}</Btn>}><div className="native-manage">
|
||||
{tr(downloaded.length ? <div className="native-manage-list" role="list">{tr(downloaded.map(m => { const v = allSizes.find(x => x.id === m.model), loaded = native?.instances.some(i => i.model === m.model && i.status === 'ready'); return <div className="native-manage-row" role="listitem" key={m.model} data-selected={selectedSize?.id === m.model || undefined}>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { fetchUpdateCheck, publishUpdateCheck, useInstalledVersions, useLastUpda
|
||||
import { nativeAction, useNative, type NativeJob } from '../lib/native';
|
||||
import { Btn, Dialog, ErrorNotice, Select } from './ui';
|
||||
import { NativeProgress } from './NativeModels';
|
||||
import { jobErrorText } from '../lib/native-job-diagnostics';
|
||||
export function NativeBenchmark({ open, onClose }: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@@ -114,7 +115,7 @@ export function NativeOperations() {
|
||||
}
|
||||
return <div className="stack stack--lg native-engine-settings">
|
||||
<section className="st-card"><div className="section-toolbar"><h2 className="h3">{tr("Update component")}</h2>{tr(update === 'available' ? <Btn size="sm" variant="primary" disabled={!!active || (native?.instances.length ?? 0) > 0} onClick={() => setConfirm(true)}>{tr("Update available")}</Btn> : null)}{tr(update !== 'available' ? <Btn size="sm" disabled={update === 'checking'} onClick={() => void checkForUpdates()}>{tr(update === 'checking' ? 'Checking…' : 'Check for updates')}</Btn> : null)}</div><p className="small muted">{tr("Update the engine for this computer. Your models and settings are kept.")}</p>{tr(update === 'current' ? <p className="small muted">{tr("Up to date")}</p> : null)}{tr(checkError ? <p className="small inline-error" role="alert">{tr(checkError)}</p> : null)}{tr(update === 'available' && (native?.instances.length ?? 0) > 0 ? <p className="small muted">{tr("Open Models and unload the running native models first.")}</p> : null)}</section>
|
||||
<section className="st-card"><div className="section-toolbar"><h2 className="h3">{tr("Diagnostics")}</h2><Btn size="sm" onClick={() => setShowLogs(true)}>{tr("View logs")}</Btn></div><p className="small muted">{tr("Downloads, preparation and engine failures include their original messages here.")}</p>{tr(native?.jobs.filter(j => j.state === 'error').slice(-3).map(j => <ErrorNotice key={j.id} message={j.error ?? j.summary} detail={j.details}/>))}</section>
|
||||
<section className="st-card"><div className="section-toolbar"><h2 className="h3">{tr("Diagnostics")}</h2><Btn size="sm" onClick={() => setShowLogs(true)}>{tr("View logs")}</Btn></div><p className="small muted">{tr("Downloads, preparation and engine failures include their original messages here.")}</p>{tr(native?.jobs.filter(j => j.state === 'error').slice(-3).map(j => <ErrorNotice key={j.id} message={jobErrorText(j, native.jobs)} detail={j.details}/>))}</section>
|
||||
<Dialog open={showLogs} wide title={tr("Engine logs")} onClose={() => setShowLogs(false)} actions={<><Btn icon="download" disabled={saving} onClick={() => void saveLogs()}>{tr(saving ? 'Working…' : 'Save logs')}</Btn><Btn variant="primary" onClick={() => setShowLogs(false)}>{tr("Close")}</Btn></>}>
|
||||
<pre className="native-log" aria-label={tr("Engine log")}>{logs.length ? logs.map(l => `${l.at} · ${l.line}`).join(String.fromCharCode(10)) : tr('No engine messages yet.')}</pre>
|
||||
{tr(saved ? <p className="small" role="status">{tr("Saved to {0}", [saved])}</p> : null)}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DecimalInput } from './DecimalInput';
|
||||
import { tr } from '../lib/i18n';
|
||||
import {nativeSettingsResponse,nativeSettingsValues} from '../lib/native-settings-response';
|
||||
import { describeChange, type TunedChange } from '../lib/tune-values';
|
||||
@@ -5,12 +6,15 @@ import { useEffect, useState } from 'react';
|
||||
import { api } from '../lib/client';
|
||||
import { refreshNative, useNative, type NativeField } from '../lib/native';
|
||||
import { Btn, ErrorNotice, Select } from './ui';
|
||||
import { speedOptions } from '../lib/deepseek-speed';
|
||||
import type { RunCard } from '../lib/native-fit';
|
||||
import { NativeMcpEditor } from './NativeMcpEditor';
|
||||
export function NativeSettings({ model }: {
|
||||
model: string;
|
||||
}) {
|
||||
const native = useNative();
|
||||
const [revision,setRevision]=useState(0);
|
||||
const [dsExperts, setDsExperts] = useState(false); // the installed DeepSeek engine knows --experts (0.1.50 or newer)
|
||||
const [tuned, setTuned] = useState<{ preset: { name: string } | null; changed: TunedChange[] } | null>(null);
|
||||
const [values, setValues] = useState<Record<string, any>>({}), [mcp, setMcp] = useState('{}'), [original, setOriginal] = useState(''), [text, setText] = useState(''), [tab, setTab] = useState('fields'), [pending, setPending] = useState(false), [error, setError] = useState(''), [notice, setNotice] = useState(''), [query, setQuery] = useState('');
|
||||
useEffect(() => {
|
||||
@@ -25,9 +29,11 @@ export function NativeSettings({ model }: {
|
||||
values: Record<string, any>;
|
||||
mcp: Record<string, any>;
|
||||
tuned?: { preset: { name: string } | null; changed: TunedChange[] } | null;
|
||||
capabilities?: { dsExperts?: boolean };
|
||||
}>(`/api/native/models/${model}/settings`).then(input => {
|
||||
if (!active)
|
||||
return;
|
||||
setDsExperts(!!input.capabilities?.dsExperts);
|
||||
setTuned(input.tuned ?? null);
|
||||
const value = nativeSettingsResponse(input);
|
||||
setValues(value.values);
|
||||
@@ -90,16 +96,9 @@ export function NativeSettings({ model }: {
|
||||
<summary>{tr('Native engine settings')}</summary>
|
||||
{error ? <><ErrorNotice message={tr('Couldn’t load model settings.')} next={tr('Check that the engine is connected and this model is installed, then try again.')} onRetry={() => setRevision(value => value + 1)}/><details className="native-settings-diagnostic"><summary className="small">{tr('Details')}</summary><p className="small muted">{error}</p></details></> : <p className="small muted" role="status">{tr('Reading saved settings…')}</p>}
|
||||
</details>;
|
||||
const rows = (native?.fields ?? []).filter(f => `${tr(f.label)} ${f.id} ${tr(f.group)}`.toLowerCase().includes(query.toLowerCase()));
|
||||
const rows = (native?.fields ?? []).filter(f => (!f.forModel || (model.toLowerCase().includes(f.forModel) && (f.id !== 'ds_experts' || dsExperts))) && `${tr(f.label)} ${f.id} ${tr(f.group)}`.toLowerCase().includes(query.toLowerCase()));
|
||||
return <details className="native-settings"><summary>{tr("Native engine settings")}</summary>{tr(tuned?.changed?.length ? <div className="native-tuned"><p className="small"><strong>{tr("Your tuning '{0}' sets", [tuned.preset?.name ?? ''])}</strong></p><ul>{tuned.changed.map(c => { const d = describeChange(c); return <li key={c.flag}><span>{d.name}</span><b>{d.now || tr('off')}</b>{d.was !== null ? <small className="muted">{tr("was {0}", [d.was])}</small> : null}</li>; })}</ul></div> : null)}<p className="small muted">{tr(!original ? 'Reading saved settings… ' : '')}{tr("Settings for this model. Blank values use Strata\u2019s defaults.")}</p><div className="editor-tabs" role="tablist" aria-label={tr("Native settings editor")}>{tr(['fields', 'json', 'tools'].map(t => <button key={t} role="tab" aria-selected={tab === t} onClick={() => chooseTab(t)}>{tr(t === 'fields' ? 'Parameters' : t === 'json' ? 'JSON' : 'MCP tools')}</button>))}</div>
|
||||
{tr(tab === 'fields' ? <><input className="st-input" type="search" aria-label={tr("Search native parameters")} placeholder={tr("Search parameters…")} value={query} onChange={e => setQuery(e.target.value)}/>{tr(['Responses', 'GPU & memory', 'Serving', 'Image reading', 'Tools', 'Recovery'].map(group => rows.some(f => f.group === group) ? <section className="native-settings-group" key={group}><h3 className="h4">{tr(group)}</h3>{tr(rows.filter(f => f.group === group).map(f => <label className="native-setting-row" key={f.id}><span><strong>{tr(f.label)}</strong>{tr(f.supported === false ? <small>{tr("Requires a newer Strata installation.")}</small> : f.hint ? <small>{tr(f.hint)}</small> : null)}</span><NativeFieldInput field={f} value={values[f.id]} onChange={v => change(f.id, v)}/></label>))}</section> : null))}<section className="native-settings-group"><h3 className="h4">{tr("Sampling")}</h3>{tr([['temperature', 'Temperature'], ['top_p', 'Top-p'], ['top_k', 'Top-k'], ['min_p', 'Min-p'], ['repetition_penalty', 'Repetition penalty'], ['presence_penalty', 'Presence penalty'], ['frequency_penalty', 'Frequency penalty'], ['penalty_last_n', 'Penalty window'], ['seed', 'Seed']].map(([id, label]) => <label className="native-setting-row" key={id}><span><strong>{tr(label)}</strong></span><input className="st-input" aria-label={tr("Native {0}", [label])} inputMode="decimal" value={values.sampling?.[id] ?? ''} onChange={e => {
|
||||
const sampling = { ...values.sampling };
|
||||
if (e.target.value === '')
|
||||
delete sampling[id];
|
||||
else
|
||||
sampling[id] = Number(e.target.value);
|
||||
change('sampling', sampling);
|
||||
}}/></label>))}</section></> : tab === 'json' ? <><p className="small muted">{tr("Engine arguments and STRATA environment flags are editable here. Program paths and network access remain managed by the host.")}</p><textarea className="st-input native-json" aria-label={tr("Native configuration JSON")} spellCheck={false} value={text} onChange={e => { setText(e.target.value); setNotice(''); }}/></> : <><p className="small muted">{tr("Add local programs or HTTP MCP servers. Connected tools can read or change the resources you give them access to. Enable tools in chat when you want the model to use them.")}</p><NativeMcpEditor text={mcp} onChange={value => { setMcp(value); setNotice(''); }}/><details><summary className="small">{tr("Configuration example")}</summary><pre className="native-example">{'{\n "files": {\n "command": "npx",\n "args": ["-y", "@modelcontextprotocol/server-filesystem", "/your/folder"],\n "disabled": false\n }\n}'}</pre><p className="xs muted">{tr("Saved credentials are masked. Keep the mask to preserve an existing value; type a new value to replace it.")}</p></details></>)}
|
||||
{tr(tab === 'fields' ? <><input className="st-input" type="search" aria-label={tr("Search native parameters")} placeholder={tr("Search parameters…")} value={query} onChange={e => setQuery(e.target.value)}/>{tr(['Responses', 'GPU & memory', 'Serving', 'Image reading', 'Tools', 'Recovery'].map(group => rows.some(f => f.group === group) ? <section className="native-settings-group" key={group}><h3 className="h4">{tr(group)}</h3>{tr(rows.filter(f => f.group === group).map(f => <label className="native-setting-row" key={f.id}><span><strong>{tr(f.label)}</strong>{tr(f.supported === false ? <small>{tr("Requires a newer Strata installation.")}</small> : f.hint ? <small>{tr(f.hint)}</small> : null)}</span><NativeFieldInput field={f} value={values[f.id]} onChange={v => change(f.id, v)}/></label>))}</section> : null))}<section className="native-settings-group"><h3 className="h4">{tr("Sampling")}</h3>{tr([['temperature', 'Temperature'], ['top_p', 'Top-p'], ['top_k', 'Top-k'], ['min_p', 'Min-p'], ['repetition_penalty', 'Repetition penalty'], ['presence_penalty', 'Presence penalty'], ['frequency_penalty', 'Frequency penalty'], ['penalty_last_n', 'Penalty window'], ['seed', 'Seed']].map(([id, label]) => <label className="native-setting-row" key={id}><span><strong>{tr(label)}</strong></span><DecimalInput ariaLabel={tr("Native {0}", [label])} value={values.sampling?.[id] ?? null} onChange={n => { const sampling = { ...values.sampling }; if (n === null) delete sampling[id]; else sampling[id] = n; change('sampling', sampling); }}/></label>))}</section></> : tab === 'json' ? <><p className="small muted">{tr("Engine arguments and STRATA environment flags are editable here. Program paths and network access remain managed by the host.")}</p><textarea className="st-input native-json" aria-label={tr("Native configuration JSON")} spellCheck={false} value={text} onChange={e => { setText(e.target.value); setNotice(''); }}/></> : <><p className="small muted">{tr("Add local programs or HTTP MCP servers. Connected tools can read or change the resources you give them access to. Enable tools in chat when you want the model to use them.")}</p><NativeMcpEditor text={mcp} onChange={value => { setMcp(value); setNotice(''); }}/><details><summary className="small">{tr("Configuration example")}</summary><pre className="native-example">{'{\n "files": {\n "command": "npx",\n "args": ["-y", "@modelcontextprotocol/server-filesystem", "/your/folder"],\n "disabled": false\n }\n}'}</pre><p className="xs muted">{tr("Saved credentials are masked. Keep the mask to preserve an existing value; type a new value to replace it.")}</p></details></>)}
|
||||
<div className="native-settings-actions"><Btn disabled={pending || !original || !dirty} variant="primary" onClick={() => void save()}>{tr(pending ? 'Saving…' : 'Save settings')}</Btn><Btn disabled={pending} onClick={() => { const saved = JSON.parse(original || '{"values":{},"mcp":{}}'); setValues(saved.values); setText(JSON.stringify(saved.values, null, 2)); setMcp(JSON.stringify(saved.mcp, null, 2)); setError(''); }}>{tr("Restore saved")}</Btn></div>{tr(notice ? <p role="status" className="small">{tr(notice)}</p> : null)}{tr(error ? <ErrorNotice message={error} onRetry={() => setRevision(value => value + 1)}/> : null)}
|
||||
</details>;
|
||||
}
|
||||
@@ -112,9 +111,20 @@ function NativeFieldInput({ field, value, onChange }: {
|
||||
return <NativeAliases field={field} value={value} onChange={onChange}/>;
|
||||
if (field.type === 'boolean')
|
||||
return <Select disabled={field.supported === false} ariaLabel={tr(field.label)} value={value === null || value === undefined ? '' : String(value)} options={[{ value: '', label: 'Default' }, { value: 'true', label: 'On' }, { value: 'false', label: 'Off' }]} onChange={v => onChange(v === '' ? null : v === 'true')}/>;
|
||||
if (field.id === 'ds_experts')
|
||||
return <DeepSeekSpeed field={field} value={value} onChange={onChange}/>;
|
||||
if (field.type === 'enum')
|
||||
return <Select disabled={field.supported === false} ariaLabel={tr(field.label)} value={value ?? ''} options={[{ value: '', label: 'Default' }, ...(field.choices ?? []).map(v => ({ value: v, label: v }))]} onChange={v => onChange(v || null)}/>;
|
||||
return <input disabled={field.supported === false} className="st-input" aria-label={tr(field.label)} inputMode={field.type === 'number' ? 'numeric' : undefined} placeholder={tr("Default")} value={Array.isArray(value) ? value.join(', ') : value ?? ''} onChange={e => onChange(e.target.value === '' ? null : field.type === 'array' ? e.target.value.split(',').map(s => s.trim()).filter(Boolean) : field.type === 'number' ? Number(e.target.value) : e.target.value)}/>;
|
||||
if (field.type === 'number')
|
||||
return <DecimalInput disabled={field.supported === false} ariaLabel={tr(field.label)} placeholder={tr("Default")} value={value === null || value === undefined || value === '' || !Number.isFinite(Number(value)) ? null : Number(value)} onChange={onChange}/>;
|
||||
return <input disabled={field.supported === false} className="st-input" aria-label={tr(field.label)} placeholder={tr("Default")} value={Array.isArray(value) ? value.join(', ') : value ?? ''} onChange={e => onChange(e.target.value === '' ? null : field.type === 'array' ? e.target.value.split(',').map(s => s.trim()).filter(Boolean) : field.type === 'number' ? Number(e.target.value) : e.target.value)}/>;
|
||||
}
|
||||
/** DeepSeek's Speed choice. The engine decides the default itself (--experts auto); the labels name the same default (lib/deepseek-speed.ts). */
|
||||
function DeepSeekSpeed({ field, value, onChange }: { field: NativeField; value: any; onChange: (v: any) => void }) {
|
||||
const native = useNative();
|
||||
const options = speedOptions(native?.hardware?.gpus as RunCard[] | undefined).map(o => ({ ...o, label: tr(o.label) }));
|
||||
return <Select disabled={field.supported === false} ariaLabel={tr(field.label)} value={value === null || value === undefined ? '' : String(value)}
|
||||
options={options} onChange={v => onChange(v === '' ? null : v)}/>;
|
||||
}
|
||||
function NativeAliases({ field, value, onChange }: {
|
||||
field: NativeField;
|
||||
|
||||
@@ -8,9 +8,20 @@ export function OnboardingMemory({ hardware, native }: {
|
||||
hardware: Hardware | null;
|
||||
native: NativeState['hardware'] | undefined;
|
||||
}) {
|
||||
const gpus = hardware ? hardware.gpus.filter(gpu => gpu.backend !== 'cpu').map(gpu => ({ id: gpu.id ?? String(gpu.index), name: gpu.name, memory: gpu.vramGb, shared: gpu.unified })) : native?.gpus.map(gpu => ({ id: String(gpu.index), name: gpu.name, memory: gpu.vram_gb, shared: false })) ?? [];
|
||||
// The Windows host probe (Win32_VideoController) cannot read the VRAM of AMD cards and reports 0 for them; the engine's own list has
|
||||
// the real size. For a dedicated card the host reports as 0, take the engine's figure for the card of the same name, or for the
|
||||
// card in the same position when both lists have the same length. A card the engine does not list stays at 0.
|
||||
const nativeGpus = native?.gpus ?? [];
|
||||
const hostGpus = hardware ? hardware.gpus.filter(gpu => gpu.backend !== 'cpu') : [];
|
||||
const engineVram = (name: string, position: number) => {
|
||||
const norm = (value: string) => value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
const byName = nativeGpus.find(g => norm(g.name).includes(norm(name)) || norm(name).includes(norm(g.name)));
|
||||
return (byName ?? (nativeGpus.length === hostGpus.length ? nativeGpus[position] : undefined))?.vram_gb || 0;
|
||||
};
|
||||
const gpus = hardware ? hostGpus.map((gpu, position) => ({ id: gpu.id ?? String(gpu.index), name: gpu.name, memory: gpu.unified || gpu.vramGb > 0 ? gpu.vramGb : engineVram(gpu.name, position), shared: gpu.unified })) : nativeGpus.map(gpu => ({ id: String(gpu.index), name: gpu.name, memory: gpu.vram_gb, shared: false }));
|
||||
const dedicatedCount = gpus.filter(gpu => !gpu.shared).length;
|
||||
const memory = hardware ? memoryInventory(hardware) : native ? { ram: native.ram_gb, vram: native.gpus.reduce((sum, gpu) => sum + Math.max(0, gpu.vram_gb || 0), 0), total: 0, shared: false } : null;
|
||||
const base = hardware ? memoryInventory(hardware) : null;
|
||||
const memory = base ? (base.shared ? base : { ...base, vram: gpus.reduce((sum, gpu) => sum + (gpu.shared ? 0 : Math.max(0, gpu.memory || 0)), 0) }) : native ? { ram: native.ram_gb, vram: native.gpus.reduce((sum, gpu) => sum + Math.max(0, gpu.vram_gb || 0), 0), total: 0, shared: false } : null;
|
||||
if (!memory)
|
||||
return <div className="onboard-hardware" role="group" aria-label={tr("Computer memory")}><div className="onboard-memory-intro"><Icon name="memory" size={18}/><span>{tr("Reading memory\u2026")}</span></div></div>;
|
||||
const total = memory.ram + memory.vram;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useStore } from '../lib/store';
|
||||
import type { SoftwareUpdate, UpdateCheckResult } from '../lib/types';
|
||||
import { nativeAction, useNative } from '../lib/native';
|
||||
import { NativeProgress } from './NativeModels';
|
||||
import { jobErrorText } from '../lib/native-job-diagnostics';
|
||||
import { appVersionLabel } from '../lib/app-version';
|
||||
import { fetchUpdateCheck, publishUpdateCheck, useInstalledVersions, useLastUpdateCheck } from '../lib/update-check';
|
||||
/** A check from an earlier day shows its date too, because the result is kept between visits. */
|
||||
@@ -70,7 +71,7 @@ export function SettingsUpdates() {
|
||||
const labels: Record<SoftwareUpdate['status'], string> = { available: 'Update available', current: 'Up to date', unavailable: 'Check unavailable', error: 'Check failed', 'no-releases': 'No release yet' };
|
||||
return <div className="settings-version" data-update={update?.status}>
|
||||
<Icon name={name === 'Strata' ? 'layers' : 'engine'} size={20}/>
|
||||
<span><strong>{tr(name)}</strong><small>{tr(version ?? 'Version unavailable')}</small>{tr(!checking && update ? <small className="settings-version__message">{tr(update.message)}</small> : null)}</span>
|
||||
<span><strong>{tr(name)}</strong><small>{tr(version ?? 'Version unavailable')}</small>{tr(!checking && update ? <small className="settings-version__message">{tr(update.message)}</small> : null)}{tr(!checking && name === 'Strata' && update?.details ? <details className="native-settings-diagnostic"><summary className="small">{tr("Details")}</summary><p className="small muted">{update.details}</p></details> : null)}</span>
|
||||
{tr(!checking && update?.status === 'available' ? (native?.available ? <Btn size="sm" variant="primary" disabled={!!active || (name === 'Engine' && !native.installed?.models.length && !native.capabilities.componentUpdates)} onClick={() => setConfirm(name === 'Strata' ? 'update-strata' : 'update-engine')}>{tr("Update available")}</Btn> : update.releaseUrl ? <a className="st-btn st-btn--secondary st-btn--sm" href={update.releaseUrl} target="_blank" rel="noreferrer" title={tr("Open the release page to update this installation")}>{tr("Update available")}<Icon name="download" size={15}/></a> : <span className="settings-version__label">{tr(labels[update.status])}</span>) : <span className="settings-version__label">{tr(checking ? 'Checking…' : update ? labels[update.status] : version === 'Not installed' ? 'Not installed' : 'Installed')}</span>)}
|
||||
</div>;
|
||||
}
|
||||
@@ -89,12 +90,12 @@ export function SettingsUpdates() {
|
||||
<Btn variant="primary" disabled={component !== 'engine' || !native?.capabilities.rollback || !!active || (native?.instances.length ?? 0) > 0} onClick={() => void operate('rollback')}>{tr("Restore previous engine")}</Btn>
|
||||
</div>
|
||||
</Dialog>
|
||||
{tr(active ? <NativeProgress job={active}/> : null)}{tr(native?.jobs.filter(j => j.state === 'error' && ['update-strata', 'update-engine', 'rollback'].includes(j.kind)).slice(-1).map(j => <p role="alert" key={j.id} className="small inline-error">{tr(j.error ?? j.summary)}</p>))}
|
||||
{tr(active ? <NativeProgress job={active}/> : null)}{tr(native?.jobs.filter(j => j.state === 'error' && ['update-strata', 'update-engine', 'rollback'].includes(j.kind)).slice(-1).map(j => <p role="alert" key={j.id} className="small inline-error">{tr(jobErrorText(j, native.jobs))}</p>))}
|
||||
<Dialog open={!!confirm} compact title={tr(confirm === 'update-strata' ? 'Update Strata?' : 'Update engine?')} onClose={() => setConfirm('')} actions={<><Btn onClick={() => setConfirm('')}>{tr("Cancel")}</Btn><Btn variant="primary" disabled={!!active || (native?.instances.length ?? 0) > 0} onClick={() => void operate(confirm)}>{tr("Update")}</Btn></>}><p>{tr("The native installer updates the installation. Model files and settings are kept. Unload native models before updating.")}</p>{tr((native?.instances.length ?? 0) > 0 ? <p className="small muted">{tr("Open Models and unload the running native models first.")}</p> : null)}</Dialog>
|
||||
</>;
|
||||
}
|
||||
|
||||
/** "Strata 0.2.2 · Engine 0.1.47": the app's own version first, the engine's beside it (About). */
|
||||
/** "Strata 0.2.5 · Engine 0.1.50": 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);
|
||||
|
||||
@@ -518,18 +518,20 @@ export function MetricGrid({ children, min = 200 }: {
|
||||
</div>);
|
||||
}
|
||||
/** Shared, announced error with a cause and a concrete recovery action. */
|
||||
export function ErrorNotice({ message, next, action, onRetry, detail }: {
|
||||
export function ErrorNotice({ message, next, action, onRetry, detail, secondary }: {
|
||||
message: string;
|
||||
next?: string;
|
||||
action?: string;
|
||||
onRetry?: () => void;
|
||||
detail?: string;
|
||||
/** A second way out, beside the first (a context error offers two). */
|
||||
secondary?: { label: string; onClick: () => void };
|
||||
}) {
|
||||
// A known failure is shown as a sentence and a next step; the original text stays under Details for experts.
|
||||
const plain = plainFailure(message);
|
||||
const known = plain.kind !== 'plain';
|
||||
const nextStep = known ? (plain.next ? tr(plain.next) : next) : next;
|
||||
const raw = detail ?? (known ? plain.detail : undefined);
|
||||
return <div className="error-notice" role="alert"><Icon name="error" size={18}/><div><strong>{tr(known ? plain.message : userError(message))}</strong>{tr(nextStep ? <p>{tr(nextStep)}</p> : null)}{tr(raw ? <details className="error-detail"><summary className="small">{tr("Details")}</summary><pre className="small muted">{raw}</pre></details> : null)}</div>{tr(onRetry ? <Btn size="sm" onClick={onRetry}>{tr(action ?? 'Try again')}</Btn> : null)}</div>;
|
||||
return <div className="error-notice" role="alert"><Icon name="error" size={18}/><div><strong>{tr(known ? plain.message : userError(message))}</strong>{tr(nextStep ? <p>{tr(nextStep)}</p> : null)}{tr(raw ? <details className="error-detail"><summary className="small">{tr("Details")}</summary><pre className="small muted">{raw}</pre></details> : null)}</div>{tr(onRetry ? (secondary ? <div className="row"><Btn size="sm" onClick={onRetry}>{tr(action ?? 'Try again')}</Btn><Btn size="sm" variant="ghost" onClick={secondary.onClick}>{tr(secondary.label)}</Btn></div> : <Btn size="sm" onClick={onRetry}>{tr(action ?? 'Try again')}</Btn>) : null)}</div>;
|
||||
}
|
||||
export function PreviewBadge() { const demo = useStore(s => s.library?.mode === 'simulated'); return demo ? <span className="preview-badge">{tr("Preview build")}</span> : null; }
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* The version the person sees as "Strata". It is the DESKTOP APP's version (0.2.2), not that of Strata Control, the tools the app runs
|
||||
* (0.1.45, the version of the Terminal). Showing Control's number as "Strata" made a tester report "Strata 0.1.45" for the 0.2.2 app.
|
||||
* The version the person sees as "Strata". It is the DESKTOP APP's version (0.2.3), not that of Strata Control, the tools the app runs
|
||||
* (0.1.45, the version of the Terminal). Showing Control's number as "Strata" made a tester report "Strata 0.1.45" for the 0.2.3 app.
|
||||
* Without a real app version (a source checkout reports 'dev') the other number is the best there is.
|
||||
*/
|
||||
export function appVersionLabel(app: string | null | undefined, control?: string | null): string | undefined {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/** A file that is not a picture or plain text (a PDF) is saved by the host in the chat's folder and named in the next message, so the assistant can open it (#3838). */
|
||||
export async function uploadChatFile(file: File, sessionId: string, fetchFn: typeof fetch = fetch): Promise<{ name: string }> {
|
||||
const host = (globalThis as { __STRATA_HOST__?: string }).__STRATA_HOST__ ?? '';
|
||||
const res = await fetchFn(`${host}/api/chat/upload?sessionId=${encodeURIComponent(sessionId)}&name=${encodeURIComponent(file.name)}`, { method: 'POST', headers: { 'content-type': file.type || 'application/octet-stream' }, body: file });
|
||||
const data = await res.json().catch(() => ({})) as { files?: { name: string; attached?: boolean }[]; error?: string };
|
||||
if (!res.ok) throw new Error(data.error || 'The file could not be added. Try again.');
|
||||
return { name: data.files?.find(f => f.attached)?.name ?? file.name };
|
||||
}
|
||||
|
||||
/** Types the Linux file dialog needs next to the extensions (WebKitGTK reads only the type part of `accept`). */
|
||||
export const FILE_ACCEPT = 'image/*,application/pdf,text/*,application/json,application/xml,.pdf,.txt,.md,.markdown,.csv,.ts,.tsx,.js,.jsx,.py,.json,.css,.rs,.go,.sh,.html,.htm,.yaml,.yml,.toml,.xml,.log';
|
||||
@@ -13,7 +13,10 @@ export function chatMetrics(session:Session|null, monitor:MonitorState|null, con
|
||||
const finished=latest?(latest.contextUsed??latest.inputTokens!+(latest.tokens??0)):null;
|
||||
const liveUsed=active&&Number.isFinite(monitor!.context.usedTokens)&&monitor!.context.usedTokens>0?monitor!.context.usedTokens:null;
|
||||
const live=replies.some(m=>m.streaming)||(active&&['reading','thinking','generating'].includes(monitor!.phase));
|
||||
const used=live?(liveUsed??finished):(finished??liveUsed);
|
||||
// While a turn runs, its own figure comes first: the host reports what the context holds after each model request of the turn. The engine's live
|
||||
// figure belongs to the request in flight (a small one between two tools read as 1%), and `finished` to an earlier reply.
|
||||
const turnUsed=replies.find(m=>m.streaming&&Number.isFinite(m.contextUsed)&&m.contextUsed!>0)?.contextUsed??null;
|
||||
const used=live?(turnUsed??liveUsed??finished):(finished??liveUsed);
|
||||
const max=remote?(replies.at(-1)?.contextMax||null):configuredContext??(active?monitor!.context.maxTokens:latest?.contextMax??null);
|
||||
const streaming=replies.find(m=>m.streaming);
|
||||
// Answer speed: everything the model generated over its decode time. While a reply streams it is that reply's; after it, the LAST reply's own
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copy text for the person. The browser clipboard call can be refused (the desktop web view asks for permission, or has no clipboard
|
||||
* object); the older copy command through a hidden field still works there. The answer is always a toast: "Copied", or one plain
|
||||
* sentence and what to do next (#3731).
|
||||
*/
|
||||
export interface ClipboardDeps {
|
||||
clipboard?: { writeText(text: string): Promise<void> } | null;
|
||||
legacyCopy?: (text: string) => boolean;
|
||||
toast: (tone: 'success' | 'warn', title: string, body?: string) => void;
|
||||
}
|
||||
|
||||
function legacyCopy(text: string): boolean {
|
||||
if (typeof document === 'undefined') return false;
|
||||
const field = document.createElement('textarea');
|
||||
field.value = text;
|
||||
field.setAttribute('readonly', '');
|
||||
field.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none';
|
||||
document.body.appendChild(field);
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
try {
|
||||
field.select();
|
||||
return document.execCommand('copy');
|
||||
} catch { return false; }
|
||||
finally { field.remove(); active?.focus?.(); }
|
||||
}
|
||||
|
||||
export async function copyText(text: string, label: string, deps: ClipboardDeps): Promise<boolean> {
|
||||
const clipboard = deps.clipboard === undefined ? (typeof navigator === 'undefined' ? null : navigator.clipboard) : deps.clipboard;
|
||||
let done = false;
|
||||
try { if (clipboard) { await clipboard.writeText(text); done = true; } } catch { /* the fallback is next */ }
|
||||
if (!done) { try { done = (deps.legacyCopy ?? legacyCopy)(text); } catch { done = false; } }
|
||||
if (done) deps.toast('success', 'Copied', label);
|
||||
else deps.toast('warn', 'Couldn’t copy', 'Select the text and copy it with Ctrl+C.');
|
||||
return done;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { tr } from './i18n';
|
||||
import { sendOversized, startNewChat, summarizeAndContinue } from './store';
|
||||
|
||||
/**
|
||||
* What the error card of a chat offers when the model's window is the problem. A conversation that no longer fits can be summarised and
|
||||
* carried on, or left for a new chat; a single message that is too large can go as a file the assistant reads in parts, or as a shortened
|
||||
* preview. "Try the message again" would only fail the same way, so the card never says it for these.
|
||||
*/
|
||||
export function contextRecovery(code: string | undefined): {
|
||||
next: string;
|
||||
action?: string;
|
||||
onRetry?: () => void;
|
||||
secondary?: { label: string; onClick: () => void };
|
||||
} {
|
||||
if (code === 'CONTEXT_MESSAGE_TOO_LARGE') {
|
||||
return {
|
||||
next: tr('Attach it as a file for the assistant to read in parts, or send a shortened preview.'),
|
||||
action: 'Attach as a file',
|
||||
onRetry: () => void sendOversized('file'),
|
||||
secondary: { label: 'Send a shortened preview', onClick: () => void sendOversized('preview') },
|
||||
};
|
||||
}
|
||||
if (code === 'CONTEXT_WINDOW_EXCEEDED') {
|
||||
return {
|
||||
next: tr('Summarize it now and continue, or start a new chat.'),
|
||||
action: 'Summarize and continue',
|
||||
onRetry: () => void summarizeAndContinue(),
|
||||
secondary: { label: 'Start a new chat', onClick: () => startNewChat() },
|
||||
};
|
||||
}
|
||||
return { next: tr('Try sending the message again.') };
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/** A number typed by a person: "0.95", ".95", "0,95" (a comma is a decimal point in many languages). null while it is not a whole number yet ("", ".", "0.", "-"). */
|
||||
export function parseDecimal(text: string): number | null {
|
||||
const t = String(text ?? '').trim().replace(',', '.');
|
||||
if (!/^-?(\d+\.?\d*|\.\d+)$/.test(t) || t.endsWith('.')) return null;
|
||||
const n = Number(t);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
/** Whether what is typed could still become a number: the field keeps it while the person is in the middle of "0." */
|
||||
export const partialDecimal = (text: string) => /^-?\d*[.,]?\d*$/.test(String(text ?? '').trim());
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { RunCard } from './native-fit';
|
||||
|
||||
/**
|
||||
* DeepSeek's Speed default, said the way the engine decides it (`--experts auto`, src/dsv41/ds4_experts_rule.hpp): Faster (5 experts a word) when
|
||||
* a card in use is an integrated / unified-memory GPU (Strix Halo, gfx1151: the model shares the PC's memory), or when the cards in use total
|
||||
* 16 GiB or less; otherwise Full quality (6). The app never decides: the engine does; this only names the same default beside the choice.
|
||||
* The total is compared in whole MiB (a 16 GB card reports 16376 MiB = 15.99 GiB and counts as 16 GiB or less).
|
||||
*/
|
||||
export const FASTER_MIB = 16 * 1024;
|
||||
|
||||
export const isIntegrated = (card: RunCard) => !!(card.uma || card.integrated || (card.unified_gb ?? 0) > 0);
|
||||
|
||||
export function fasterByDefault(cards: readonly RunCard[] | null | undefined): boolean {
|
||||
const used = (cards ?? []).filter(card => card.usable !== false);
|
||||
if (!used.length) return false;
|
||||
if (used.some(isIntegrated)) return true;
|
||||
const mib = used.reduce((sum, card) => sum + Math.round((card.vram_gb ?? 0) * 1024), 0);
|
||||
return mib > 0 && mib <= FASTER_MIB;
|
||||
}
|
||||
|
||||
/** The three entries of the Speed select: "Faster" or "Full quality" says "(default on this PC)" when it is what the engine picks by itself. */
|
||||
export function speedOptions(cards: readonly RunCard[] | null | undefined): { value: string; label: string }[] {
|
||||
const faster = fasterByDefault(cards);
|
||||
const mark = ' (default on this PC)';
|
||||
return [
|
||||
{ value: '', label: faster ? 'Default (Faster on this PC)' : 'Default (Full quality on this PC)' },
|
||||
{ value: '5', label: 'Faster' + (faster ? mark : '') },
|
||||
{ value: '6', label: 'Full quality' + (faster ? '' : mark) },
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* The Drafting choices the Models screen offers for a model. DFlash2 is for Qwen3.8-Flash-Next only (the qwen and gyro families) AND only while
|
||||
* the engine serves it: strata_control/drafting.py ENGINE_SERVES_DFLASH2 (Control reports it as `capabilities.dflash2`). Until then the setting
|
||||
* is Auto / MTP / Off; DFlash2 appears by itself when the engine can run it. The Terminal's --drafting flag has it either way, gated with a note.
|
||||
*/
|
||||
export const DRAFTING_FAMILIES = ['qwen', 'gyro'];
|
||||
|
||||
export function draftingChoices(family: string | null | undefined, dflash2Served: boolean | null | undefined): string[] {
|
||||
return DRAFTING_FAMILIES.includes(String(family)) && dflash2Served === true ? ['auto', 'mtp', 'dflash2', 'off'] : ['auto', 'mtp', 'off'];
|
||||
}
|
||||
@@ -128,3 +128,6 @@ export function formatModelName(model: {
|
||||
export function userError(message: string): string {
|
||||
return message.replace(/^\s*Fixture:\s*/i, '').replace(/^./, c => c.toUpperCase());
|
||||
}
|
||||
|
||||
/** Decimal megabytes (1 MB = 1,000,000 bytes, at least 1): the same figure as the Terminal and the release page. The only size helper in MB; no binary units. */
|
||||
export const megabytes = (bytes: number) => Math.max(1, Math.round(bytes / 1_000_000));
|
||||
@@ -89,17 +89,20 @@ export function pickMain(cs: RoleCard[]): { main: RoleCard | null; why: MainWhy
|
||||
export type OffWhy = 'cannot_run' | 'igpu' | 'small' | 'helper';
|
||||
export interface Recommendation { roles: Roles; main: number | null; why: MainWhy; reasons: Record<number, MainWhy | OffWhy> }
|
||||
|
||||
/** The best card is Main; every other card starts as Helper, EXCEPT a card with 8 GB or less, built-in graphics beside a real card, and a card the engine cannot run: those start Off. */
|
||||
/** The best card is Main; every other card starts as Helper, EXCEPT a card with 8 GB or less when a bigger card is there (or it has under 4 GB), built-in graphics beside a real card, and a card the engine cannot run: those start Off. */
|
||||
export function recommend(cards: RoleCard[] | null | undefined, vendor?: string | null): Recommendation {
|
||||
const cs = rows(cards, vendor);
|
||||
const { main, why } = pickMain(cs);
|
||||
const roles: Roles = {}, reasons: Recommendation['reasons'] = {};
|
||||
// An 8 GB card is a poor Helper beside a bigger card, but a perfectly good one when 8 GB is all there is: two GTX 1080s are a working
|
||||
// 16 GB pool (#3655). A card with 8 GB or less starts Off only when a bigger usable card is there, or when it is too small to help.
|
||||
const hasBigger = cs.some(c => canRun(c) && !shares(c) && rnd(mem(c)) > OFF_AT_OR_BELOW_GB);
|
||||
for (const c of cs) {
|
||||
const i = c.index;
|
||||
if (main && c === main) { roles[i] = 'main'; reasons[i] = why; }
|
||||
else if (!canRun(c)) { roles[i] = 'off'; reasons[i] = 'cannot_run'; }
|
||||
else if (shares(c) && main) { roles[i] = 'off'; reasons[i] = 'igpu'; }
|
||||
else if (rnd(mem(c)) <= OFF_AT_OR_BELOW_GB) { roles[i] = 'off'; reasons[i] = 'small'; }
|
||||
else if (rnd(mem(c)) <= OFF_AT_OR_BELOW_GB && (hasBigger || mem(c) < MIN_CARD_GB)) { roles[i] = 'off'; reasons[i] = 'small'; }
|
||||
else { roles[i] = 'helper'; reasons[i] = 'helper'; }
|
||||
}
|
||||
return { roles, main: main ? main.index : null, why, reasons };
|
||||
|
||||
@@ -7,6 +7,22 @@ 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 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 fix_en from '../locales/fix-en.json';
|
||||
import fix_zh from '../locales/fix-zh.json';
|
||||
import fix_ja from '../locales/fix-ja.json';
|
||||
import fix_de from '../locales/fix-de.json';
|
||||
import fix_es from '../locales/fix-es.json';
|
||||
import fix_ru from '../locales/fix-ru.json';
|
||||
import fix_pt from '../locales/fix-pt.json';
|
||||
import fix_ko from '../locales/fix-ko.json';
|
||||
import base_zh from '../locales/zh.json';
|
||||
import base_ja from '../locales/ja.json';
|
||||
import base_de from '../locales/de.json';
|
||||
@@ -18,8 +34,10 @@ import base_ko from '../locales/ko.json';
|
||||
export const LANGUAGES=[{value:'en',label:'English'},{value:'zh',label:'中文(简体)'},{value:'ja',label:'日本語'},{value:'de',label:'Deutsch'},{value:'es',label:'Español'},{value:'ru',label:'Русский'},{value:'pt',label:'Português'},{value:'ko',label:'한국어'}] as const;
|
||||
export type Language=typeof LANGUAGES[number]['value'];
|
||||
/* The Tune model dialog's strings live in tune-<lang>.json (apps/host/scripts/make-tune-locales.py) so other interface work does not collide with them. */
|
||||
const english:Record<string,string>={...baseEnglish,...tune_en};
|
||||
const [zh,ja,de,es,ru,pt,ko]=[{...base_zh,...tune_zh},{...base_ja,...tune_ja},{...base_de,...tune_de},{...base_es,...tune_es},{...base_ru,...tune_ru},{...base_pt,...tune_pt},{...base_ko,...tune_ko}] as Record<string,string>[];
|
||||
/* The tool-server, picture and agent-result strings of the tools and images fixes live in tools-<lang>.json for the same reason. */
|
||||
/* The strings that had no key in the files above (#3479, #3825) and the 0.2.4.3 fixes live in fix-<lang>.json (scripts/make-fix-locales.py). */
|
||||
const english:Record<string,string>={...baseEnglish,...tune_en,...tools_en,...fix_en};
|
||||
const [zh,ja,de,es,ru,pt,ko]=[{...base_zh,...tune_zh,...tools_zh,...fix_zh},{...base_ja,...tune_ja,...tools_ja,...fix_ja},{...base_de,...tune_de,...tools_de,...fix_de},{...base_es,...tune_es,...tools_es,...fix_es},{...base_ru,...tune_ru,...tools_ru,...fix_ru},{...base_pt,...tune_pt,...tools_pt,...fix_pt},{...base_ko,...tune_ko,...tools_ko,...fix_ko}] as Record<string,string>[];
|
||||
const catalogs:Record<Language,Record<string,string>>={en:english,zh,ja,de,es,ru,pt,ko};
|
||||
const 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);
|
||||
@@ -36,7 +54,7 @@ const patterns=Object.entries(english).filter(([,value])=>/\{\d+\}/.test(value))
|
||||
expression+=escape(value.slice(at));
|
||||
return{id,slots,pattern:new RegExp('^'+expression+'$'),specificity:value.replace(/\{\d+\}/g,'').length};
|
||||
}).sort((a,b)=>b.specificity-a.specificity);
|
||||
const localizedTemplateSlots:Record<string,number[]>={s2473:[1,2],s0825:[0,1],s2455:[0],s2456:[0],s7400:[1]};
|
||||
const localizedTemplateSlots:Record<string,number[]>={s1242:[0],s2473:[1,2],s0825:[0,1],s2455:[0],s2456:[0],s7400:[1]};
|
||||
|
||||
/** Translate interface copy only. Non-string React children pass through unchanged. */
|
||||
export function tr<T>(value:T,values?:readonly unknown[]):T{
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { rawUrlOf } from './deliverables';
|
||||
|
||||
/**
|
||||
* Which picture addresses a model's Markdown may put in an <img> (tools and images review O3, 2026-10-22).
|
||||
*
|
||||
* An answer can carry ``. It is shown only when the picture is already on this computer or inside the message:
|
||||
* - a data: address of a PNG, JPEG, WebP or GIF (never SVG: an SVG can carry script and styles);
|
||||
* - the host's own /api/files/raw address (a file inside the chat's folder, served by the host and by nobody else);
|
||||
* - a path without a scheme, which is read as a file of the chat's folder through that same address.
|
||||
* Nothing is fetched from the internet by default (an address in the answer would tell its owner that the answer was read, and
|
||||
* which IP read it): an http(s) picture stays a link the person can open. file:, javascript:, blob: and every other scheme show as text.
|
||||
*/
|
||||
export type ImagePlan =
|
||||
| { kind: 'img'; src: string }
|
||||
| { kind: 'link'; href: string }
|
||||
| { kind: 'text' };
|
||||
|
||||
const DATA_IMAGE = /^data:image\/(?:png|jpe?g|webp|gif);base64,[A-Za-z0-9+/]+={0,2}$/i;
|
||||
const SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
||||
|
||||
export function imagePlan(src: string, sessionId?: string | null): ImagePlan {
|
||||
const value = src.trim();
|
||||
if (!value) return { kind: 'text' };
|
||||
if (DATA_IMAGE.test(value)) return { kind: 'img', src: value };
|
||||
if (/^https?:\/\//i.test(value)) return { kind: 'link', href: value };
|
||||
if (value.startsWith('/api/files/raw?')) return { kind: 'img', src: value };
|
||||
// a drive letter (C:\x.png, C:/x.png) is a path, not a scheme
|
||||
if (SCHEME.test(value) && !/^[a-z]:[\\/]/i.test(value)) return { kind: 'text' };
|
||||
if (value.startsWith('//') || value.startsWith('\\\\')) return { kind: 'text' }; // another computer's share
|
||||
if (!sessionId) return { kind: 'text' };
|
||||
return { kind: 'img', src: rawUrlOf(sessionId, value) };
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/** The one rule for the name of a tool server. A copy of server/mcp-names.mjs (tests/mcp-rows.test.mjs checks that they agree). */
|
||||
export const MCP_NAME_MAX = 32;
|
||||
export const MCP_NAME_RE = /^[A-Za-z0-9_-]{1,32}$/;
|
||||
export const validMcpName = (name: string) => MCP_NAME_RE.test(name);
|
||||
export const MCP_NAME_HELP = 'Use a simple server name of up to 32 characters: letters, numbers, hyphens or underscores.';
|
||||
@@ -22,3 +22,9 @@ export function pickerGroups(library: ModelLibrary) {
|
||||
}
|
||||
return Array.from(groups, ([family, versions]) => ({ family, versions }));
|
||||
}
|
||||
|
||||
/** Optimized models that are on this PC but not loaded yet: the chat picker must list them, or it says "none downloaded" while Storage shows them (#3814). */
|
||||
export function pickerNativeModels<M extends { model: string; model_name?: string; ready?: boolean }>(installed: M[] | undefined | null, instances: { label: string }[] | undefined | null): M[] {
|
||||
const loaded = new Set((instances ?? []).map(i => i.label));
|
||||
return (installed ?? []).filter(m => m.ready !== false && !loaded.has(m.model_name ?? m.model));
|
||||
}
|
||||
@@ -13,6 +13,18 @@ export function gpuMetric(devices:GpuInfo[],readings:GpuReading[]|undefined,key:
|
||||
return mode==='max'?Math.max(...values):values.reduce((n,v)=>n+v,0)/(mode==='mean'?values.length:1);
|
||||
}
|
||||
|
||||
/** The card's name without its maker: "NVIDIA GeForce RTX 5070" -> "RTX 5070". */
|
||||
export const shortGpuName=(name:string)=>String(name??'').replace(/^(NVIDIA|AMD|Intel)(\(R\)|\(TM\))?\s+/i,'').replace(/^(GeForce|Radeon|Arc)(\(TM\))?\s+/i,'').trim()||String(name??'');
|
||||
|
||||
/**
|
||||
* The reading over the cards that DO report this value. A card that cannot report it (an integrated one without a temperature sensor) is left
|
||||
* out instead of blanking the figure; `partial` says some were left out, and `devices` names the cards the figure is from.
|
||||
*/
|
||||
export function gpuReported(devices:GpuInfo[],readings:GpuReading[]|undefined,key:keyof GpuReading,mode:'sum'|'mean'|'max'='sum'):{value:number|null;devices:GpuInfo[];partial:boolean} {
|
||||
const reporting=devices.filter(device=>hasReading(readings?.find(reading=>reading.id===gpuId(device))?.[key]));
|
||||
return {value:reporting.length?gpuMetric(reporting,readings,key,mode):null,devices:reporting,partial:reporting.length>0&&reporting.length<devices.length};
|
||||
}
|
||||
|
||||
export function gpuCapacity(devices:GpuInfo[]) {
|
||||
const shared=devices.filter(g=>g.unified).map(g=>g.vramGb);
|
||||
return devices.filter(g=>!g.unified).reduce((n,g)=>n+g.vramGb,0)+(shared.length?Math.max(...shared):0);
|
||||
|
||||
@@ -3,7 +3,7 @@ import {tr} from './i18n';
|
||||
import {nvfp4Version} from './native-names';
|
||||
export interface CatalogVersion extends NativeSize{sourceFamily:string;edition:string;images:boolean}
|
||||
export interface CatalogModel extends Omit<NativeFamily,'sizes'>{sizes:CatalogVersion[]}
|
||||
export function nativeVersionLabel(model:string){const nv=nvfp4Version(model);if(nv)return tr(nv.name);return tr(({Q2_0:'Fastest',IQ2_XS:'Compact',IQ3_XXS:'Balanced',IQ3_S:'High quality',IQ1_M:'Coding','Gyro-S':'Smallest','Gyro-M':'Small memory'} as Record<string,string>)[model]??model);}
|
||||
export function nativeVersionLabel(model:string){const nv=nvfp4Version(model);if(nv)return tr(nv.name);return tr(({Q2_0:'Fastest',IQ2_XS:'Compact',IQ3_XXS:'Balanced',IQ3_S:'High quality',IQ1_M:'Coding','Gyro-S':'Smallest','Gyro-M':'Small memory','IST-DAS-1.75bpw':'Smallest','IST-DAS-2.00bpw':'Balanced','IST-DAS-2.25bpw':'Best quality'} as Record<string,string>)[model]??model);}
|
||||
export const catalogVersionLabel=(version:CatalogVersion)=>{const label=nativeVersionLabel(version.model);return [tr(version.edition),label===version.model&&version.edition?'':label].filter(Boolean).join(' · ');};
|
||||
/** The one line that says how a version differs from its neighbours (the three NVFP4 versions); empty for the rest. */
|
||||
export const versionNote=(model:string)=>{const nv=nvfp4Version(model);return nv?tr(nv.note):'';};
|
||||
@@ -12,6 +12,7 @@ export function nativeCatalogModels(families:NativeFamily[]):CatalogModel[]{
|
||||
if(!families.length)return[];
|
||||
const groups:CatalogModel[]=[{family:'qwen3.8',title:'Qwen3.8-Flash-Next',about:'',experimental:false,images:false,sizes:[]},{family:'qwen3.6',title:'Qwen3.6-35B-A3B',about:'',experimental:false,images:false,sizes:[]}];
|
||||
for(const family of families){
|
||||
if(family.family==='deepseek41'){const sizes=family.sizes.filter(size=>!/^UD-Q8/i.test(size.model));if(sizes.length)groups.push({family:'deepseek41',title:family.title||'DeepSeek-V4.1-Flash',about:family.about??'',experimental:false,images:false,sizes:sizes.map(size=>({...size,sourceFamily:family.family,edition:'',images:false}))});continue;}
|
||||
const target=/qwen3[._-]?6|35b[-_]?a3b/i.test(family.title+' '+family.family)?groups[1]:['qwen','swift','coder','unsloth','gyro'].includes(family.family)||/qwen3[._-]?8|flash.next/i.test(family.title)?groups[0]:undefined;
|
||||
if(!target)continue;
|
||||
const edition=target===groups[0]?({swift:'Swift 1.5',coder:'Coder',unsloth:'Unsloth',gyro:'Gyro'} as Record<string,string>)[family.family]??'':'';
|
||||
|
||||
@@ -30,16 +30,27 @@ export function memoryNeeded(v: FitInput): number {
|
||||
return round1((v.experts_gb ?? v.ram_needed_gb ?? 0) + DENSE_EXTRA_GB);
|
||||
}
|
||||
|
||||
export function fitOn(v: FitInput, hw: FitHardware | null | undefined, mtp = true): Fit {
|
||||
/** The conversation cache of Flash-Next: 13 attention layers, bytes per token per layer by cache format, 5.4% over the bare cache
|
||||
* (strata_control/vram_fit.py KV_BYTES, KV_MEASURED_FACTOR; setup.py UD_FIT_KV_BYTES). */
|
||||
const KV_BYTES: Record<string, number> = { fp16: 2112, int8: 1056, q4_0: 576, k8v4: 816 };
|
||||
const KV_LAYERS = 13, KV_FACTOR = 1.054, BASE_CONTEXT = 32768;
|
||||
export function cacheGb(context: number, kv = 'int8'): number {
|
||||
return (121 + context * KV_LAYERS * (KV_BYTES[String(kv).toLowerCase()] ?? KV_BYTES.int8) * KV_FACTOR / 2 ** 20) / 1024; // 121 MiB fixed per session
|
||||
}
|
||||
export interface CacheChoice { context: number; kv?: string }
|
||||
|
||||
export function fitOn(v: FitInput, hw: FitHardware | null | undefined, mtp = true, cache?: CacheChoice | null): Fit {
|
||||
const memoryGb = memoryNeeded(v);
|
||||
// DENSE_GB already holds a 32K cache in the default format; the person's context and format replace it (#3832).
|
||||
const denseGb = cache && cache.context > 0 ? Math.max(DENSE_GB - cacheGb(BASE_CONTEXT), 0) + cacheGb(cache.context, cache.kv) : DENSE_GB;
|
||||
const ram = Number(hw?.ram_gb), vram = Number(hw?.vram_gb) || 0;
|
||||
if (!Number.isFinite(ram) || ram <= 0) return { memoryGb, vramUseGb: 0, ramUseGb: 0, spillGb: 0, known: false };
|
||||
const experts = Math.max(0, memoryNeeded(v) - DENSE_EXTRA_GB);
|
||||
const onGpu = Math.max(0, Math.min(experts, vram - DENSE_GB - (mtp && vram > 0 ? MTP_VRAM_GB : 0)));
|
||||
const onGpu = Math.max(0, Math.min(experts, vram - denseGb - (mtp && vram > 0 ? MTP_VRAM_GB : 0)));
|
||||
const ramRoom = Math.max(0, ram - SYSTEM_ROOM_GB);
|
||||
const spillGb = Math.max(0, experts - onGpu - ramRoom);
|
||||
const inRam = Math.max(0, experts - onGpu - spillGb);
|
||||
const vramUseGb = vram > 0 ? Math.min(vram, DENSE_GB + (mtp ? MTP_VRAM_GB : 0) + onGpu) : 0;
|
||||
const vramUseGb = vram > 0 ? Math.min(vram, denseGb + (mtp ? MTP_VRAM_GB : 0) + onGpu) : 0;
|
||||
return { memoryGb, vramUseGb: round1(vramUseGb), ramUseGb: round1(inRam + DENSE_EXTRA_GB * (vram > 0 ? 0 : 1)), spillGb: round1(spillGb), known: true };
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { NativeJob } from './native';
|
||||
|
||||
/** What the host says about a job a restart cut short. */
|
||||
export const INTERRUPTED = 'The host restarted before completion. Check diagnostics, then retry; completed files are kept.';
|
||||
const updateKinds = new Set(['update-strata', 'update-engine']);
|
||||
|
||||
/**
|
||||
* A later finished update of the same component settles the restart interruption of an earlier one (ported from #3578 by medking82).
|
||||
* The records are never changed: a running retry, another component or an ordinary failure stays a plain failure.
|
||||
*/
|
||||
export function recoveredInterruptions(jobs: readonly NativeJob[]): Set<string> {
|
||||
const time = (job: NativeJob) => Date.parse(job.startedAt);
|
||||
return new Set(jobs.filter(job => job.state === 'error' && updateKinds.has(job.kind) && job.error === INTERRUPTED && Number.isFinite(time(job))
|
||||
&& jobs.some(later => later.state === 'done' && later.kind === job.kind && Number.isFinite(time(later)) && time(later) > time(job))).map(job => job.id));
|
||||
}
|
||||
|
||||
/** The text of a failed job's alert. The alert stays red; an interruption a later update has settled says so in plain words. */
|
||||
export function jobErrorText(job: NativeJob, jobs: readonly NativeJob[]): string {
|
||||
return recoveredInterruptions(jobs).has(job.id)
|
||||
? 'This update was cut short when the app restarted. A later update finished, so nothing needs doing.'
|
||||
: job.error ?? job.summary;
|
||||
}
|
||||
@@ -3,18 +3,22 @@ import { api } from './client';
|
||||
import type { Hardware, RuntimeModels } from './types';
|
||||
export interface GpuChoiceOption {key:string;cards:number[];label:string;verdict:'fits'|'tight'|'spills'|'unknown';words:string;notes:string[];recommended:boolean}
|
||||
export interface GpuChoice {options:GpuChoiceOption[];recommended:string|null;ignored:{name?:string;why_ignored:string}[];cards:number}
|
||||
export interface NativeSize {/** The first engine that reads this size's files (the catalog), and whether the installed engine is older: the screen then says "needs a newer engine" and offers the update. */engine_min?:string|null;needsNewerEngine?:boolean;id:string;model:string;about:string;download_gb:number;ram_needed_gb:number;experts_gb:number;on_this_pc:string|null;installed:boolean;gpu_options?:GpuChoice|null;images?:boolean;imagesCapable?:boolean}
|
||||
export interface NativeSize {/** The first engine that reads this size's files (the catalog), and whether the installed engine is older: the screen then says "needs a newer engine" and offers the update. */engine_min?:string|null;needsNewerEngine?:boolean;id:string;model:string;about:string;download_gb:number;ram_needed_gb:number;experts_gb:number;on_this_pc:string|null;installed:boolean;gpu_options?:GpuChoice|null;images?:boolean;imagesCapable?:boolean;/** DeepSeek-V4.1-Flash and the like: several big files and an engine of their own. */multi_file?:boolean}
|
||||
export interface NativeFamily {family:string;title:string;about:string;experimental:boolean;images:boolean;sizes:NativeSize[]}
|
||||
export interface NativeField {id:string;label:string;group:string;type:'number'|'boolean'|'string'|'enum'|'array';min?:number;max?:number;choices?:string[];hint?:string;supported?:boolean}
|
||||
export interface NativeField {id:string;label:string;group:string;type:'number'|'boolean'|'string'|'enum'|'array';min?:number;max?:number;choices?:string[];hint?:string;supported?:boolean;forModel?:string}
|
||||
export interface NativeJob {id:string;kind:string;guided?:boolean;model?:string;family?:string;state:'running'|'done'|'error'|'cancelled';startedAt:string;summary:string;error:string|null;details?:string;result:Record<string,any>|null;progress?:Record<string,any>|null}
|
||||
export interface NativeInstance {id:string;apiModelId:string;model:string;label:string;aliases?:string[];vision?:boolean;resourceProfile?:'work'|'performance'|null;context?:number;port:number;gpus:number[];status:'loading'|'ready'|'unloading'|'error';phase:string;loadedAt:string;error:string|null;metrics:Record<string,any>|null}
|
||||
export interface NativeState {
|
||||
/** The picture reader (strata-vision) is installed with the engine, so models marked for pictures can read them here. */
|
||||
/** Strata Control's view of the engine for this computer (the app): usable false = none found or recognised; `notice` says why in plain words. */
|
||||
engine?:{usable:boolean;notice?:string|null;wrongMake?:boolean;version?:string|null};
|
||||
visionReader?: boolean;available:boolean;repo:string;downloadDirectory?:string;/** Free bytes on the drive that holds the models folder. */downloadFreeBytes?:number|null;downloadTotalBytes?:number|null;version?:string|null;summary:string;error:string|null;catalog:{families:NativeFamily[];contexts:number[];recommendation:{family:string|null;model:string|null;context?:number;why?:string};context_note:string}|null;installed:{models:{model:string;config:string;model_name:string;ready:boolean;/** The picture-reading files are on this computer (notes-split-agent8.md, item 8). */visionPrepared?:boolean;context?:number;kv?:string;images?:string;visionEnabled?:boolean;visionDevice?:string;lowRamMode?:string;resourceProfile?:'work'|'performance';dataFolder?:string;diskGb?:number;diskBytes?:number;/** Files this model shares with another (the picture reader, the draft layer): counted once, in `usage`. */sharedBytes?:number;/** What Delete frees: files in Strata's own folders that no other model uses. */removableGb?:number;missing_files?:string[]}[];/** What all models take on disk, from their real files, shared files counted once. */usage?:{total_bytes:number;total_gb:number;shared_bytes:number;shared_gb:number;models:Record<string,{bytes:number;gb:number}>}|null;data_folder:string;engine:{version:string|null}}|null;hardware:{ram_gb:number;engine_vendor?:string;gpus:{index:number;name:string;vram_gb:number;usable:boolean;/** SMs x clock: only the order between cards matters (strata_control/gpu_roles.py). */speed?:number;vendor?:string;uma?:boolean;integrated?:boolean;unified_gb?:number;problem?:string|null}[]}|null;fields:NativeField[];instances:NativeInstance[];defaultId:string|null;jobs:NativeJob[];capabilities:{responses?:boolean;updates:boolean;rollback:boolean;calibration:boolean;componentUpdates?:boolean}}
|
||||
visionReader?: boolean;available:boolean;repo:string;downloadDirectory?:string;/** Free bytes on the drive that holds the models folder. */downloadFreeBytes?:number|null;downloadTotalBytes?:number|null;version?:string|null;summary:string;error:string|null;catalog:{families:NativeFamily[];contexts:number[];recommendation:{family:string|null;model:string|null;context?:number;why?:string};context_note:string}|null;installed:{models:{model:string;config:string;model_name:string;ready:boolean;/** The picture-reading files are on this computer (notes-split-agent8.md, item 8). */visionPrepared?:boolean;context?:number;kv?:string;images?:string;visionEnabled?:boolean;visionDevice?:string;lowRamMode?:string;resourceProfile?:'work'|'performance';/** The Drafting choice saved with the model: auto | mtp | dflash2 | off. */drafting?:string;dataFolder?:string;diskGb?:number;diskBytes?:number;/** Files this model shares with another (the picture reader, the draft layer): counted once, in `usage`. */sharedBytes?:number;/** What Delete frees: files in Strata's own folders that no other model uses. */removableGb?:number;missing_files?:string[]}[];/** What all models take on disk, from their real files, shared files counted once. */usage?:{total_bytes:number;total_gb:number;shared_bytes:number;shared_gb:number;models:Record<string,{bytes:number;gb:number}>}|null;data_folder:string;engine:{version:string|null}}|null;hardware:{ram_gb:number;engine_vendor?:string;gpus:{index:number;name:string;vram_gb:number;usable:boolean;/** SMs x clock: only the order between cards matters (strata_control/gpu_roles.py). */speed?:number;vendor?:string;uma?:boolean;integrated?:boolean;unified_gb?:number;problem?:string|null}[]}|null;fields:NativeField[];instances:NativeInstance[];defaultId:string|null;jobs:NativeJob[];capabilities:{/** The engine serves DFlash2 (Control says): the Drafting choice then offers it. */dflash2?:boolean;responses?:boolean;updates:boolean;rollback:boolean;calibration:boolean;componentUpdates?:boolean}}
|
||||
let state:NativeState|null=null;
|
||||
const listeners=new Set<()=>void>();
|
||||
/** True once the first read of the native state has ended, whatever its answer: until then the Models page must not pick a model of its own (#3740). */
|
||||
let settled=false;
|
||||
export const nativeSettled=()=>settled;
|
||||
export function subscribeNative(fn:()=>void){listeners.add(fn);return()=>{listeners.delete(fn);};}
|
||||
function put(value:NativeState){if(state&&JSON.stringify(state)===JSON.stringify(value))return;state=value;listeners.forEach(fn=>fn());}
|
||||
/** The host pushes its whole native state (jobs included) at every progress step: the page shows it at once, without waiting for the next poll. */
|
||||
export function applyNativeEvent(value:unknown){if(value&&typeof value==='object'&&'available' in value&&'jobs' in value)put(value as NativeState);}
|
||||
@@ -24,7 +28,7 @@ function read(force:boolean):Promise<void>{
|
||||
// A read that never answers must not freeze the page: after the limit the next poll starts a fresh one.
|
||||
let stop:ReturnType<typeof setTimeout>|undefined;
|
||||
const limit=new Promise<never>((_,reject)=>{stop=setTimeout(()=>reject(new Error('The host did not answer in time.')),READ_LIMIT_MS);});
|
||||
return Promise.race([request,limit]).then(put).catch(error=>{if(state){state={...state,error:error.message,instances:state.instances.map(i=>({...i,metrics:null,phase:'unavailable'}))};listeners.forEach(fn=>fn());}}).finally(()=>clearTimeout(stop));
|
||||
return Promise.race([request,limit]).then(put).catch(error=>{if(state){state={...state,error:error.message,instances:state.instances.map(i=>({...i,metrics:null,phase:'unavailable'}))};listeners.forEach(fn=>fn());}}).finally(()=>{clearTimeout(stop);if(!settled){settled=true;listeners.forEach(fn=>fn());}});
|
||||
}
|
||||
/* Two kinds of read that never wait for each other: the light poll (the host answers at once with the jobs it holds) and the forced read after an action (the host
|
||||
re-reads the installation, which can take many seconds while a download is writing). A slow forced read used to hold every later poll, so a running download looked stuck. */
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/** The pictures on the clipboard, named so each one is told apart in the composer (a pasted screenshot arrives as "image.png"). */
|
||||
export function pastedFiles(data: DataTransfer | null | undefined): File[] {
|
||||
const out: File[] = [];
|
||||
const found: File[] = Array.from(data?.files ?? []);
|
||||
// A screenshot from the clipboard (Win+Shift+S) often comes as an item and not in `files` (#3748).
|
||||
if (!found.some(f => f.type.startsWith('image/'))) {
|
||||
for (const item of Array.from(data?.items ?? [])) {
|
||||
if (item.kind !== 'file' || !item.type.startsWith('image/')) continue;
|
||||
const file = item.getAsFile();
|
||||
if (file) found.push(file);
|
||||
}
|
||||
}
|
||||
for (const file of found) {
|
||||
if (!file.type.startsWith('image/')) continue;
|
||||
const extension = file.type.split('/')[1]?.replace('jpeg', 'jpg').replace(/[+].*/, '') || 'png';
|
||||
out.push(new File([file], `Pasted picture ${new Date().toLocaleTimeString().replace(/[^0-9]+/g, '-').replace(/-$/, '')}${out.length ? `-${out.length + 1}` : ''}.${extension}`, { type: file.type }));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -8,13 +8,15 @@ import { tr } from './i18n';
|
||||
|
||||
export interface PlainFailure { message: string; next?: string; detail?: string; kind: string }
|
||||
|
||||
interface Rule { kind: string; match: RegExp; message: string; next?: string; detail?: (m: RegExpMatchArray, raw: string) => string | undefined }
|
||||
interface Rule { kind: string; match: RegExp; message: string | ((m: RegExpMatchArray) => string); next?: string; detail?: (m: RegExpMatchArray, raw: string) => string | undefined }
|
||||
|
||||
const MEMORY = 'The model stopped while starting, most likely because the PC ran low on memory.';
|
||||
const MEMORY_NEXT = 'Close other programs and try again, or choose “Leave 20% for my work”. In Expert mode you can also turn on the low-RAM option, so part of the model is read from the SSD.';
|
||||
|
||||
/** Order matters: the first rule that matches wins. */
|
||||
const RULES: Rule[] = [
|
||||
// The host names the files it could not find (#3478). The sentence with the names is a template in every language: tr() fills it.
|
||||
{ kind: 'model-files-missing', match: /Some model files are missing: (.+?)\. Use Download/i, message: m => `These model files are missing: ${m[1]}.`, next: 'Press Download & load again to fetch them. Files that are already there are kept. If the same files are listed again, open Details and share them when you ask for help.' },
|
||||
{ kind: 'tunnel-own-running', match: /Strata’s tunnel for this address is already running \(PID/i, message: 'Strata’s tunnel for this address is already running.', next: 'Stop it first, or use it as it is, then connect again here.' },
|
||||
{ kind: 'gpu-shared', match: /GPU \d+ is already used by .*, so this model did not fit there/i, message: 'Another model on the same graphics card left no room for this one.', next: 'Load it on a different GPU, or unload the other model first.' },
|
||||
{ kind: 'gpu-no-code', match: /none of this computer.s graphics cards has code in this Strata engine|not an architecture this Strata engine was compiled for/i, message: 'This Strata engine has no code for the graphics card it tried to use.', next: 'Pick another card under Run on, or open Details to see which cards were found and what the engine is built for.' },
|
||||
@@ -34,6 +36,7 @@ const RULES: Rule[] = [
|
||||
{ kind: 'blocked', match: /0xC0000022|3221225506|\bEACCES\b|\bEPERM\b|operation not permitted/i, message: 'Windows or your security software stopped the engine from starting.', next: 'Allow Strata in your security software, then try again.' },
|
||||
{ kind: 'file-missing', match: /\bENOENT\b|no such file or directory|cannot find the (?:file|path)|0x80070002|exit code 127|file not found/i, message: 'Strata could not find a file it needs to start the engine.', next: 'Reinstall Strata or press Repair.' },
|
||||
{ kind: 'memory', match: /out of memory|memory guard|\boom\b|\bkilled\b|exit code 137|cannot allocate memory|not enough memory|bad_alloc|pinned-memory-exhausted|mapped staging|cudaErrorMemoryAllocation|insufficient memory/i, message: MEMORY, next: MEMORY_NEXT },
|
||||
{ kind: 'previous-session', match: /lost host child is still present|retry after it exits/i, message: 'The last Strata session is still closing down.', next: 'Wait a moment and try again. If it keeps happening, restart the computer.' },
|
||||
{ kind: 'start-failed', match: /server_exited|engine stopped|exited before|died before/i, message: 'The model stopped while starting.', next: 'Try again. If it keeps happening, open Details and share them when you ask for help.' },
|
||||
{ kind: 'slow-load', match: /loading took more than/i, message: 'Loading is taking too long.', next: 'The PC may be short on memory. Close other programs and try again.' },
|
||||
{ kind: 'engine-missing', match: /no engine for this pc|engine component|never downloads an engine|engine .*not installed|no strata engine/i, message: 'Strata’s engine for this computer isn’t installed yet.', next: 'It is added when you download a model. Download your model again to add it.' },
|
||||
@@ -50,7 +53,7 @@ export function plainFailure(raw: string | null | undefined): PlainFailure {
|
||||
for (const rule of RULES) {
|
||||
const m = text.match(rule.match);
|
||||
if (!m) continue;
|
||||
return { kind: rule.kind, message: rule.message, next: rule.next, detail: rule.detail ? rule.detail(m, text) : text };
|
||||
return { kind: rule.kind, message: typeof rule.message === 'function' ? rule.message(m) : rule.message, next: rule.next, detail: rule.detail ? rule.detail(m, text) : text };
|
||||
}
|
||||
return { kind: 'plain', message: text.replace(/^./, c => c.toUpperCase()) };
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ function normalise(loc: Location): void {
|
||||
* Navigate, morphing between screens when the browser can.
|
||||
*
|
||||
* A view transition needs the DOM update to happen inside its callback, and
|
||||
* React commits asynchronously — so we wait two frames before resolving. It is
|
||||
* React commits asynchronously — so we wait for the commit (never for a frame: see afterRouteUpdate). It is
|
||||
* skipped while a response is streaming (the transcript is being appended to on
|
||||
* every token, and snapshotting it mid-stream would fight the SSE updates) and
|
||||
* when the user has asked for reduced motion.
|
||||
@@ -100,6 +100,34 @@ export function isWebKitGtk(ua: string = navigator.userAgent): boolean {
|
||||
return /AppleWebKit/.test(ua) && /Linux|X11/.test(ua) && !/Chrome|Chromium|CriOS|Edg|OPR|Android/.test(ua);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve once the new route has been drawn into the page, or after a short deadline, whichever comes first.
|
||||
*
|
||||
* The update callback of a view transition must not wait for requestAnimationFrame: while the callback is pending the browser holds
|
||||
* rendering (Chromium and Firefox both do), so a frame callback never runs, and every click waits for the browser's own guard,
|
||||
* about 4 s in Chromium and the WebView2 window, about 5 s in Firefox (#3673). A DOM change after the hash flipped means React has
|
||||
* committed the new screen; the deadline covers a route that changes nothing visible.
|
||||
*/
|
||||
export function afterRouteUpdate(done: () => void, timeoutMs = 150): void {
|
||||
let settled = false;
|
||||
let observer: MutationObserver | undefined;
|
||||
const finish = () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
observer?.disconnect();
|
||||
done();
|
||||
};
|
||||
try {
|
||||
if (typeof MutationObserver === 'function' && typeof document !== 'undefined' && document.body) {
|
||||
observer = new MutationObserver(() => setTimeout(finish, 0));
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
} catch {
|
||||
/* the deadline alone is enough */
|
||||
}
|
||||
setTimeout(finish, timeoutMs);
|
||||
}
|
||||
|
||||
function navigateHash(next: string, morph = true) {
|
||||
if (window.location.hash === next) return;
|
||||
|
||||
@@ -121,7 +149,7 @@ function navigateHash(next: string, morph = true) {
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
window.location.hash = next;
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
||||
afterRouteUpdate(resolve);
|
||||
}),
|
||||
);
|
||||
// A transition is *expected* to be skipped when one is already running (rapid
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { Session } from './types';
|
||||
|
||||
/**
|
||||
* Saving the chat list without hurting the page (#3604, #3603).
|
||||
*
|
||||
* Every streamed token used to stringify every chat and write the lot to localStorage. On a long history that is
|
||||
* megabytes of string work per token, and the WebView2 renderer grew to gigabytes before it died (the window goes
|
||||
* white). Three rules fix it, and the stored format is unchanged, so older builds and the flow scripts still read it:
|
||||
*
|
||||
* 1. Coalesce: the newest state wins and at most one write happens per `delayMs`. The caller flushes when a turn ends
|
||||
* or the page goes away.
|
||||
* 2. Only the chats that changed are serialised again. A chat is replaced by a new object when it changes (the store
|
||||
* never mutates), so the previous JSON of every other chat is reused by object identity.
|
||||
* 3. The stored copy is capped: a huge tool result or an old chat that no longer fits is left out of the saved copy,
|
||||
* never out of memory. The saved list is a cache, not the source of truth.
|
||||
*/
|
||||
|
||||
/** Longest text kept for one field in the saved copy. */
|
||||
export const MAX_FIELD_CHARS = 200_000;
|
||||
/** Longest saved copy of one chat (JSON characters). A longer chat is shortened from its oldest messages. */
|
||||
export const MAX_SESSION_CHARS = 1_500_000;
|
||||
/** Longest saved chat list (JSON characters). localStorage allows about 5M; keep a margin for everything else. */
|
||||
export const MAX_TOTAL_CHARS = 3_500_000;
|
||||
|
||||
type Json = unknown;
|
||||
|
||||
function clipFields(value: Json, depth = 0): Json {
|
||||
if (typeof value === 'string') return value.length > MAX_FIELD_CHARS ? `${value.slice(0, MAX_FIELD_CHARS)}\n[shortened in the saved copy]` : value;
|
||||
if (depth > 6 || value === null || typeof value !== 'object') return value;
|
||||
if (Array.isArray(value)) return value.map(v => clipFields(v, depth + 1));
|
||||
const out: Record<string, Json> = {};
|
||||
for (const [k, v] of Object.entries(value as Record<string, Json>)) out[k] = clipFields(v, depth + 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The JSON text kept for one chat: whole when it fits, otherwise clipped and then shortened from the oldest message. */
|
||||
export function serializeSession(session: Session): string {
|
||||
const whole = JSON.stringify(session);
|
||||
if (whole.length <= MAX_SESSION_CHARS) return whole;
|
||||
let copy = clipFields(session) as Session;
|
||||
let text = JSON.stringify(copy);
|
||||
if (text.length <= MAX_SESSION_CHARS) return text;
|
||||
let messages = copy.messages ?? [];
|
||||
// Drop the oldest messages in halves until it fits; always keep the latest two.
|
||||
while (text.length > MAX_SESSION_CHARS && messages.length > 2) {
|
||||
messages = messages.slice(Math.max(1, Math.floor(messages.length / 2)));
|
||||
copy = { ...copy, messages };
|
||||
text = JSON.stringify(copy);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
export interface SessionWriterOptions {
|
||||
/** Where the list is saved (the storage write). Receives the finished JSON text. */
|
||||
write: (json: string) => void;
|
||||
delayMs?: number;
|
||||
maxTotalChars?: number;
|
||||
/** Counts a serialisation of one chat; for tests. */
|
||||
onSerialize?: (session: Session) => void;
|
||||
}
|
||||
|
||||
export function createSessionWriter(options: SessionWriterOptions) {
|
||||
const delayMs = options.delayMs ?? 1_000;
|
||||
const maxTotal = options.maxTotalChars ?? MAX_TOTAL_CHARS;
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
let pending: Session[] | null = null;
|
||||
// Chat object -> its JSON. WeakMap: a replaced chat is forgotten with its object.
|
||||
const cache = new WeakMap<Session, string>();
|
||||
|
||||
const textOf = (s: Session): string => {
|
||||
let text = cache.get(s);
|
||||
if (text === undefined) {
|
||||
options.onSerialize?.(s);
|
||||
text = serializeSession(s);
|
||||
cache.set(s, text);
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const flush = () => {
|
||||
if (timer !== null) { clearTimeout(timer); timer = null; }
|
||||
const sessions = pending;
|
||||
pending = null;
|
||||
if (!sessions) return;
|
||||
const parts = sessions.map(s => ({ s, text: textOf(s) }));
|
||||
let total = parts.reduce((n, p) => n + p.text.length + 1, 2);
|
||||
if (total > maxTotal) {
|
||||
// Leave the oldest unpinned chats out of the saved copy until the rest fits. The newest chat is always kept.
|
||||
const order = parts
|
||||
.map((p, i) => ({ i, p }))
|
||||
.sort((a, b) => Number(!!a.p.s.pinned) - Number(!!b.p.s.pinned) || (a.p.s.updatedAt < b.p.s.updatedAt ? -1 : 1));
|
||||
const drop = new Set<number>();
|
||||
for (const { i, p } of order) {
|
||||
if (total <= maxTotal || drop.size >= parts.length - 1) break;
|
||||
drop.add(i);
|
||||
total -= p.text.length + 1;
|
||||
}
|
||||
options.write(`[${parts.filter((_, i) => !drop.has(i)).map(p => p.text).join(',')}]`);
|
||||
return;
|
||||
}
|
||||
options.write(`[${parts.map(p => p.text).join(',')}]`);
|
||||
};
|
||||
|
||||
const schedule = (sessions: Session[]) => {
|
||||
pending = sessions;
|
||||
if (timer === null) timer = setTimeout(flush, delayMs);
|
||||
};
|
||||
|
||||
return { schedule, flush, hasPending: () => pending !== null };
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { Session, Workspace } from './types';
|
||||
|
||||
/** The workspace a chat was made in: the sidebar groups chats by this same rule (the saved path, or an earlier folder of the workspace). */
|
||||
export function workspaceIdForSession(workspaces: Workspace[], session: Pick<Session, 'workspace'> | null | undefined): string | null {
|
||||
const path = session?.workspace;
|
||||
if (!path) return null;
|
||||
return workspaces.find(w => w.path === path || !!w.aliases?.includes(path))?.id ?? null;
|
||||
}
|
||||
+97
-11
@@ -1,3 +1,4 @@
|
||||
import { workspaceIdForSession } from './session-workspace';
|
||||
import { plainFailure, plainFailureText } from './plain-errors';
|
||||
import { showcaseMessages, SHOWCASE_AGENTS } from './chat-showcase';
|
||||
import { updateAgent } from './agents';
|
||||
@@ -19,6 +20,7 @@ import { noModelNotice } from './no-model';
|
||||
import { taskHandoverMessage, taskHandoverTitle, chatHandoverMessage, chatHandoverTitle } from './task-handover';
|
||||
import type { Task } from './tasks';
|
||||
import { postWithFolderFallback } from './folder-pick';
|
||||
import { createSessionWriter } from './session-persist';
|
||||
import { setTranslationLanguage, tr, validLanguage, type Language } from './i18n';
|
||||
/**
|
||||
* Strata app store.
|
||||
@@ -720,7 +722,7 @@ export async function bootstrap() {
|
||||
skills,
|
||||
workspacePath: workspaceState.workspaces[0]?.path ?? '',
|
||||
workspaces: workspaceState.workspaces,
|
||||
activeWorkspace: workspaceState.workspaces.some(w => w.id === state.activeWorkspace) ? state.activeWorkspace : workspaceState.workspaces[0]?.id ?? null,
|
||||
activeWorkspace: workspaceState.workspaces.some(w => w.id === state.activeWorkspace) ? state.activeWorkspace : workspaceIdForSession(workspaceState.workspaces, state.sessions.find(s => s.id === state.activeSession)) ?? workspaceState.workspaces[0]?.id ?? null,
|
||||
connection: 'live',
|
||||
});
|
||||
applyProviders(providers);
|
||||
@@ -1008,12 +1010,33 @@ export async function scopeSkill(id: string, workspaceId: string | null) {
|
||||
|
||||
/* ---------------------------------------------------------------- sessions */
|
||||
|
||||
function persistSessions(sessions: Session[]) {
|
||||
try {
|
||||
localStorage.setItem(SESSION_KEY, JSON.stringify(sessions));
|
||||
} catch {
|
||||
/* quota — sessions are a cache, not the source of truth */
|
||||
}
|
||||
const sessionWriter = createSessionWriter({
|
||||
write: (json) => {
|
||||
try {
|
||||
localStorage.setItem(SESSION_KEY, json);
|
||||
} catch {
|
||||
/* quota — sessions are a cache, not the source of truth */
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Save the chat list. A one-off change (new, rename, delete, pin) is written now; `soon` is for the streaming
|
||||
* path, where a write per token grew the renderer to gigabytes (#3604, #3603): those are coalesced to one a second.
|
||||
*/
|
||||
function persistSessions(sessions: Session[], soon = false) {
|
||||
sessionWriter.schedule(sessions);
|
||||
if (!soon) sessionWriter.flush();
|
||||
}
|
||||
|
||||
/** Write the newest chat list now: a turn ended, or the page is going away. */
|
||||
export function flushSessions() {
|
||||
sessionWriter.flush();
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && typeof document !== 'undefined' && window.addEventListener) {
|
||||
window.addEventListener('pagehide', flushSessions);
|
||||
document.addEventListener?.('visibilitychange', () => { if (document.visibilityState === 'hidden') flushSessions(); });
|
||||
}
|
||||
|
||||
export function newSession(): Session {
|
||||
@@ -1082,7 +1105,11 @@ export function continueTaskInNewChat(task: Task) {
|
||||
}
|
||||
|
||||
export function selectSession(id: string) {
|
||||
set({ activeSession: id });
|
||||
// A chat comes back in the workspace it was made in, so its next turn runs in the same folder (#3754).
|
||||
const workspace = workspaceIdForSession(state.workspaces, state.sessions.find(s => s.id === id));
|
||||
const change = workspace && workspace !== state.activeWorkspace;
|
||||
set(change ? { activeSession: id, activeWorkspace: workspace } : { activeSession: id });
|
||||
if (change) void loadWorkspaceFiles();
|
||||
markRead(id);
|
||||
}
|
||||
|
||||
@@ -1120,7 +1147,7 @@ export function renameSession(id: string, title: string) {
|
||||
|
||||
function patchSession(id: string, fn: (s: Session) => Session) {
|
||||
const sessions = state.sessions.map((s) => (s.id === id ? fn(s) : s));
|
||||
persistSessions(sessions);
|
||||
persistSessions(sessions, true);
|
||||
set({ sessions });
|
||||
}
|
||||
|
||||
@@ -1366,6 +1393,55 @@ export async function retryLast(messageId?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/** The open chat's last question and the answer that failed under it (null when the chat does not end that way). */
|
||||
function failedExchange(): { prompt: string; sessionId: string } | null {
|
||||
const sessionId = state.activeSession;
|
||||
const chat = state.sessions.find(s => s.id === sessionId);
|
||||
if (!sessionId || !chat || state.streaming) return null;
|
||||
const reply = chat.messages[chat.messages.length - 1], ask = chat.messages[chat.messages.length - 2];
|
||||
return reply?.role === 'assistant' && reply.error && ask?.role === 'user' ? { prompt: ask.content, sessionId } : null;
|
||||
}
|
||||
/** Takes the failed question and its error out of the transcript, so asking again does not show the question twice. */
|
||||
function dropFailedExchange(sessionId: string) {
|
||||
patchSession(sessionId, s => ({ ...s, messages: s.messages.slice(0, -2) }));
|
||||
}
|
||||
|
||||
/**
|
||||
* The error card of a conversation that no longer fits the model's window: summarise the older messages (the same as typing /compact), then
|
||||
* ask the assistant to answer. The refused question is already in the assistant's conversation (it is the newest message, and a summary keeps
|
||||
* the newest message as it is), so it is not sent a second time: a large one twice would not fit either.
|
||||
*/
|
||||
export async function summarizeAndContinue() {
|
||||
if (!failedExchange()) return;
|
||||
await sendMessage('/compact');
|
||||
await sendMessage('Please answer my last message now.');
|
||||
}
|
||||
|
||||
/**
|
||||
* The error card of a message that is too large to send: the host makes it small enough (its text as a file in the chat's folder that the
|
||||
* assistant reads in parts, or a shortened preview), and that is sent in its place.
|
||||
*/
|
||||
export async function sendOversized(mode: 'file' | 'preview') {
|
||||
const failed = failedExchange();
|
||||
if (!failed) return;
|
||||
let reply: { message?: string };
|
||||
try {
|
||||
reply = await api.post<{ message?: string }>('/api/chat/oversize', { sessionId: failed.sessionId, mode, text: failed.prompt, nativeModelId: nativeState()?.instances.length ? nativeState()?.defaultId : undefined });
|
||||
} catch {
|
||||
pushToast('error', 'The message could not be prepared. Try a shorter message.');
|
||||
return;
|
||||
}
|
||||
if (!reply.message) return;
|
||||
dropFailedExchange(failed.sessionId);
|
||||
await sendMessage(reply.message);
|
||||
}
|
||||
|
||||
/** A new, empty chat, opened. */
|
||||
export function startNewChat() {
|
||||
const session = newSession();
|
||||
go('chat', session.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forks a conversation at a message, leaving the original untouched.
|
||||
*
|
||||
@@ -1657,7 +1733,7 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
|
||||
// The Mode menu's choice for this chat, read when the turn starts; the host applies it to the agent.
|
||||
mode:state.sessions.find(s=>s.id===sessionId)?.preferences?.mode??'manual',
|
||||
// So a skill narrowed to another workspace does not fire here.
|
||||
workspaceId: state.activeWorkspace,
|
||||
workspaceId: workspaceIdForSession(state.workspaces, state.sessions.find(s => s.id === sessionId)) ?? state.activeWorkspace,
|
||||
},
|
||||
(frame) => {
|
||||
// A line from the app itself carries `key` (the English sentence): shown in the person's language.
|
||||
@@ -1733,9 +1809,15 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
|
||||
tps: typeof frame.tps === 'number' ? frame.tps : m.tps,
|
||||
promptTps: typeof frame.promptTps === 'number' ? frame.promptTps : m.promptTps,
|
||||
}));
|
||||
} else if (type === 'context') {
|
||||
// What the context holds after each model request of an agent turn: the meter follows the turn instead of waiting for its end.
|
||||
const used = Number(frame.contextTokens), max = Number(frame.contextMax);
|
||||
if (Number.isFinite(used) && used > 0) patchAssistant((m) => ({ ...m, contextUsed: used, ...(Number.isFinite(max) && max > 0 ? { contextMax: max } : {}) }));
|
||||
} else if (type === 'error') {
|
||||
clearStatus(sessionId);
|
||||
patchAssistant((m) => ({ ...m, error: plainFailure(String(frame.message ?? 'Engine error')).message, streaming: false }));
|
||||
// A sentence the host wrote for the person (it carries its catalog key) is shown in their language as it is; anything else is made plain here.
|
||||
const said = typeof frame.key === 'string' ? tr(frame.key, Array.isArray(frame.values) ? frame.values : undefined) : plainFailure(String(frame.message ?? 'Engine error')).message;
|
||||
patchAssistant((m) => ({ ...m, error: said, streaming: false, ...(typeof frame.code === 'string' ? { errorCode: frame.code } : {}), ...(typeof frame.details === 'string' && frame.details ? { errorDetail: frame.details } : {}) }));
|
||||
} else if (type === 'done') {
|
||||
clearStatus(sessionId);
|
||||
patchSession(sessionId, (sess) => ({
|
||||
@@ -1756,6 +1838,8 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
|
||||
tps: typeof frame.tps === 'number' ? frame.tps : m.tps,
|
||||
promptTps: typeof frame.promptTps === 'number' ? frame.promptTps : m.promptTps,
|
||||
}));
|
||||
// The final text is in `state`: write it once now instead of waiting for the coalescing timer.
|
||||
flushSessions();
|
||||
}
|
||||
},
|
||||
turnAbort.signal,
|
||||
@@ -1772,6 +1856,8 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
|
||||
patchAssistant((m) => ({ ...m, streaming: false }));
|
||||
}
|
||||
} finally {
|
||||
// A cancelled or failed turn is still worth keeping.
|
||||
flushSessions();
|
||||
clearStatus(sessionId);
|
||||
markTurn(sessionId, null);
|
||||
const next = releasedMessages.get(sessionId)?.shift();
|
||||
|
||||
@@ -8,6 +8,8 @@ export interface SoftwareUpdate {
|
||||
releaseUrl?: string;
|
||||
canInstall: boolean;
|
||||
message: string;
|
||||
/** The technical reason behind `message`, shown only behind the Details disclosure. */
|
||||
details?: string;
|
||||
}
|
||||
export interface UpdateCheckResult { checkedAt: string; strata: SoftwareUpdate; engine: SoftwareUpdate }
|
||||
|
||||
@@ -358,6 +360,10 @@ export interface ChatMessage {
|
||||
promptTps?: number;
|
||||
durationMs?: number;
|
||||
error?: string;
|
||||
/** What went wrong, as a code the page acts on (a context error offers its own recovery buttons). */
|
||||
errorCode?: string;
|
||||
/** The engine's own text of the error, kept behind Details. */
|
||||
errorDetail?: string;
|
||||
tools?: ToolCall[];
|
||||
}
|
||||
|
||||
|
||||
@@ -3489,5 +3489,42 @@
|
||||
"s8643": "Nur Ihr Prozessor und Arbeitsspeicher werden verwendet.",
|
||||
"s8644": "Für Experten: Die Engine startet auf den Karten {0} (Hauptkarte zuletzt).",
|
||||
"s8645": "Wird nicht gespeichert, solange Sie die Empfehlung behalten: Strata wählt neu, wenn Sie eine Karte hinzufügen oder entfernen.",
|
||||
"s8646": "Nicht genutzt: {0} (anderer Grafikkarten-Hersteller als der, den diese Engine ansteuert)."
|
||||
"s8646": "Nicht genutzt: {0} (anderer Grafikkarten-Hersteller als der, den diese Engine ansteuert).",
|
||||
"s9001": "Diese Nachricht ist etwa {0} Tokens lang, und dieses Modell kann etwa {1} auf einmal lesen. Sie ist zu groß, um so gesendet zu werden.",
|
||||
"s9002": "Hängen Sie sie als Datei an, die der Assistent in Teilen liest, oder senden Sie eine gekürzte Vorschau.",
|
||||
"s9003": "Diese Unterhaltung passt nicht mehr in das Fenster des Modells, und das Zusammenfassen der älteren Nachrichten hat nicht genug Platz geschaffen.",
|
||||
"s9004": "Fassen Sie sie jetzt zusammen und machen Sie weiter, oder beginnen Sie einen neuen Chat.",
|
||||
"s9005": "Frühere Nachrichten werden zusammengefasst …",
|
||||
"s9006": "Ältere Nachrichten wurden zusammengefasst, um Platz zu schaffen. Der Chat war zu {0} % voll und ist jetzt zu {1} % voll.",
|
||||
"s9007": "Ältere Nachrichten wurden zusammengefasst, um Platz zu schaffen. Der Chat war zu {0} % voll.",
|
||||
"s9008": "Ältere Nachrichten wurden zusammengefasst, um Platz zu schaffen.",
|
||||
"s9009": "Dieser Chat ist zu {0} % voll, aber es gibt nichts Älteres zusammenzufassen: Die neueste Nachricht füllt ihn. Beginnen Sie einen neuen Chat oder senden Sie eine kürzere Nachricht.",
|
||||
"s9010": "Es gibt noch nichts Älteres zusammenzufassen. Alles in diesem Chat braucht der Assistent noch.",
|
||||
"s9011": "Es gibt noch nichts zusammenzufassen. In diesem Chat hat noch keine Unterhaltung mit dem Assistenten begonnen.",
|
||||
"s9012": "Die älteren Nachrichten konnten gerade nicht zusammengefasst werden. Versuchen Sie es gleich noch einmal oder beginnen Sie einen neuen Chat.",
|
||||
"s9013": "In diesem Chat läuft noch etwas. Halten Sie ihn an oder warten Sie, bis er fertig ist, und versuchen Sie es dann erneut.",
|
||||
"s9014": "Als Datei anhängen",
|
||||
"s9015": "Gekürzte Vorschau senden",
|
||||
"s9016": "Zusammenfassen und weitermachen",
|
||||
"s9017": "Neuen Chat beginnen",
|
||||
"s9018": "Die Nachricht konnte nicht vorbereitet werden. Versuchen Sie es mit einer kürzeren Nachricht.",
|
||||
"s9019": "Die letzte Strata-Sitzung wird noch beendet.",
|
||||
"s9020": "Warten Sie einen Moment und versuchen Sie es erneut. Wenn es wieder passiert, starten Sie den Computer neu.",
|
||||
"s9021": "Vorausraten",
|
||||
"s9022": "Automatisch (empfohlen)",
|
||||
"s9023": "MTP",
|
||||
"s9024": "DFlash2",
|
||||
"s9025": "Vorausraten: MTP (DFlash2 hat nicht hineingepasst)",
|
||||
"s9026": "Vorausraten: MTP (DFlash2 ist in diesem Modus nicht verfügbar)",
|
||||
"s9027": "Vorausraten: MTP (DFlash2 gibt es nur für Qwen3.8-Flash-Next)",
|
||||
"s9028": "Vorausraten: MTP (der DFlash2-Entwurfsbaustein ist noch nicht heruntergeladen)",
|
||||
"s9029": "Wenn Sie DFlash2 wählen, wird auch sein Entwurfsbaustein heruntergeladen: etwa {0} GB, einmalig. Passt er nicht in den Grafikspeicher, nutzt das Modell stattdessen MTP.",
|
||||
"s9030": "DFlash2-Entwurfsbaustein wird heruntergeladen …",
|
||||
"s9031": "DFlash2-Entwurfsbaustein wird für die Engine vorbereitet …",
|
||||
"s9032": "Der DFlash2-Entwurfsbaustein ist bereit.",
|
||||
"s9033": "Der DFlash2-Entwurfsbaustein konnte nicht heruntergeladen werden, deshalb startet das Modell mit MTP. Das Modell wird geladen …",
|
||||
"s9034": "Das Modell wird geladen …",
|
||||
"s9035": "Diese Modelldateien fehlen: {0}.",
|
||||
"s9036": "Drücken Sie noch einmal auf „Herunterladen und laden“, um sie zu holen. Vorhandene Dateien bleiben erhalten. Werden dieselben Dateien wieder aufgelistet, öffnen Sie die Details und teilen Sie sie mit, wenn Sie um Hilfe bitten.",
|
||||
"s9037": "Dieses Update wurde beim Neustart der App unterbrochen. Ein späteres Update ist abgeschlossen, Sie müssen nichts tun."
|
||||
}
|
||||
@@ -3489,5 +3489,42 @@
|
||||
"s8643": "Only your processor and memory are used.",
|
||||
"s8644": "For experts: the engine starts on cards {0} (Main last).",
|
||||
"s8645": "Not saved while you keep the recommendation: Strata picks again if you add or remove a card.",
|
||||
"s8646": "Not used: {0} (another make of graphics card than this engine drives)."
|
||||
"s8646": "Not used: {0} (another make of graphics card than this engine drives).",
|
||||
"s9001": "This message is about {0} tokens long, and this model can read about {1} at a time. It is too large to send as it is.",
|
||||
"s9002": "Attach it as a file for the assistant to read in parts, or send a shortened preview.",
|
||||
"s9003": "This conversation no longer fits the model’s window, and summarizing the older messages did not make enough room.",
|
||||
"s9004": "Summarize it now and continue, or start a new chat.",
|
||||
"s9005": "Summarizing earlier messages…",
|
||||
"s9006": "Older messages were summarized to make room. The chat was {0}% full and is now {1}% full.",
|
||||
"s9007": "Older messages were summarized to make room. The chat was {0}% full.",
|
||||
"s9008": "Older messages were summarized to make room.",
|
||||
"s9009": "This chat is {0}% full, but there is nothing older to summarize: the newest message is what fills it. Start a new chat, or send a shorter message.",
|
||||
"s9010": "There is nothing older to summarize yet. Everything in this chat is still needed by the assistant.",
|
||||
"s9011": "There is nothing to summarize yet. This chat has not started a conversation with the assistant.",
|
||||
"s9012": "The older messages could not be summarized right now. Try again in a moment, or start a new chat.",
|
||||
"s9013": "This chat is still working. Stop it or wait for it to finish, then try again.",
|
||||
"s9014": "Attach as a file",
|
||||
"s9015": "Send a shortened preview",
|
||||
"s9016": "Summarize and continue",
|
||||
"s9017": "Start a new chat",
|
||||
"s9018": "The message could not be prepared. Try a shorter message.",
|
||||
"s9019": "The last Strata session is still closing down.",
|
||||
"s9020": "Wait a moment and try again. If it keeps happening, restart the computer.",
|
||||
"s9021": "Drafting",
|
||||
"s9022": "Auto (recommended)",
|
||||
"s9023": "MTP",
|
||||
"s9024": "DFlash2",
|
||||
"s9025": "Drafting: MTP (DFlash2 didn't fit)",
|
||||
"s9026": "Drafting: MTP (DFlash2 isn't available in this mode)",
|
||||
"s9027": "Drafting: MTP (DFlash2 is for Qwen3.8-Flash-Next only)",
|
||||
"s9028": "Drafting: MTP (the DFlash2 drafter isn't downloaded yet)",
|
||||
"s9029": "Choosing DFlash2 also downloads its drafter: about {0} GB, once. If it does not fit in graphics memory, the model uses the MTP instead.",
|
||||
"s9030": "Downloading the DFlash2 drafter…",
|
||||
"s9031": "Packing the DFlash2 drafter for the engine…",
|
||||
"s9032": "The DFlash2 drafter is ready.",
|
||||
"s9033": "The DFlash2 drafter could not be downloaded, so the model starts with the MTP. Loading the model…",
|
||||
"s9034": "Loading the model…",
|
||||
"s9035": "These model files are missing: {0}.",
|
||||
"s9036": "Press Download & load again to fetch them. Files that are already there are kept. If the same files are listed again, open Details and share them when you ask for help.",
|
||||
"s9037": "This update was cut short when the app restarted. A later update finished, so nothing needs doing."
|
||||
}
|
||||
@@ -3489,5 +3489,42 @@
|
||||
"s8643": "Solo se usan tu procesador y tu memoria.",
|
||||
"s8644": "Para expertos: el motor arranca en las tarjetas {0} (la principal al final).",
|
||||
"s8645": "No se guarda mientras mantengas la recomendación: Strata elige de nuevo si añades o quitas una tarjeta.",
|
||||
"s8646": "No se usa: {0} (otra marca de tarjeta gráfica distinta de la que maneja este motor)."
|
||||
"s8646": "No se usa: {0} (otra marca de tarjeta gráfica distinta de la que maneja este motor).",
|
||||
"s9001": "Este mensaje tiene unos {0} tokens y este modelo puede leer unos {1} a la vez. Es demasiado grande para enviarlo tal cual.",
|
||||
"s9002": "Adjúntalo como archivo para que el asistente lo lea por partes, o envía una vista previa acortada.",
|
||||
"s9003": "Esta conversación ya no cabe en la ventana del modelo, y resumir los mensajes antiguos no dejó espacio suficiente.",
|
||||
"s9004": "Resúmela ahora y sigue, o empieza un chat nuevo.",
|
||||
"s9005": "Resumiendo mensajes anteriores…",
|
||||
"s9006": "Se resumieron los mensajes antiguos para hacer sitio. El chat estaba al {0} % y ahora está al {1} %.",
|
||||
"s9007": "Se resumieron los mensajes antiguos para hacer sitio. El chat estaba al {0} %.",
|
||||
"s9008": "Se resumieron los mensajes antiguos para hacer sitio.",
|
||||
"s9009": "Este chat está al {0} %, pero no hay nada antiguo que resumir: el mensaje más reciente es lo que lo llena. Empieza un chat nuevo o envía un mensaje más corto.",
|
||||
"s9010": "Todavía no hay nada antiguo que resumir. El asistente aún necesita todo lo que hay en este chat.",
|
||||
"s9011": "Todavía no hay nada que resumir. Este chat aún no ha empezado una conversación con el asistente.",
|
||||
"s9012": "No se pudieron resumir los mensajes antiguos ahora mismo. Inténtalo de nuevo en un momento o empieza un chat nuevo.",
|
||||
"s9013": "Este chat sigue trabajando. Deténlo o espera a que termine y vuelve a intentarlo.",
|
||||
"s9014": "Adjuntar como archivo",
|
||||
"s9015": "Enviar una vista previa acortada",
|
||||
"s9016": "Resumir y continuar",
|
||||
"s9017": "Empezar un chat nuevo",
|
||||
"s9018": "No se pudo preparar el mensaje. Prueba con un mensaje más corto.",
|
||||
"s9019": "La última sesión de Strata todavía se está cerrando.",
|
||||
"s9020": "Espera un momento y vuelve a intentarlo. Si sigue pasando, reinicia el ordenador.",
|
||||
"s9021": "Predicción",
|
||||
"s9022": "Automático (recomendado)",
|
||||
"s9023": "MTP",
|
||||
"s9024": "DFlash2",
|
||||
"s9025": "Predicción: MTP (DFlash2 no cabía)",
|
||||
"s9026": "Predicción: MTP (DFlash2 no está disponible en este modo)",
|
||||
"s9027": "Predicción: MTP (DFlash2 es solo para Qwen3.8-Flash-Next)",
|
||||
"s9028": "Predicción: MTP (el predictor de DFlash2 aún no está descargado)",
|
||||
"s9029": "Elegir DFlash2 también descarga su predictor: unos {0} GB, una sola vez. Si no cabe en la memoria gráfica, el modelo usa MTP.",
|
||||
"s9030": "Descargando el predictor de DFlash2…",
|
||||
"s9031": "Preparando el predictor de DFlash2 para el motor…",
|
||||
"s9032": "El predictor de DFlash2 está listo.",
|
||||
"s9033": "No se pudo descargar el predictor de DFlash2, así que el modelo arranca con MTP. Cargando el modelo…",
|
||||
"s9034": "Cargando el modelo…",
|
||||
"s9035": "Faltan estos archivos del modelo: {0}.",
|
||||
"s9036": "Pulse de nuevo «Descargar y cargar» para obtenerlos. Los archivos que ya están se conservan. Si vuelven a aparecer los mismos archivos, abra los detalles y compártalos cuando pida ayuda.",
|
||||
"s9037": "Esta actualización se interrumpió cuando la aplicación se reinició. Una actualización posterior terminó, así que no hay nada que hacer."
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"fx01": "wird nicht mehr ausgeführt und entfernt.",
|
||||
"fx02": "Heruntergeladen, aber nicht geladen. Öffne „Modelle“, um es zu laden.",
|
||||
"fx03": "{0} Zeilen hinzugefügt, {1} entfernt",
|
||||
"fx04": "{0} Dateien bearbeitet",
|
||||
"fx05": "Dateien hier ablegen, um sie hinzuzufügen",
|
||||
"fx06": "Bilder und Textdateien",
|
||||
"fx07": "Ziel behalten",
|
||||
"fx08": "Ziel entfernen",
|
||||
"fx09": "Der Assistent arbeitet nicht mehr darauf hin. Dein Chat und deine Dateien bleiben unverändert.",
|
||||
"fx10": "Dieses Modell kann Bilder lesen, aber die hier installierte Engine enthält den Bildleser noch nicht, deshalb ist „Bilder lesen“ aus. Aktualisiere die Engine unter Einstellungen → Info, um es einzuschalten.",
|
||||
"fx11": "Zeigt die Werte deiner Abstimmung „{0}“.",
|
||||
"fx12": "Deine Abstimmung „{0}“ setzt",
|
||||
"fx13": "war {0}",
|
||||
"fx14": "· {0} RAM",
|
||||
"fx15": "Die Nachricht wurde nicht abgebrochen",
|
||||
"fx16": "Zusätzliche Einstellungen für das Programm",
|
||||
"fx17": "Mit KI entwerfen",
|
||||
"fx18": "Denkzeit",
|
||||
"fx19": "Schreibzeit",
|
||||
"fx20": "Token/s",
|
||||
"fx21": "% des Kontexts",
|
||||
"fx22": "Diese Seite lädt Dateien aus dem Internet, was die Vorschau nicht tut. Sie kann deshalb unvollständig aussehen. „Code anzeigen“ zeigt genau, was geschrieben wurde.",
|
||||
"fx23": "A behalten",
|
||||
"fx24": "B behalten",
|
||||
"fx25": "Dauer",
|
||||
"fx26": "Schwankung",
|
||||
"fx27": "Geändert",
|
||||
"fx28": "Nichts",
|
||||
"fx29": "Beide Versionen sind fertig. Vergleiche A und B und behalte eine.",
|
||||
"fx30": "Der Assistent antwortet noch",
|
||||
"fx31": "Sende deine Nachricht, wenn er fertig ist.",
|
||||
"fx32": "Die Nachricht wurde nicht eingereiht",
|
||||
"fx33": "Zur neuesten Nachricht scrollen",
|
||||
"fx34": "Aufgabe entfernen",
|
||||
"fx35": "Neue Unterhaltung beginnen; diese bleibt erhalten",
|
||||
"fx36": "Der Assistent schreibt zuerst einen Plan; nichts läuft, bis du ihn freigibst",
|
||||
"fx37": "Im Web suchen, die Seiten lesen und mit Quellen antworten",
|
||||
"fx38": "Kopiert",
|
||||
"fx39": "Antwort",
|
||||
"fx40": "Nachricht",
|
||||
"fx41": "PDF hinzugefügt",
|
||||
"fx42": "Markiere den Text und kopiere ihn mit Strg+C.",
|
||||
"fx43": "{0} ist in diesem Chat. Frag in deiner nächsten Nachricht danach.",
|
||||
"fx44": "Die Engine ist bereit.",
|
||||
"fx45": "Als Nächstes: ein Modell auswählen.",
|
||||
"fx46": "Die Engine wird heruntergeladen",
|
||||
"fx47": "Du kannst Strata schließen; der Download geht dort weiter, wo er aufgehört hat.",
|
||||
"fx48": "Willkommen bei Strata",
|
||||
"fx49": "Deine Grafikkarte wird erkannt…",
|
||||
"fx50": "Strata hat deine Grafikkarte gefunden:",
|
||||
"fx51": "{0} Grafikkarten gefunden. Strata nimmt für die Engine die erste; ändere das später in den Einstellungen.",
|
||||
"fx52": "Strata hat keine unterstützte Grafikkarte erkannt.",
|
||||
"fx53": "Um darauf Modelle auszuführen, braucht Strata einen Download:",
|
||||
"fx54": "Die Engine für diese Grafikkarte",
|
||||
"fx55": "Die Engine braucht etwa {0} GB freien Platz auf diesem Laufwerk, frei sind nur {1} GB. Schaffe Platz und mach dann weiter.",
|
||||
"fx56": "Sie kommt von Stratas eigenem Update-Server und wird vor der Nutzung geprüft. Deine Modelle und Chats bleiben unberührt.",
|
||||
"fx57": "Strata konnte github.com nicht erreichen. Prüfe deine Verbindung und versuche es erneut, oder lade die Engine-Datei auf einem anderen PC herunter und wähle sie hier aus.",
|
||||
"fx58": "Die hier installierte Engine ist für einen anderen Grafikkarten-Hersteller.",
|
||||
"fx59": "Strata kann aus seiner Update-Liste keine Engine für diesen PC wählen.",
|
||||
"fx60": "Ohne Engine fortfahren",
|
||||
"fx61": "Der Download wurde unterbrochen. „Weiter“ setzt ihn dort fort, wo er aufgehört hat.",
|
||||
"fx62": "Das ist ein sehr großes Modell. Der Download umfasst {0} GB und braucht etwa so viel freien Speicherplatz. Es läuft auf einer Grafikkarte mit 12 GB und 46 bis 64 GB RAM, langsam: etwa 5 bis 7 Wörter pro Sekunde. Was nicht in den Speicher passt, wird von deiner SSD gelesen, deshalb zählt eine NVMe-SSD.",
|
||||
"fx63": "Du kannst den Download stoppen und später fortsetzen; er macht dort weiter, wo er aufgehört hat. Strata warnt dich hier, hält dich aber nie auf.",
|
||||
"fx64": "Schneller",
|
||||
"fx65": "Volle Qualität",
|
||||
"fx66": "Standard (Schneller auf diesem PC)",
|
||||
"fx67": "Standard (Volle Qualität auf diesem PC)",
|
||||
"fx68": "Schneller (Standard auf diesem PC)",
|
||||
"fx69": "Volle Qualität (Standard auf diesem PC)"
|
||||
}
|
||||
Loaded 100 of 577 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user