Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9de04cd44e | ||
|
|
31ca248fbe | ||
|
|
63214a5655 | ||
|
|
86620837b2 | ||
|
|
cbfd5b926b | ||
|
|
5b47f889b0 | ||
|
|
5db632e72a | ||
|
|
f5c4047e78 | ||
|
|
dea6920b81 | ||
|
|
9e24f4ab32 | ||
|
|
dcbaa992d2 | ||
|
|
79cb59c574 | ||
|
|
ae456dfe5c | ||
|
|
176e11a597 | ||
|
|
5dc151d4d3 | ||
|
|
0dbea601a0 | ||
|
|
3d5594d656 | ||
|
|
284552fe26 | ||
|
|
c24f2a139f | ||
|
|
bd4c6bf1af | ||
|
|
fa56c2b387 | ||
|
|
cf34e915c4 | ||
|
|
14be49605f | ||
|
|
6b29adad1a | ||
|
|
e728a132d7 | ||
|
|
32b9f75376 | ||
|
|
1972af6da1 | ||
|
|
7db513297d | ||
|
|
bf2926e05a | ||
|
|
4dee320dd5 | ||
|
|
c316a7c215 | ||
|
|
73b68dfd88 | ||
|
|
ce938fb8c9 | ||
|
|
fb2f387407 | ||
|
|
9d2870d187 | ||
|
|
b611ec5f1e | ||
|
|
ddbbda4824 | ||
|
|
386155fa25 | ||
|
|
ff64e08bba | ||
|
|
a569d80680 | ||
|
|
461d0baee7 | ||
|
|
ef5d8ab946 | ||
|
|
bcda2e4b0d | ||
|
|
34d6c37df3 | ||
|
|
f207907b14 | ||
|
|
7ebdd34356 | ||
|
|
aa44b9ce53 | ||
|
|
af189129a4 | ||
|
|
c6a0e739be | ||
|
|
e52668857c | ||
|
|
c0c3a06ce4 | ||
|
|
f39d951904 | ||
|
|
968fcb70b0 | ||
|
|
9ae93f7b1f | ||
|
|
9f13d1e78f | ||
|
|
a0abdf7588 | ||
|
|
c093ac263f | ||
|
|
436fbd0a6e | ||
|
|
5bda973a43 | ||
|
|
90767252f4 | ||
|
|
a7bdbfdabc | ||
|
|
9944d2f7f1 | ||
|
|
b42ec662c7 | ||
|
|
b35a9816c7 | ||
|
|
d33bb832ca | ||
|
|
3e7c5d8e1c | ||
|
|
894d1be1f4 | ||
|
|
93aa62ed64 | ||
|
|
83027df587 | ||
|
|
484e06b961 | ||
|
|
ed4595ad69 | ||
|
|
06d7604af7 | ||
|
|
ecf2bdcb7f | ||
|
|
3f1fa67f14 | ||
|
|
881a42adab | ||
|
|
3616148bbf | ||
|
|
24e56539c5 | ||
|
|
8e957cd53b | ||
|
|
4f6ec303b5 | ||
|
|
1f52a00373 | ||
|
|
e68a032a58 | ||
|
|
10715d856d | ||
|
|
e45a9bfd3a | ||
|
|
34e7c5cb1a | ||
|
|
8aaf188aa9 | ||
|
|
22703894ec | ||
|
|
b1713b3ff2 | ||
|
|
0875029d10 | ||
|
|
952cce7752 | ||
|
|
30c64f09d7 | ||
|
|
cbc0e329c6 | ||
|
|
f680a80d88 | ||
|
|
d8a6a0e012 | ||
|
|
3cc18d7bb7 | ||
|
|
1994f3acb2 | ||
|
|
c911065a14 | ||
|
|
40c605f7bc | ||
|
|
96a9107a27 | ||
|
|
4e76d6c9a6 | ||
|
|
8441cfd0e2 | ||
|
|
9cc2a9b6aa | ||
|
|
9b8436df4e | ||
|
|
c6a20b7e85 | ||
|
|
88b29d481a | ||
|
|
2efa4e067a | ||
|
|
eb7eac250b | ||
|
|
9f5488e3b2 | ||
|
|
cac4816afd | ||
|
|
4ed8eb02fa | ||
|
|
b203b5f71e | ||
|
|
468e3a6252 | ||
|
|
c6aa4223d9 | ||
|
|
c23a78694e | ||
|
|
509466a668 | ||
|
|
a10777a671 | ||
|
|
bcf67d1774 | ||
|
|
c8e5d2f3ce | ||
|
|
6d41b37e76 | ||
|
|
e1ef9df21c | ||
|
|
dab7ee72d0 | ||
|
|
6b28dbbd1e | ||
|
|
e0e0c0545a | ||
|
|
8acee5a055 | ||
|
|
dca9770ef4 | ||
|
|
ff18fbb868 | ||
|
|
cf4749276d | ||
|
|
578bb6aa7a | ||
|
|
1073929c80 | ||
|
|
e5dc00c81e | ||
|
|
1c3c6d3351 | ||
|
|
ce968a2fac | ||
|
|
40d1a3e7d7 | ||
|
|
a269d1b983 | ||
|
|
9d2cb4f6d2 | ||
|
|
74e25e165b | ||
|
|
f5690d51a8 | ||
|
|
e2071c2f8b | ||
|
|
f2345ebd66 | ||
|
|
671f4573da | ||
|
|
3d0a79ca23 | ||
|
|
f523a94227 | ||
|
|
267f35f777 | ||
|
|
71bdfda0b3 | ||
|
|
7662bf85c4 | ||
|
|
44a56865f5 | ||
|
|
7f037f4451 | ||
|
|
5c9884cdee | ||
|
|
5b4db785ca | ||
|
|
321f159032 | ||
|
|
54a02dc32c | ||
|
|
40bf5f5c40 | ||
|
|
bc54d945f6 | ||
|
|
7a2bfc52ae | ||
|
|
e88ee39d1d | ||
|
|
a5ee74ffa3 | ||
|
|
74652692e1 | ||
|
|
85f77658f3 | ||
|
|
f57c0adcb3 | ||
|
|
3fa1b524d7 | ||
|
|
76e51103c7 | ||
|
|
fcca2ccb84 | ||
|
|
eac694ae4c | ||
|
|
4caf857c76 | ||
|
|
e5d987a553 | ||
|
|
dde39e8a8f | ||
|
|
03b75b8da9 | ||
|
|
3d1fb48090 | ||
|
|
042cd201c9 | ||
|
|
b961c7aa8c | ||
|
|
6ec549d2dd | ||
|
|
4ff5972fcb | ||
|
|
04a2e6a3f1 | ||
|
|
c301209636 | ||
|
|
78bba245a5 | ||
|
|
18f7b90ca6 | ||
|
|
1b8e88b9e5 | ||
|
|
ef59c0b555 | ||
|
|
4ea8171636 | ||
|
|
3c3d2881b4 | ||
|
|
604e23cd9a | ||
|
|
41cc4b2e89 | ||
|
|
3d9c21509e | ||
|
|
779bc30501 | ||
|
|
d5bcc3bd8d | ||
|
|
116adbf7d6 | ||
|
|
19d5cd87fa | ||
|
|
2ca90e48ec | ||
|
|
adfb7ecb2a | ||
|
|
a122ba0bdf | ||
|
|
04221c3e2b | ||
|
|
fdb5e48616 | ||
|
|
af78949b17 | ||
|
|
935407d883 | ||
|
|
899d1b1b3a | ||
|
|
02a8193da7 | ||
|
|
c7bfd131b5 | ||
|
|
6f2a3e758d | ||
|
|
e193161de2 | ||
|
|
06b6b23e51 | ||
|
|
553c25ca7c | ||
|
|
8ac47258bb | ||
|
|
dbd1cb9af8 | ||
|
|
94f095c893 | ||
|
|
a4c1644a37 | ||
|
|
aa82a88639 | ||
|
|
9962aae5b7 | ||
|
|
cbbdd16b43 | ||
|
|
25492e4a6b | ||
|
|
7e3a9fec71 | ||
|
|
2a6650a364 | ||
|
|
5e7b9a8301 | ||
|
|
2b209b406d | ||
|
|
6ff75059a9 | ||
|
|
e46be1fe0a | ||
|
|
f86ca9d1c5 | ||
|
|
477fd36bce | ||
|
|
193cbb1fbf | ||
|
|
76e4fef398 | ||
|
|
dac635445b | ||
|
|
046bdc83d9 | ||
|
|
667d3cf7d7 | ||
|
|
4794b13779 | ||
|
|
09b8b54d19 | ||
|
|
98b865c56f | ||
|
|
91baead754 | ||
|
|
80a10f31c3 | ||
|
|
b27c46cbd6 | ||
|
|
1c4970b9ac | ||
|
|
26613a44f6 | ||
|
|
afe92c1727 | ||
|
|
ad262c80aa | ||
|
|
14ec155ec7 | ||
|
|
e9e08adb37 | ||
|
|
fd0e3766a3 | ||
|
|
95f3807699 | ||
|
|
a2367ca55b | ||
|
|
e657d310e7 | ||
|
|
64af23dafc | ||
|
|
bccb0f4280 | ||
|
|
adc4a6822b | ||
|
|
3d2e7f166e | ||
|
|
545fccf007 | ||
|
|
d6aa6504e0 | ||
|
|
d5fda4d2f3 | ||
|
|
8667a3dbf7 | ||
|
|
b7b65edeb1 | ||
|
|
4440a303f8 | ||
|
|
e107297a8f | ||
|
|
6b8cfcd1a3 | ||
|
|
d060783fbb | ||
|
|
a3024ed10a | ||
|
|
c4345a7c03 | ||
|
|
10f3435b00 | ||
|
|
35698ffe70 | ||
|
|
e6ab4c024d | ||
|
|
23b08c7620 | ||
|
|
e23a56f2f8 | ||
|
|
b0b8860d13 | ||
|
|
1486151d4d | ||
|
|
97cb031bf7 | ||
|
|
c59505e638 | ||
|
|
34bd640a19 | ||
|
|
0f5511cfdf | ||
|
|
b3348da493 | ||
|
|
b31c13875e | ||
|
|
b2e3008bfb | ||
|
|
55ca7b955a | ||
|
|
979da9d2bc | ||
|
|
536c7f019e | ||
|
|
de9101935d | ||
|
|
f82fffbe25 | ||
|
|
b808d6e442 | ||
|
|
75127c794e | ||
|
|
96658aebae | ||
|
|
fb14c354b4 | ||
|
|
43ea31544b | ||
|
|
a1788c54be | ||
|
|
7f7bfa962b | ||
|
|
2832586641 | ||
|
|
a04244a4be | ||
|
|
714ca744dc | ||
|
|
e5476b0e12 | ||
|
|
fef5dfec03 | ||
|
|
c157ea373f | ||
|
|
40b41da916 | ||
|
|
31030b170a | ||
|
|
b23dad1d5f | ||
|
|
8837cb6f0f | ||
|
|
707eebd1dd | ||
|
|
f4cb8095c4 | ||
|
|
2325b2ee26 | ||
|
|
7572afc5bf | ||
|
|
4794a8067f | ||
|
|
95ece0bcd2 | ||
|
|
e0aa3b872a | ||
|
|
1237d9a5fa | ||
|
|
337c70d3c2 | ||
|
|
e6aa6f317f | ||
|
|
969d7da7df | ||
|
|
c0abfdc358 | ||
|
|
323a11e721 | ||
|
|
f4078faf4f | ||
|
|
e09eccc839 | ||
|
|
83f2a2c73a | ||
|
|
66ad879d18 | ||
|
|
76ceb365ca | ||
|
|
8f2f34ec4e | ||
|
|
bac1a3e0e9 | ||
|
|
24dc4a1dd6 | ||
|
|
44fbe69c66 | ||
|
|
c31367979d | ||
|
|
240304928b | ||
|
|
82c80ac4d7 | ||
|
|
fb7c62963e | ||
|
|
8b1139d026 | ||
|
|
7b06ea51bd | ||
|
|
2e111557b0 | ||
|
|
c47fbb3f22 | ||
|
|
cd1b3bd31a | ||
|
|
0c7872ddc2 | ||
|
|
ea1e28436f | ||
|
|
261145baaf | ||
|
|
201d674e94 | ||
|
|
4eeccd9c00 | ||
|
|
2dbd302038 | ||
|
|
6149cec03e | ||
|
|
5eea92bb09 | ||
|
|
6ad1ad69ab | ||
|
|
c1440388fb | ||
|
|
e2d03ac4fa | ||
|
|
b533ae5246 | ||
|
|
7a70fcccbc | ||
|
|
796937d485 | ||
|
|
e932ad7eb7 | ||
|
|
3084cd93de | ||
|
|
14691f6e53 | ||
|
|
36e06fcd86 | ||
|
|
65ac8f9106 | ||
|
|
138075961f | ||
|
|
bec4c838ab | ||
|
|
af26053565 | ||
|
|
cfe65f8d1a | ||
|
|
ae40bd7516 | ||
|
|
79e94f37f0 | ||
|
|
bca49fc5f2 | ||
|
|
d631ed2da7 | ||
|
|
036c8f6ec3 | ||
|
|
9319219623 | ||
|
|
31f225e4fb | ||
|
|
eeb05631fe | ||
|
|
8a66da38a5 | ||
|
|
a451dcd913 | ||
|
|
ac658f54a6 | ||
|
|
12fd8cff65 | ||
|
|
fca3c59586 | ||
|
|
35af455aec | ||
|
|
3fe84ec28d | ||
|
|
83cde2ca30 | ||
|
|
5ece068f7e | ||
|
|
196a683d6d | ||
|
|
fd46e34d5c | ||
|
|
c765c5f87b | ||
|
|
983fa18ad3 | ||
|
|
5bcc208a39 | ||
|
|
0e58a652fb | ||
|
|
88c1502dc7 | ||
|
|
97b680e13e | ||
|
|
a77d9fab54 | ||
|
|
58f81f20e3 | ||
|
|
8253477bf9 | ||
|
|
6aa8e25f49 | ||
|
|
af3770c6b0 | ||
|
|
40a2216b42 | ||
|
|
2e1d62961a | ||
|
|
b6a38b3288 | ||
|
|
d9070189c0 | ||
|
|
2725c23f74 | ||
|
|
3980a5ce70 | ||
|
|
2044864dba | ||
|
|
9a01b6e3b8 | ||
|
|
961d37cd55 | ||
|
|
2053388dc9 | ||
|
|
ebdfaacd1c | ||
|
|
9c4516509f | ||
|
|
4908ddaa49 | ||
|
|
d8471050bd | ||
|
|
2856a8045e | ||
|
|
c85cc6dd19 | ||
|
|
aef32e224b | ||
|
|
83aa59fdc5 | ||
|
|
f3be974359 | ||
|
|
9b75c6a127 | ||
|
|
219ae436fa | ||
|
|
79ac3164a0 | ||
|
|
2c6bfef271 | ||
|
|
99f6d47233 | ||
|
|
577d644480 | ||
|
|
b1d4686f4a | ||
|
|
00d2f12a2d | ||
|
|
e77f1dfc3f | ||
|
|
4306182741 | ||
|
|
1b003bd406 | ||
|
|
e4ed5d1a4e | ||
|
|
52f50ba1f1 | ||
|
|
a3cf8a647b | ||
|
|
6c16529a84 | ||
|
|
01c8381ce6 | ||
|
|
efa888b143 | ||
|
|
c37a3c415b | ||
|
|
dd1a55f99a | ||
|
|
51c60366b8 | ||
|
|
1b27554b9c | ||
|
|
c1a5b13fdf | ||
|
|
9d868f38e8 | ||
|
|
3409d9aaec | ||
|
|
20f92128c5 | ||
|
|
b1e0fdaa4b | ||
|
|
a0469e9455 | ||
|
|
e1168345f1 | ||
|
|
5c5fdafb94 | ||
|
|
eaddf15b77 | ||
|
|
a8fdceba5b | ||
|
|
8be9e0976f | ||
|
|
e01f766118 | ||
|
|
401169c02f | ||
|
|
b829a3189d | ||
|
|
b25b309703 | ||
|
|
959e6c34c4 | ||
|
|
75afe3f4a0 | ||
|
|
fa726b4c9c | ||
|
|
1d03976529 | ||
|
|
941041768b | ||
|
|
44997124ff | ||
|
|
3b4c379b68 | ||
|
|
2aeb246cfc | ||
|
|
ae31e3cd36 | ||
|
|
8d838f770f | ||
|
|
7bdc1ffb72 | ||
|
|
a00ed935f1 | ||
|
|
404713f234 | ||
|
|
4895b1cfa0 | ||
|
|
039d2d4609 | ||
|
|
fa6367a98f | ||
|
|
33d37ddff6 | ||
|
|
80e6d91a63 | ||
|
|
ba2c2d5fb7 | ||
|
|
75fabbd941 | ||
|
|
390336f68d | ||
|
|
7e490ef63c | ||
|
|
da34c60ba7 | ||
|
|
3641dafe98 | ||
|
|
c7f803d0db | ||
|
|
5f1bd58e30 | ||
|
|
f533c7faa6 | ||
|
|
31abd50a60 | ||
|
|
91cccc1ae1 | ||
|
|
e77e5550df | ||
|
|
15339326e9 | ||
|
|
1d8d830afa | ||
|
|
c198c25e57 | ||
|
|
ac38793caa | ||
|
|
a6759787c3 | ||
|
|
b2761df626 | ||
|
|
bfed34873a | ||
|
|
b7505b72a4 | ||
|
|
1b856af242 | ||
|
|
051d0fc9c4 | ||
|
|
447b99ba38 | ||
|
|
d85bb21c6b | ||
|
|
d7347cdde6 | ||
|
|
ad3b340d30 | ||
|
|
37f7f03cd2 | ||
|
|
cbb249d3dd | ||
|
|
f95fb3e074 | ||
|
|
6aa502d938 | ||
|
|
67bd2a1609 | ||
|
|
fe6fa9a274 | ||
|
|
db5be2aa55 | ||
|
|
50768496f5 | ||
|
|
ab5adeb79f | ||
|
|
ac5f0d19a3 | ||
|
|
9a22b78887 | ||
|
|
7c5e846121 | ||
|
|
417cbab4ff | ||
|
|
788e0b731b | ||
|
|
debe8ddabb | ||
|
|
7ecfe4a6a6 | ||
|
|
4db866ae00 | ||
|
|
8c676a7ccd | ||
|
|
8b3a96779e | ||
|
|
c61364fc89 | ||
|
|
e29112390f | ||
|
|
8d65058c9f | ||
|
|
e819ba0bd0 | ||
|
|
0b055d8316 | ||
|
|
ee0f328053 | ||
|
|
d4abe4b745 | ||
|
|
78fc321cd3 | ||
|
|
254ee2b9cd | ||
|
|
a93264877c | ||
|
|
e4b885e9fa | ||
|
|
f2fe9a9097 | ||
|
|
d2a38c9e5c | ||
|
|
47e2343b1b | ||
|
|
eeeeddd3f6 | ||
|
|
63ac0838bc | ||
|
|
ccb33b7c7c | ||
|
|
78914f55be | ||
|
|
6c4dbe3362 | ||
|
|
8df9e0afd0 | ||
|
|
395ed1ac54 | ||
|
|
13ded2d662 | ||
|
|
60c4d8de1e | ||
|
|
e288308b46 | ||
|
|
3618b48030 | ||
|
|
59cee65da3 | ||
|
|
9a58a4b73a | ||
|
|
f202d08c5f | ||
|
|
872bcd1974 | ||
|
|
6ad5596b9d | ||
|
|
982f43dee5 | ||
|
|
87fbbdaaa9 | ||
|
|
e3bf0ad04d | ||
|
|
b8c1602715 | ||
|
|
08297223e4 | ||
|
|
64fff9e149 | ||
|
|
b799ef5f6d | ||
|
|
8cc02778e4 | ||
|
|
87efc9ace5 | ||
|
|
c1533d6d5f | ||
|
|
a693660a15 | ||
|
|
436fabc466 | ||
|
|
7c2f878051 | ||
|
|
a2bab5c8c2 | ||
|
|
dc1aa5030a | ||
|
|
463d6780a9 | ||
|
|
9330989efa | ||
|
|
69f9cc7b9e | ||
|
|
b3ff0c2ebf | ||
|
|
4fbd0b3fcb | ||
|
|
d247802094 | ||
|
|
1fe683e951 | ||
|
|
eaa3a58172 | ||
|
|
7ce4b6df37 | ||
|
|
f548cfd843 | ||
|
|
6a29939806 | ||
|
|
ec06637d48 | ||
|
|
4e6a1b46be | ||
|
|
efa38a99ca | ||
|
|
4e788fbf3e | ||
|
|
da4a3a4765 | ||
|
|
4c9423e194 | ||
|
|
4fac421d14 | ||
|
|
7b1405e104 | ||
|
|
97e4dcaa1f | ||
|
|
c473d97109 | ||
|
|
4a786e03d6 | ||
|
|
e0a932a32d | ||
|
|
f31b6efd68 | ||
|
|
f11768e873 | ||
|
|
af34f0a4b7 | ||
|
|
64034f1674 | ||
|
|
8251089c1f | ||
|
|
af4a7e5638 | ||
|
|
5fbd539628 | ||
|
|
e61e90e555 | ||
|
|
17ba2d71ad | ||
|
|
76707b230b | ||
|
|
0a6eced5cc | ||
|
|
490fda2657 | ||
|
|
30a367e5f4 | ||
|
|
c3e9480592 | ||
|
|
d615708a7b | ||
|
|
4d7fede4ed | ||
|
|
9e2d698c9e | ||
|
|
7af6b6c152 | ||
|
|
917d2fc345 | ||
|
|
0ac98f80ee | ||
|
|
8509870849 | ||
|
|
aa936f59f0 | ||
|
|
ef3902477e | ||
|
|
ddfed500a9 | ||
|
|
ade3e6adb5 | ||
|
|
4736639a7e | ||
|
|
dbbe79aae8 | ||
|
|
bdb04dd8d6 | ||
|
|
48b87d42e1 | ||
|
|
e7f7dad3e0 | ||
|
|
0da73d9159 | ||
|
|
5e7a965ac1 | ||
|
|
19d16c7dbe | ||
|
|
d465e37bc1 | ||
|
|
c163678f7d | ||
|
|
0b871e2d79 | ||
|
|
4cca90e757 | ||
|
|
87d70909a8 | ||
|
|
7f9e719c8a | ||
|
|
f37abb1365 | ||
|
|
2b54cf4fbd | ||
|
|
b6b546ae13 | ||
|
|
d42319b6c9 | ||
|
|
c937aeb1df | ||
|
|
9c6255cfce | ||
|
|
998e9df959 | ||
|
|
c6a86ce168 | ||
|
|
743a6b3c61 | ||
|
|
e9a2ca44a7 | ||
|
|
89d0965408 | ||
|
|
8da1d3f8c1 | ||
|
|
eecb29593c | ||
|
|
83c8f0b120 | ||
|
|
c751c23965 | ||
|
|
2e1fcd15d9 | ||
|
|
e849aef2ae | ||
|
|
50f973c9f9 | ||
|
|
60bfe0693a | ||
|
|
9550318e34 | ||
|
|
2812c939fe | ||
|
|
87b90a7882 | ||
|
|
fa25086125 | ||
|
|
7c4b3d743e | ||
|
|
a7cd2b38e6 | ||
|
|
9cede7db08 | ||
|
|
35c72bcd10 | ||
|
|
99cc9829a0 | ||
|
|
837f992bd9 | ||
|
|
19ea30f6ab | ||
|
|
4b5cc08d1a | ||
|
|
ca34357b08 | ||
|
|
4818b1f4a1 | ||
|
|
0c165b48bf | ||
|
|
dab63e9c38 | ||
|
|
a79e653036 | ||
|
|
f9bfe8bc12 | ||
|
|
df52a28548 | ||
|
|
334dde73a2 | ||
|
|
c99896ae66 | ||
|
|
04cca3bca8 | ||
|
|
20b377d30d | ||
|
|
088f8685a7 | ||
|
|
3e6c354504 | ||
|
|
b085d54b94 | ||
|
|
ee8b23bcc7 | ||
|
|
7c4092dabe | ||
|
|
965f22e3db | ||
|
|
44a12d78be | ||
|
|
aaf37247da | ||
|
|
c5755ed837 | ||
|
|
baeb4533be | ||
|
|
ea2d565e60 | ||
|
|
c7b65c7f7b | ||
|
|
d146b0da29 | ||
|
|
9f24934663 | ||
|
|
70f8268dab | ||
|
|
0848864a03 | ||
|
|
8075af7c2a | ||
|
|
533f6b713f | ||
|
|
c20c4aed11 | ||
|
|
24e0157b53 | ||
|
|
f662759046 | ||
|
|
0d4e0dde8c | ||
|
|
db9c322bce | ||
|
|
a655bf22b2 | ||
|
|
df13760cf4 | ||
|
|
e1dca4bff5 | ||
|
|
95e4f27f17 | ||
|
|
1ea66f60af | ||
|
|
394de92915 | ||
|
|
b15b209a03 | ||
|
|
76cdc271c0 | ||
|
|
15937b7e80 | ||
|
|
a161098036 | ||
|
|
0398e09af5 | ||
|
|
004e22c1de | ||
|
|
a4d20310f1 | ||
|
|
0b22b8c736 | ||
|
|
a3dd4b702d | ||
|
|
5fb8fa95b3 | ||
|
|
bcbf2caf3f | ||
|
|
0edf5cb9fc | ||
|
|
14acfc3b3c | ||
|
|
529a7019ae | ||
|
|
fd0d7fd875 | ||
|
|
0bf5fe1ebe | ||
|
|
07fe758ac9 | ||
|
|
90a5d082bc | ||
|
|
fa0d99e5b7 | ||
|
|
8820fa433d | ||
|
|
781690f397 | ||
|
|
20e0bf2e9d | ||
|
|
3571ceb1e4 | ||
|
|
a4a0da0d5d | ||
|
|
22265c6ac7 | ||
|
|
e62e6bf2f5 | ||
|
|
84ca0b6c21 | ||
|
|
daa45b1e37 | ||
|
|
ff62ce3fbe | ||
|
|
7d43470f31 | ||
|
|
14168890c9 | ||
|
|
10ede85dd1 | ||
|
|
5f054d2a2c | ||
|
|
aab3810b54 | ||
|
|
bd773bbd77 | ||
|
|
35cfd187cf | ||
|
|
eaf8745697 | ||
|
|
c0616f06be | ||
|
|
e6b3af5889 | ||
|
|
8e2460d18c | ||
|
|
33120935a3 | ||
|
|
bb33349149 | ||
|
|
f23eda8c95 | ||
|
|
326e391188 | ||
|
|
158a42d8b6 | ||
|
|
be1a871349 | ||
|
|
5e33c4e85d | ||
|
|
b74d6ad3d3 | ||
|
|
375f76fef0 | ||
|
|
02bbf51fb7 | ||
|
|
945971648a | ||
|
|
365c60e7a4 | ||
|
|
34204c4a31 | ||
|
|
805a308d2e | ||
|
|
ff14364de0 | ||
|
|
4e67f1f045 | ||
|
|
e1d11fe727 | ||
|
|
4ff5cd4050 | ||
|
|
4f74de9cdb | ||
|
|
ffbc606682 | ||
|
|
eca3aef3a2 | ||
|
|
ba64156fd4 | ||
|
|
ec53cae11f | ||
|
|
697cbc6376 | ||
|
|
26da97bde0 | ||
|
|
701f31964b | ||
|
|
5ebea67b79 | ||
|
|
d0a02fe426 | ||
|
|
25a1f6e046 | ||
|
|
84507d7179 | ||
|
|
86faf035b6 | ||
|
|
4557f68053 | ||
|
|
8fd8e6da3c | ||
|
|
dd32704d4d | ||
|
|
390da5d905 | ||
|
|
99581ed5e3 | ||
|
|
294c95ebbc | ||
|
|
7709a0e2c8 | ||
|
|
cce9b16a72 | ||
|
|
926171c779 | ||
|
|
f5b7628a7f | ||
|
|
28651a4079 | ||
|
|
72f21aca93 | ||
|
|
b4e477c014 | ||
|
|
81aef750d2 | ||
|
|
75d9b260da | ||
|
|
2fce1e1e61 | ||
|
|
e05565e223 | ||
|
|
21aa723a2a | ||
|
|
ffbb367b8a | ||
|
|
5f48f323da | ||
|
|
0ca3752ea6 | ||
|
|
f4abf43154 | ||
|
|
62bb21a2d7 | ||
|
|
f7f21bc823 | ||
|
|
bda227817e | ||
|
|
fbf045eb6a | ||
|
|
f47b0385f3 | ||
|
|
89febede8b | ||
|
|
31556cd1a0 | ||
|
|
322e3fdfb3 | ||
|
|
8401c1cdb5 | ||
|
|
486555d4f1 | ||
|
|
3152013186 | ||
|
|
1e73f53160 | ||
|
|
b9c6c65482 | ||
|
|
37cab2c76f | ||
|
|
14d0ec7ac8 | ||
|
|
5a00c8fb64 | ||
|
|
1696b5c959 | ||
|
|
0db5d17102 | ||
|
|
85c7bfda29 | ||
|
|
42ffb27951 | ||
|
|
bfe0798660 | ||
|
|
5df75b1d4d | ||
|
|
844f91c606 | ||
|
|
36c6ff200e | ||
|
|
7b2082dbb0 | ||
|
|
8a9d724e6e | ||
|
|
1b6ed0898d | ||
|
|
0c37d0e285 | ||
|
|
2bd0cdc4f9 | ||
|
|
d949d600f8 | ||
|
|
5a8985620c | ||
|
|
10a326a90c | ||
|
|
5322caafe0 | ||
|
|
11bf85d8a8 | ||
|
|
ec42c93947 | ||
|
|
5778a9ef17 | ||
|
|
4868f3234d | ||
|
|
d3c4bba4ca | ||
|
|
8ad2a62730 | ||
|
|
e3de5044b2 | ||
|
|
13cd63567e | ||
|
|
1aa306d7ea | ||
|
|
912d7c1673 | ||
|
|
bc9058a8d4 | ||
|
|
c8698f6257 | ||
|
|
5ec49d7adf | ||
|
|
95654fe073 | ||
|
|
921668da66 | ||
|
|
7d0b3e7090 | ||
|
|
b11004671f | ||
|
|
1d26952f72 | ||
|
|
871a242721 | ||
|
|
e09733aa9d | ||
|
|
d0e9cb3e37 | ||
|
|
896c66180e | ||
|
|
f31f24c8dc | ||
|
|
6b83f43dc9 | ||
|
|
ec73f63f28 | ||
|
|
22fea7b767 | ||
|
|
e437bc472d | ||
|
|
6aa4663bf2 | ||
|
|
651e64ff32 | ||
|
|
322790aff0 | ||
|
|
347d255eaa | ||
|
|
48e6d64c4f | ||
|
|
865b184a0f | ||
|
|
c5388c78cd | ||
|
|
1bd9746f53 | ||
|
|
7e469d797c | ||
|
|
dc1bc972ba | ||
|
|
21da8442f5 | ||
|
|
ac4770f2b1 | ||
|
|
d7895e1690 | ||
|
|
633a87577f | ||
|
|
2d784fa6ff | ||
|
|
3e8b5161be | ||
|
|
480cfc5f3b | ||
|
|
4c968e6199 | ||
|
|
be885026f9 | ||
|
|
dc87d45eb3 | ||
|
|
2496301c60 | ||
|
|
24d28c3f41 | ||
|
|
6aa3b99816 | ||
|
|
e1ee39169d | ||
|
|
9546c3af26 | ||
|
|
64d8488c67 | ||
|
|
aecee58bdd | ||
|
|
4736323d86 | ||
|
|
2622762f89 | ||
|
|
9ddebc1b11 | ||
|
|
dc859b26ff | ||
|
|
a7bb051154 | ||
|
|
a51d311fb9 | ||
|
|
cfd45e0c4d | ||
|
|
3df612f80a | ||
|
|
2425a18705 | ||
|
|
178bde4ce6 | ||
|
|
537333045a | ||
|
|
7f81445c02 | ||
|
|
a24e78f6f1 | ||
|
|
fad1ce5568 | ||
|
|
e4da566272 | ||
|
|
597039b6cb | ||
|
|
3e373187ea | ||
|
|
bf63faab2d | ||
|
|
9627ee00ce | ||
|
|
b73ea23b68 | ||
|
|
236cca71bb | ||
|
|
3e5e0c73b2 | ||
|
|
2e324bbbe8 | ||
|
|
d5be478937 | ||
|
|
52d58ba32e | ||
|
|
66bc889072 | ||
|
|
2e3085a555 | ||
|
|
9587a7a1e0 | ||
|
|
874b8850e8 | ||
|
|
135430b076 | ||
|
|
4d988cb460 | ||
|
|
44d8858747 | ||
|
|
03d9f35ed5 | ||
|
|
c24c426c06 | ||
|
|
341228c718 | ||
|
|
a0bd7b25ea | ||
|
|
18bb5b793b | ||
|
|
b0e3b5add6 | ||
|
|
f7e12d23ce | ||
|
|
62413d835c | ||
|
|
1ee7c7337d | ||
|
|
01f309efef | ||
|
|
decb3f3f8d | ||
|
|
8a11d125db | ||
|
|
f549c31d1d | ||
|
|
5f051b83d0 | ||
|
|
515e045faf | ||
|
|
d442b0307f | ||
|
|
439339ab14 | ||
|
|
5389eb9db8 | ||
|
|
31a3d5553e | ||
|
|
e424cc940d | ||
|
|
cef6027dcb | ||
|
|
48fc04a425 | ||
|
|
486120787d | ||
|
|
090e45adf1 | ||
|
|
48ae834b58 | ||
|
|
0b3299a019 | ||
|
|
a242537838 | ||
|
|
c968d37a48 | ||
|
|
79d8ea066a | ||
|
|
64a91dcdc0 | ||
|
|
85b75dcbea | ||
|
|
456ec79c0a | ||
|
|
4ef3855745 | ||
|
|
dea9b6de5d | ||
|
|
036d473a3c | ||
|
|
c0d02ba96a | ||
|
|
a6bdc3fb5b | ||
|
|
a87fb80e13 | ||
|
|
ac35eebf30 | ||
|
|
080abf86a4 | ||
|
|
6e29a5d314 | ||
|
|
96cb96aced | ||
|
|
1327c0916c | ||
|
|
fec7d1cae2 | ||
|
|
ab7c977216 | ||
|
|
83081577e5 | ||
|
|
ab5ea838de | ||
|
|
dca6a6532f | ||
|
|
4dc3206402 | ||
|
|
caa3c8425a | ||
|
|
2d9b1b348a | ||
|
|
cb73a0538a | ||
|
|
140f011030 | ||
|
|
fb433c3a57 | ||
|
|
cb2740a8c2 | ||
|
|
f1a9eaa47e | ||
|
|
fd13adf248 | ||
|
|
c0a4a9e657 | ||
|
|
7c4ff2de72 | ||
|
|
d284809fb0 | ||
|
|
1e14e6469e | ||
|
|
132faae7cf | ||
|
|
6a5be385c6 |
No files matched your search
@@ -4,9 +4,24 @@ bench/prompts/** -text
|
||||
# launchers keep their platform line endings wherever the repository is cloned
|
||||
*.sh text eol=lf
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
bin/strata text eol=lf
|
||||
|
||||
# copies of real packs' native_experts.txt, parsed byte for byte by tests/core/expert_layout_test.cpp
|
||||
tests/data/native_experts/** -text
|
||||
|
||||
# the quant yardstick's text set: tokenized byte for byte (set.ids.json), never convert its line endings
|
||||
data/quant_quality/** -text
|
||||
|
||||
# the license text is shipped and checked as LF text on every checkout
|
||||
LICENSE text eol=lf
|
||||
|
||||
# container scripts run in Linux: always LF
|
||||
docker/*.sh text eol=lf
|
||||
Dockerfile text eol=lf
|
||||
docker/Dockerfile text eol=lf
|
||||
docker/compose/* text eol=lf
|
||||
|
||||
# Apple tools (codesign, plutil) read these: LF only, wherever the repository is cloned
|
||||
*.plist text eol=lf
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Benchmark
|
||||
description: Share measurements from your machine, or report a problem with the benchmark tools
|
||||
title: "[Benchmark] "
|
||||
labels: ["benchmark"]
|
||||
body:
|
||||
- type: markdown
|
||||
value: |
|
||||
The title must start with **[Benchmark]**. Keep it, and add the GPU and model after it.
|
||||
|
||||
To share a full report, follow the [community benchmark guide](https://github.com/Niko1221/Strata/blob/main/docs/COMMUNITY_BENCHMARKS.md) and open a results-only pull request. The standard protocol is a [draft](https://github.com/Niko1221/Strata/blob/main/docs/BENCHMARK_PROTOCOL.md); existing tools do not implement it yet. Include poor results and failed requests.
|
||||
- type: textarea
|
||||
id: what
|
||||
attributes:
|
||||
label: What did you measure, or what went wrong
|
||||
description: Hardware (GPU and VRAM, CPU, RAM, OS), Strata version, model and quantization, the exact command, and the numbers. Paste the report or link the pull request.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: reproduction-status
|
||||
attributes:
|
||||
label: Reproduction status
|
||||
options:
|
||||
- Measured locally
|
||||
- Unconfirmed (I did not run it)
|
||||
validations:
|
||||
required: true
|
||||
@@ -1,37 +1,101 @@
|
||||
name: Bug report
|
||||
description: Something in Strata doesn't work as documented
|
||||
title: "[Bug]: "
|
||||
title: "[Area] "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
value: |
|
||||
Thanks for the report.
|
||||
Thanks for the report. Please read [CONTRIBUTING.md](https://github.com/Niko1221/Strata/blob/main/CONTRIBUTING.md) first.
|
||||
|
||||
Please search the [open issues](https://github.com/Niko1221/Strata/issues?q=is%3Aissue+state%3Aopen) first — this may already be tracked.
|
||||
Search the [open and closed issues](https://github.com/Niko1221/Strata/issues?q=is%3Aissue) first: this may already be tracked, and new evidence belongs in that thread.
|
||||
|
||||
Write only what you saw. Do not guess at steps, errors, hardware or severity.
|
||||
|
||||
**The title must start with its area:** [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other]. Choose the same one below.
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area
|
||||
description: Start the issue title with the same area in square brackets, for example "[Engine] decode stops after 2 minutes". Pick Other if none fits.
|
||||
options:
|
||||
- "[Engine] (speed, kernels, models, the server and API)"
|
||||
- "[GUI] (the desktop app)"
|
||||
- "[TUI] (the Terminal: strata launch, connect, monitor)"
|
||||
- "[Benchmark] (measurements and reports)"
|
||||
- "[Docs] (documentation)"
|
||||
- "[Other] (anything else, including setup and installation)"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: how
|
||||
attributes:
|
||||
label: Desktop app, Terminal or source?
|
||||
options:
|
||||
- Desktop app (Windows, Linux or Mac)
|
||||
- Terminal (the `strata` command)
|
||||
- Source (START-HERE.bat, setup.sh or a build)
|
||||
- Docker
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what
|
||||
attributes:
|
||||
label: What happened
|
||||
description: What you did, what you got, and what you expected instead. Paste the exact error text if there is one. If there's a set of steps or commands that reproduces it, put them here too.
|
||||
description: What you did, what you got, and what you expected instead. Paste the exact error text if there is one.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Smallest steps or command that reproduce it
|
||||
description: The shortest list of steps that shows the problem. If you cannot make it happen again, say so here.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: reproduction-status
|
||||
attributes:
|
||||
label: Reproduction status
|
||||
options:
|
||||
- Reproduced locally
|
||||
- Unconfirmed static analysis (I read code or ran a scanner; I did not see it fail)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
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.1 · engine 0.1.46"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: hardware
|
||||
attributes:
|
||||
label: Strata version, GPU, OS
|
||||
description: The engine version is on the About tab, or in `GET /v1/status` under `engine`.
|
||||
placeholder: "0.1.31 · RTX 5070 12GB · Windows 11"
|
||||
label: Graphics card, memory and system
|
||||
description: The card and its VRAM, the RAM of the PC, and the operating system.
|
||||
placeholder: "RTX 5070 12 GB · 64 GB RAM · Windows 11"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: model
|
||||
attributes:
|
||||
label: Model, size and settings
|
||||
description: The model and size you started, the context, and whether pictures were on. Write what is relevant.
|
||||
placeholder: "Qwen3.8-Flash-Next IQ2_XS · 128K context · pictures on"
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes:
|
||||
label: Engine log
|
||||
description: Drag in `strata-<model>.log` from your Strata folder. Drag and drop works directly into this box.
|
||||
label: Logs
|
||||
description: |
|
||||
The app: Settings > Engine > Diagnostics > View logs > Save logs, then drag the file in. The Terminal: paste the output of `strata doctor`. From source: drag in `strata-<model>.log` from your Strata folder.
|
||||
Remove API keys, passwords and private text first.
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
value: |
|
||||
A couple of extras that help a lot:
|
||||
|
||||
- Check the [troubleshooting table](https://github.com/Niko1221/Strata/blob/main/docs/DETAILS.md#troubleshooting) first, many reports are already solved there.
|
||||
- Check the [troubleshooting page](https://github.com/Niko1221/Strata/blob/main/docs/TROUBLESHOOTING.md) first, many reports are already solved there.
|
||||
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Troubleshooting and known issues
|
||||
url: https://github.com/Niko1221/Strata/blob/main/docs/DETAILS.md#troubleshooting
|
||||
url: https://github.com/Niko1221/Strata/blob/main/docs/TROUBLESHOOTING.md
|
||||
about: Symptom-to-fix table for crashes, out of memory, hangs, and port problems.
|
||||
@@ -1,18 +1,41 @@
|
||||
name: Feature request
|
||||
description: Suggest something new or an improvement
|
||||
title: "[Feature]: "
|
||||
title: "[Area] "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
value: |
|
||||
Explain what you want in plain language.
|
||||
Explain what you want in plain language. For a large feature, a change to a default or a refactor, please agree the scope here first. Read [CONTRIBUTING.md](https://github.com/Niko1221/Strata/blob/main/CONTRIBUTING.md).
|
||||
|
||||
**The title must start with its area:** [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other]. Choose the same one below.
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area
|
||||
description: Start the issue title with the same area in square brackets, for example "[Engine] decode stops after 2 minutes". Pick Other if none fits.
|
||||
options:
|
||||
- "[Engine] (speed, kernels, models, the server and API)"
|
||||
- "[GUI] (the desktop app)"
|
||||
- "[TUI] (the Terminal: strata launch, connect, monitor)"
|
||||
- "[Benchmark] (measurements and reports)"
|
||||
- "[Docs] (documentation)"
|
||||
- "[Other] (anything else, including setup and installation)"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: idea
|
||||
attributes:
|
||||
label: What do you want to do?
|
||||
description: Describe the goal rather than the implementation. If there's a workaround you use today, mention it.
|
||||
description: Describe the goal rather than the implementation. Say who needs it and why.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: workaround
|
||||
attributes:
|
||||
label: What do you do today instead?
|
||||
description: The workaround you use now, if there is one.
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
value: |
|
||||
All of this counts: model and architecture support, the server and API, the web app, multi-GPU and layer split, images and vision, MCP servers, setup and installation, and performance.
|
||||
All of this counts: model and architecture support, the server and API, the app and the Terminal, multi-GPU and layer split, images and vision, MCP servers, setup and installation, and performance.
|
||||
@@ -1,11 +1,27 @@
|
||||
name: Question
|
||||
description: Ask how to use Strata, or share a result
|
||||
title: "Question: "
|
||||
title: "[Area] "
|
||||
labels: ["question"]
|
||||
body:
|
||||
- type: markdown
|
||||
value: |
|
||||
Not sure how to do something, or want to share numbers from your machine? Ask here.
|
||||
Not sure how to do something, or want to share numbers from your machine? Ask here. To share a full benchmark, see [CONTRIBUTING.md](https://github.com/Niko1221/Strata/blob/main/CONTRIBUTING.md#share-a-benchmark).
|
||||
|
||||
**The title must start with its area:** [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other]. Choose the same one below.
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Area
|
||||
description: Start the issue title with the same area in square brackets, for example "[Engine] decode stops after 2 minutes". Pick Other if none fits.
|
||||
options:
|
||||
- "[Engine] (speed, kernels, models, the server and API)"
|
||||
- "[GUI] (the desktop app)"
|
||||
- "[TUI] (the Terminal: strata launch, connect, monitor)"
|
||||
- "[Benchmark] (measurements and reports)"
|
||||
- "[Docs] (documentation)"
|
||||
- "[Other] (anything else, including setup and installation)"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
@@ -13,10 +29,17 @@ body:
|
||||
description: What you're trying to do. Include your version, GPU, and OS if it affects the answer.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: tried
|
||||
attributes:
|
||||
label: What you tried
|
||||
description: What you already did and where you got stuck.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: numbers
|
||||
attributes:
|
||||
label: Results (optional)
|
||||
description: Only if you ran a benchmark or measured speed. Model, quant, context, and the tok/s you get.
|
||||
validations:
|
||||
required: false
|
||||
required: false
|
||||
@@ -1,12 +1,30 @@
|
||||
## Title
|
||||
<!-- If Applicable, reference the GitHub issue -->
|
||||
Issue: Resolves #
|
||||
**The title must start with its area: [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other], for example "[Engine] fix decode stop on long prompts" or "[GUI] keep the model list after a tab switch".**
|
||||
|
||||
## Summary
|
||||
<!-- Quick Summary of changes -->
|
||||
<!--
|
||||
Read CONTRIBUTING.md first. Lead with the problem and the result, not a file-by-file list.
|
||||
Placeholders below are not test results: replace them with what you observed, or write "none".
|
||||
-->
|
||||
|
||||
## What changed
|
||||
<!-- Specifics on files changed, and what changes were made there -->
|
||||
## Problem and result
|
||||
<!-- What triggered this change. How it behaves before and after. -->
|
||||
|
||||
## Extra Notes
|
||||
<!-- Any extra notes, delete if there are none -->
|
||||
## Related discussion
|
||||
<!-- The issue URL and the maintainer's agreement on scope, when the change needs one (features, refactors, defaults). Write "none" if not applicable. -->
|
||||
|
||||
## Approach
|
||||
<!-- Why this fix. Compatibility effects. Keep unrelated cleanup out of the PR. -->
|
||||
|
||||
## Validation
|
||||
<!-- Exact commands, the revision and the environment tested, what you saw. Keep the three lists apart. -->
|
||||
|
||||
**Passed:**
|
||||
<!-- command, revision/environment, observed result, log -->
|
||||
|
||||
**Failed:**
|
||||
<!-- command and failure, or "none observed" -->
|
||||
|
||||
**Not run:**
|
||||
<!-- check, why, and what stays unverified (for example CUDA, HIP, SYCL or Metal builds you could not do) -->
|
||||
|
||||
## Limits
|
||||
<!-- Known limitations or remaining work. -->
|
||||
@@ -0,0 +1,218 @@
|
||||
'use strict';
|
||||
// Format check for issues and pull requests (see CONTRIBUTING.md and .github/ISSUE_TEMPLATE/*.yml).
|
||||
// Pure functions only: no network, no filesystem. Used by intake.yml and reopen.yml through reset-bot.js.
|
||||
|
||||
const AREAS = ['Engine', 'GUI', 'TUI', 'Benchmark', 'Docs', 'Other'];
|
||||
const TITLE_RE = new RegExp('^\\[(' + AREAS.join('|') + ')\\] \\S');
|
||||
const NO_ANSWER = /^(_no response_|none|n\/a|na|-|todo|tbd|\.|\?)$/i;
|
||||
const NOT_RUN_LABELS = /^\*\*(passed|failed|not run)\s*:?\*\*\s*:?/gim;
|
||||
|
||||
// Version the reopened work must be tested on.
|
||||
const CURRENT_VERSION_RE = /\b0\.2\.\d+|0\.2\.0-beta/i;
|
||||
// Any version or commit.
|
||||
const ANY_VERSION_RE = /\bv?\d+\.\d+(\.\d+)*(-[a-z0-9.]+)?\b|\b[0-9a-f]{7,40}\b|\bcommit\b/i;
|
||||
const HARDWARE_RE = /\b(rtx|gtx|geforce|quadro|tesla|radeon|rx ?\d|instinct|arc|a\d{3,4}|b\d{2,3}|apple|m[1-5]( pro| max| ultra)?|nvidia|amd|intel|gpu|cpu|vram|\d+ ?gb|ram|windows|linux|ubuntu|debian|fedora|arch|macos|mac os|wsl)\b/i;
|
||||
const REVISION_RE = /0\.2\.\d|beta|\bmain\b|\b[0-9a-f]{7,40}\b/i;
|
||||
|
||||
function stripComments(text) {
|
||||
return String(text || '').replace(/<!--[\s\S]*?-->/g, '');
|
||||
}
|
||||
|
||||
function hasAreaPrefix(title) {
|
||||
return TITLE_RE.test(String(title || ''));
|
||||
}
|
||||
|
||||
// Issue forms render as "### Label\n\nvalue". Returns { label: value } for those headings.
|
||||
function parseSections(body) {
|
||||
const out = {};
|
||||
let cur = null;
|
||||
for (const line of stripComments(body).split(/\r?\n/)) {
|
||||
const m = /^#{1,6}\s+(.+?)\s*#*\s*$/.exec(line);
|
||||
if (m) {
|
||||
cur = m[1].trim();
|
||||
out[cur] = '';
|
||||
} else if (cur !== null) {
|
||||
out[cur] += line + '\n';
|
||||
}
|
||||
}
|
||||
for (const k of Object.keys(out)) out[k] = out[k].trim();
|
||||
return out;
|
||||
}
|
||||
|
||||
function realText(value) {
|
||||
const t = String(value || '').replace(NOT_RUN_LABELS, '').replace(/\s+/g, ' ').trim();
|
||||
return NO_ANSWER.test(t) ? '' : t;
|
||||
}
|
||||
|
||||
function findSection(sections, re) {
|
||||
for (const k of Object.keys(sections)) if (re.test(k)) return { key: k, value: sections[k] };
|
||||
return null;
|
||||
}
|
||||
|
||||
// Required parts of each issue form: [display name, heading regex, minimum characters of real text].
|
||||
const FORMS = {
|
||||
bug: {
|
||||
detect: /what happened/i,
|
||||
parts: [
|
||||
['Area', /^area$/i, 2],
|
||||
['Desktop app, Terminal or source?', /desktop app, terminal or source/i, 2],
|
||||
['What happened', /what happened/i, 20],
|
||||
['Reproduction status', /reproduction status/i, 2],
|
||||
['Strata version', /strata version/i, 3],
|
||||
['Graphics card, memory and system', /graphics card|memory and system/i, 3],
|
||||
],
|
||||
needsCurrentVersion: 'Strata version',
|
||||
},
|
||||
feature: {
|
||||
detect: /what do you want to do/i,
|
||||
parts: [
|
||||
['Area', /^area$/i, 2],
|
||||
['What do you want to do?', /what do you want to do/i, 20],
|
||||
],
|
||||
},
|
||||
question: {
|
||||
detect: /your question/i,
|
||||
parts: [
|
||||
['Area', /^area$/i, 2],
|
||||
['Your question', /your question/i, 15],
|
||||
],
|
||||
},
|
||||
benchmark: {
|
||||
detect: /what did you measure/i,
|
||||
parts: [
|
||||
['What did you measure, or what went wrong', /what did you measure/i, 30],
|
||||
['Reproduction status', /reproduction status/i, 2],
|
||||
],
|
||||
needsCurrentVersionInBody: true,
|
||||
},
|
||||
};
|
||||
|
||||
function checkIssue({ title, body, labels = [], mode = 'intake' }) {
|
||||
const missing = [];
|
||||
if (!hasAreaPrefix(title)) {
|
||||
missing.push('Start the title with its area: [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other], then a short description. Example: "[Engine] decode stops after 2 minutes".');
|
||||
}
|
||||
const text = stripComments(body);
|
||||
const sections = parseSections(text);
|
||||
const labelSet = new Set(labels.map((l) => String(l).toLowerCase()));
|
||||
const needCurrent = mode === 'reopen';
|
||||
|
||||
let formKey = null;
|
||||
for (const [key, f] of Object.entries(FORMS)) {
|
||||
if (findSection(sections, f.detect)) { formKey = key; break; }
|
||||
}
|
||||
|
||||
if (formKey) {
|
||||
const form = FORMS[formKey];
|
||||
for (const [name, re, min] of form.parts) {
|
||||
const s = findSection(sections, re);
|
||||
if (!s || realText(s.value).length < min) missing.push('Fill in "' + name + '".');
|
||||
}
|
||||
if (needCurrent && form.needsCurrentVersion) {
|
||||
const s = findSection(sections, /strata version/i);
|
||||
if (s && realText(s.value) && !CURRENT_VERSION_RE.test(s.value)) {
|
||||
missing.push('Re-test on Strata 0.2.0-beta and put that version in "' + form.needsCurrentVersion + '". If it no longer happens, say so in a comment instead.');
|
||||
}
|
||||
}
|
||||
if (needCurrent && form.needsCurrentVersionInBody && !CURRENT_VERSION_RE.test(text)) {
|
||||
missing.push('Say which Strata version you measured on. Re-measure on 0.2.0-beta if you can.');
|
||||
}
|
||||
return { ok: missing.length === 0, missing, kind: formKey };
|
||||
}
|
||||
|
||||
// Free-form text (older issues, blank issues).
|
||||
const plain = realText(text);
|
||||
const lenient = labelSet.has('enhancement') || labelSet.has('question');
|
||||
if (plain.length < (lenient ? 40 : 80)) {
|
||||
missing.push('Describe it in the issue body: what happened, what you expected, and the exact error if there is one. The issue forms (New issue > Bug report) list what to include.');
|
||||
}
|
||||
if (!lenient) {
|
||||
if (!ANY_VERSION_RE.test(text)) missing.push('Add the Strata version (Settings > About, or `strata --version`).');
|
||||
else if (needCurrent && !CURRENT_VERSION_RE.test(text)) missing.push('Re-test on Strata 0.2.0-beta and name that version in the issue. If it no longer happens, say so in a comment instead.');
|
||||
if (!HARDWARE_RE.test(text)) missing.push('Add your graphics card with its VRAM, your RAM and your operating system.');
|
||||
}
|
||||
return { ok: missing.length === 0, missing, kind: 'free-form' };
|
||||
}
|
||||
|
||||
function checkPull({ title, body, mode = 'intake' }) {
|
||||
const missing = [];
|
||||
if (!hasAreaPrefix(title)) {
|
||||
missing.push('Start the title with its area: [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other], then a short description. Example: "[Engine] fix decode stop on long prompts".');
|
||||
}
|
||||
const text = stripComments(body);
|
||||
const sections = parseSections(text);
|
||||
const problem = findSection(sections, /problem|result|summary|what.*change/i);
|
||||
const validation = findSection(sections, /validation|testing|tests?\b|how.*tested/i);
|
||||
|
||||
let valText = '';
|
||||
if (problem || validation) {
|
||||
if (!problem || realText(problem.value).length < 20) missing.push('Fill in "Problem and result": what triggered the change, and how it behaves before and after.');
|
||||
if (!validation || realText(validation.value).length < 20) {
|
||||
missing.push('Fill in "Validation": the exact commands, the revision and environment tested, what you saw. Keep Passed, Failed and Not run apart.');
|
||||
} else {
|
||||
valText = validation.value;
|
||||
}
|
||||
} else {
|
||||
const plain = realText(text);
|
||||
if (plain.length < 150) {
|
||||
missing.push('Describe the pull request: the problem and the result. The pull request template shows the parts (Problem and result, Related discussion, Approach, Validation, Limits).');
|
||||
}
|
||||
if (!/(problem|result|before|after|fix|because|bug)/i.test(plain)) missing.push('Add "Problem and result": what triggered the change, and how it behaves before and after.');
|
||||
if (!/(validat|tested|passed|ran |built|build|not run)/i.test(plain)) missing.push('Add "Validation": the exact commands, the revision and environment tested, and what you saw.');
|
||||
valText = plain;
|
||||
}
|
||||
if (mode === 'reopen' && valText && !REVISION_RE.test(valText)) {
|
||||
missing.push('Say in Validation which revision you tested: rebase on main (or 0.2.0-beta) and re-run your checks.');
|
||||
}
|
||||
return { ok: missing.length === 0, missing, kind: 'pull' };
|
||||
}
|
||||
|
||||
function checkItem({ isPull, ...rest }) {
|
||||
return isPull ? checkPull(rest) : checkIssue(rest);
|
||||
}
|
||||
|
||||
function checklist(missing) {
|
||||
return missing.map((m) => '- [ ] ' + m).join('\n');
|
||||
}
|
||||
|
||||
const CONTRIB = 'https://github.com/Niko1221/Strata/blob/main/CONTRIBUTING.md';
|
||||
|
||||
function intakeComment(isPull, missing) {
|
||||
return [
|
||||
'Thanks for the ' + (isPull ? 'pull request' : 'issue') + '. To keep the tracker readable, every item needs an area prefix in the title and the parts of the ' + (isPull ? 'pull request template' : 'issue form') + ' filled in. This one is missing:',
|
||||
'',
|
||||
checklist(missing),
|
||||
'',
|
||||
'Edit the ' + (isPull ? 'pull request' : 'issue') + ' (title and description) and this note goes away on its own. If it is still missing parts in 5 days it will be closed, which is not a rejection: fix it and comment `/reopen`.',
|
||||
'',
|
||||
'Details: [CONTRIBUTING.md](' + CONTRIB + ')',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function reopenMissingComment(isPull, missing) {
|
||||
return [
|
||||
'Not reopened yet. Still missing:',
|
||||
'',
|
||||
checklist(missing),
|
||||
'',
|
||||
'Edit the ' + (isPull ? 'pull request' : 'issue') + ' and comment `/reopen` again. Details: [CONTRIBUTING.md](' + CONTRIB + ')',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function reopenOkComment(isPull) {
|
||||
return 'Reopened, thank you. A maintainer will pick it up from here.' + (isPull ? ' If it conflicts with main, rebase it first; that speeds up review.' : '');
|
||||
}
|
||||
|
||||
function staleComment(isPull) {
|
||||
return [
|
||||
'Closing this ' + (isPull ? 'pull request' : 'issue') + ' because it still did not follow the format after 5 days. That is housekeeping, not a rejection.',
|
||||
'',
|
||||
'To bring it back: fix the title (area prefix) and the missing parts, then comment `/reopen`. A check runs and reopens it, or tells you what is still missing. See [CONTRIBUTING.md](' + CONTRIB + ').',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AREAS, TITLE_RE, hasAreaPrefix, parseSections, stripComments, realText,
|
||||
checkIssue, checkPull, checkItem, checklist,
|
||||
intakeComment, reopenMissingComment, reopenOkComment, staleComment,
|
||||
};
|
||||
@@ -0,0 +1,192 @@
|
||||
'use strict';
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const c = require('./item-check');
|
||||
const bot = require('./reset-bot');
|
||||
|
||||
const bugForm = (over = {}) => {
|
||||
const f = {
|
||||
'Area': '[Engine] (speed, kernels, models, the server and API)',
|
||||
'Desktop app, Terminal or source?': 'Terminal (the `strata` command)',
|
||||
'What happened': 'Decode stops after two minutes with "CUDA error 700". I expected it to continue.',
|
||||
'Smallest steps or command that reproduce it': '_No response_',
|
||||
'Reproduction status': 'Reproduced locally',
|
||||
'Strata version': 'Strata 0.2.0-beta · engine 0.1.45.3',
|
||||
'Graphics card, memory and system': 'RTX 5070 12 GB · 64 GB RAM · Windows 11',
|
||||
...over,
|
||||
};
|
||||
return Object.entries(f).map(([k, v]) => `### ${k}\n\n${v}`).join('\n\n');
|
||||
};
|
||||
|
||||
test('title prefix', () => {
|
||||
assert.ok(c.hasAreaPrefix('[Engine] fix decode'));
|
||||
assert.ok(c.hasAreaPrefix('[Docs] x'));
|
||||
assert.ok(!c.hasAreaPrefix('Engine fix'));
|
||||
assert.ok(!c.hasAreaPrefix('[engine] fix'));
|
||||
assert.ok(!c.hasAreaPrefix('[Engine]'));
|
||||
assert.ok(!c.hasAreaPrefix('[Engine] '));
|
||||
assert.ok(!c.hasAreaPrefix('[Area] foo'));
|
||||
assert.ok(!c.hasAreaPrefix(''));
|
||||
assert.ok(!c.hasAreaPrefix(undefined));
|
||||
});
|
||||
|
||||
test('filled bug form passes in both modes', () => {
|
||||
for (const mode of ['intake', 'reopen']) {
|
||||
const r = c.checkIssue({ title: '[Engine] decode stops', body: bugForm(), labels: ['bug'], mode });
|
||||
assert.deepStrictEqual(r.missing, []);
|
||||
assert.strictEqual(r.kind, 'bug');
|
||||
}
|
||||
});
|
||||
|
||||
test('bug form: missing parts are named', () => {
|
||||
const r = c.checkIssue({ title: 'decode stops', body: bugForm({ 'Strata version': '_No response_', 'What happened': 'bad' }) });
|
||||
assert.ok(!r.ok);
|
||||
assert.ok(r.missing.some((m) => /title/i.test(m)));
|
||||
assert.ok(r.missing.some((m) => m.includes('What happened')));
|
||||
assert.ok(r.missing.some((m) => m.includes('Strata version')));
|
||||
});
|
||||
|
||||
test('reopen needs a 0.2 version, intake does not', () => {
|
||||
const body = bugForm({ 'Strata version': 'Strata 0.1.44' });
|
||||
assert.ok(c.checkIssue({ title: '[Engine] x y', body, mode: 'intake' }).ok);
|
||||
const r = c.checkIssue({ title: '[Engine] x y', body, mode: 'reopen' });
|
||||
assert.ok(!r.ok);
|
||||
assert.ok(r.missing.some((m) => m.includes('0.2.0-beta')));
|
||||
});
|
||||
|
||||
test('html comments and placeholders do not count', () => {
|
||||
const r = c.checkIssue({ title: '[Engine] x y', body: bugForm({ 'What happened': '<!-- describe -->' }) });
|
||||
assert.ok(r.missing.some((m) => m.includes('What happened')));
|
||||
});
|
||||
|
||||
test('feature and question forms', () => {
|
||||
const f = '### Area\n\n[GUI] (the desktop app)\n\n### What do you want to do?\n\nLet me pin a model to the top of the list in the app.';
|
||||
assert.ok(c.checkIssue({ title: '[GUI] pin models', body: f, mode: 'reopen' }).ok);
|
||||
assert.ok(!c.checkIssue({ title: '[GUI] pin models', body: '### Area\n\n[GUI]\n\n### What do you want to do?\n\n_No response_' }).ok);
|
||||
const q = '### Area\n\n[Other]\n\n### Your question\n\nHow do I move the model folder to another drive?';
|
||||
assert.ok(c.checkIssue({ title: '[Other] model folder', body: q }).ok);
|
||||
});
|
||||
|
||||
test('free-form issue', () => {
|
||||
const good = 'Strata 0.2.0-beta on an RTX 3060 12 GB with 32 GB RAM on Linux: the server hangs when I send two requests at once. Expected both to finish.';
|
||||
assert.ok(c.checkIssue({ title: '[Engine] hang on two requests', body: good, mode: 'reopen' }).ok);
|
||||
assert.ok(!c.checkIssue({ title: '[Engine] hang', body: 'it crashes', mode: 'intake' }).ok);
|
||||
const old = good.replace('0.2.0-beta', '0.1.40');
|
||||
assert.ok(c.checkIssue({ title: '[Engine] hang', body: old, mode: 'intake' }).ok);
|
||||
assert.ok(!c.checkIssue({ title: '[Engine] hang', body: old, mode: 'reopen' }).ok);
|
||||
assert.ok(c.checkIssue({ title: '[Other] idea', body: 'It would help if the app remembered my last model between runs, today I pick it again.', labels: ['enhancement'], mode: 'reopen' }).ok);
|
||||
});
|
||||
|
||||
const prBody = (over = {}) => {
|
||||
const p = {
|
||||
problem: 'Decode stopped on prompts above 8k because the stop check read the wrong length. Now it continues.',
|
||||
validation: '**Passed:** `ctest -R decode`, commit abcdef1, RTX 3060, 12 of 12.\n\n**Failed:** none observed\n\n**Not run:** HIP build',
|
||||
...over,
|
||||
};
|
||||
return `## Problem and result\n<!-- hint -->\n${p.problem}\n\n## Related discussion\nnone\n\n## Approach\nFix the length.\n\n## Validation\n${p.validation}\n\n## Limits\nnone`;
|
||||
};
|
||||
|
||||
test('filled PR passes', () => {
|
||||
assert.deepStrictEqual(c.checkPull({ title: '[Engine] fix stop', body: prBody(), mode: 'reopen' }).missing, []);
|
||||
});
|
||||
|
||||
test('untouched PR template fails', () => {
|
||||
const tpl = '## Problem and result\n<!-- What triggered -->\n\n## Validation\n<!-- cmds -->\n\n**Passed:**\n<!-- x -->\n\n**Failed:**\n\n**Not run:**\n';
|
||||
const r = c.checkPull({ title: '[Engine] fix stop', body: tpl });
|
||||
assert.ok(!r.ok);
|
||||
assert.ok(r.missing.some((m) => m.includes('Problem and result')));
|
||||
assert.ok(r.missing.some((m) => m.includes('Validation')));
|
||||
});
|
||||
|
||||
test('PR: empty body and bad title', () => {
|
||||
const r = c.checkPull({ title: 'fix', body: null });
|
||||
assert.ok(!r.ok);
|
||||
assert.ok(r.missing.length >= 2);
|
||||
});
|
||||
|
||||
test('PR reopen needs a revision', () => {
|
||||
const body = prBody({ validation: '**Passed:** I tried it and it worked for me on my machine, looked fine.' });
|
||||
assert.ok(c.checkPull({ title: '[Engine] x y', body, mode: 'intake' }).ok);
|
||||
assert.ok(!c.checkPull({ title: '[Engine] x y', body, mode: 'reopen' }).ok);
|
||||
});
|
||||
|
||||
test('free-form PR', () => {
|
||||
const body = 'This fixes a crash when the context is above 8k, because the length was read from the wrong field. Before it aborted, after it continues. I built it on CUDA and ran the decode test, passed.';
|
||||
assert.ok(c.checkPull({ title: '[Engine] fix crash', body }).ok);
|
||||
assert.ok(!c.checkPull({ title: '[Engine] fix crash', body: 'fixes stuff' }).ok);
|
||||
});
|
||||
|
||||
test('messages', () => {
|
||||
const m = c.intakeComment(false, ['a', 'b']);
|
||||
assert.ok(m.includes('- [ ] a') && m.includes('/reopen') && m.includes('CONTRIBUTING.md'));
|
||||
assert.ok(c.staleComment(true).includes('/reopen'));
|
||||
});
|
||||
|
||||
// handlers with a fake GitHub client
|
||||
function fake(calls, extra = {}) {
|
||||
const rest = {
|
||||
issues: {
|
||||
getLabel: async () => ({}),
|
||||
createLabel: async (a) => calls.push(['createLabel', a.name]),
|
||||
addLabels: async (a) => calls.push(['addLabels', a.labels]),
|
||||
removeLabel: async (a) => calls.push(['removeLabel', a.name]),
|
||||
createComment: async (a) => calls.push(['comment', a.body]),
|
||||
update: async (a) => calls.push(['update', a.state]),
|
||||
get: async () => ({ data: extra.live }),
|
||||
listEvents: async () => {},
|
||||
listForRepo: async () => {},
|
||||
},
|
||||
pulls: { get: async () => ({ data: { mergeable_state: 'clean' } }) },
|
||||
};
|
||||
return { rest, paginate: async (fn) => (fn === rest.issues.listEvents ? (extra.events || []) : (extra.items || [])) };
|
||||
}
|
||||
const core = { info() {} };
|
||||
const repo = { owner: 'o', repo: 'r' };
|
||||
|
||||
test('intake labels and comments once', async () => {
|
||||
const calls = [];
|
||||
const item = { number: 1, title: 'bad', body: '', labels: [], user: { type: 'User' }, author_association: 'NONE' };
|
||||
await bot.intake({ github: fake(calls), context: { repo, payload: { action: 'opened', issue: item } }, core });
|
||||
assert.deepStrictEqual(calls.map((c) => c[0]), ['addLabels', 'comment']);
|
||||
calls.length = 0;
|
||||
item.labels = [{ name: 'needs-format' }];
|
||||
await bot.intake({ github: fake(calls), context: { repo, payload: { action: 'edited', issue: item } }, core });
|
||||
assert.deepStrictEqual(calls, []);
|
||||
});
|
||||
|
||||
test('intake clears the label when edited into shape', async () => {
|
||||
const calls = [];
|
||||
const item = { number: 1, title: '[Engine] x y', body: bugForm(), labels: [{ name: 'needs-format' }], user: { type: 'User' } };
|
||||
await bot.intake({ github: fake(calls), context: { repo, payload: { action: 'edited', issue: item } }, core });
|
||||
assert.deepStrictEqual(calls.map((c) => c[0]), ['removeLabel', 'comment']);
|
||||
});
|
||||
|
||||
test('reopen: ignores strangers, reopens the author when it passes', async () => {
|
||||
const issue = { number: 5, state: 'closed', user: { login: 'amy' }, labels: [] };
|
||||
let calls = [];
|
||||
const comment = { body: '/reopen', user: { login: 'bob', type: 'User' }, author_association: 'NONE' };
|
||||
await bot.reopen({ github: fake(calls), context: { repo, payload: { issue, comment } }, core });
|
||||
assert.deepStrictEqual(calls, []);
|
||||
|
||||
const live = { number: 5, title: '[Engine] x y', body: bugForm(), labels: [] };
|
||||
comment.user.login = 'amy';
|
||||
await bot.reopen({ github: fake(calls, { live }), context: { repo, payload: { issue, comment } }, core });
|
||||
assert.deepStrictEqual(calls.map((c) => c[0]), ['update', 'removeLabel', 'comment']);
|
||||
assert.strictEqual(calls[0][1], 'open');
|
||||
|
||||
calls = [];
|
||||
live.title = 'no prefix';
|
||||
await bot.reopen({ github: fake(calls, { live }), context: { repo, payload: { issue, comment } }, core });
|
||||
assert.deepStrictEqual(calls.map((c) => c[0]), ['comment']);
|
||||
assert.ok(calls[0][1].includes('Not reopened'));
|
||||
});
|
||||
|
||||
test('sweep closes only old flagged items', async () => {
|
||||
const calls = [];
|
||||
const old = new Date(Date.now() - 6 * 86400000).toISOString();
|
||||
const fresh = new Date(Date.now() - 1 * 86400000).toISOString();
|
||||
const mk = (n, created) => ({ number: n, title: 'bad', body: '', labels: [{ name: 'needs-format' }], created_at: created });
|
||||
const gh = fake(calls, { items: [mk(1, old), mk(2, fresh)] });
|
||||
await bot.sweep({ github: gh, context: { repo }, core });
|
||||
assert.deepStrictEqual(calls.map((c) => c[0]), ['comment', 'update']);
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
'use strict';
|
||||
// GitHub-side handlers for intake.yml, reopen.yml and the 5-day sweep.
|
||||
// They take the actions/github-script objects ({ github, context, core }) and never touch PR code:
|
||||
// everything comes from the event payload and the REST API.
|
||||
|
||||
const check = require('./item-check');
|
||||
|
||||
const LABEL = 'needs-format';
|
||||
const STALE_DAYS = 5;
|
||||
const MAINTAINER = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
|
||||
// Items a maintainer closed on purpose stay closed.
|
||||
const BLOCK_LABELS = new Set(['wontfix', 'invalid', 'duplicate', 'declined']);
|
||||
|
||||
function labelNames(item) {
|
||||
return (item.labels || []).map((l) => (typeof l === 'string' ? l : l.name));
|
||||
}
|
||||
|
||||
async function ensureLabel(github, context) {
|
||||
const { owner, repo } = context.repo;
|
||||
try {
|
||||
await github.rest.issues.getLabel({ owner, repo, name: LABEL });
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
await github.rest.issues.createLabel({
|
||||
owner, repo, name: LABEL, color: 'fbca04',
|
||||
description: 'Title or description does not follow CONTRIBUTING.md yet',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function removeLabel(github, context, number) {
|
||||
const { owner, repo } = context.repo;
|
||||
try {
|
||||
await github.rest.issues.removeLabel({ owner, repo, issue_number: number, name: LABEL });
|
||||
} catch (e) {
|
||||
if (e.status !== 404) throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function itemFromPayload(context) {
|
||||
const p = context.payload;
|
||||
return p.pull_request || p.issue;
|
||||
}
|
||||
|
||||
function isPullItem(item, context) {
|
||||
return Boolean(context.payload.pull_request || item.pull_request);
|
||||
}
|
||||
|
||||
// issues: opened/edited/reopened, pull_request_target: opened/edited/reopened
|
||||
async function intake({ github, context, core }) {
|
||||
const { owner, repo } = context.repo;
|
||||
const item = itemFromPayload(context);
|
||||
if (!item || (item.user && item.user.type === 'Bot')) return;
|
||||
if (MAINTAINER.has(item.author_association) && context.payload.action === 'opened') {
|
||||
// Maintainer items are not nagged on creation.
|
||||
return;
|
||||
}
|
||||
const isPull = isPullItem(item, context);
|
||||
const labels = labelNames(item);
|
||||
const hasLabel = labels.includes(LABEL);
|
||||
const res = check.checkItem({ isPull, title: item.title, body: item.body, labels, mode: 'intake' });
|
||||
core.info('check ' + (isPull ? 'PR' : 'issue') + ' #' + item.number + ': ' + (res.ok ? 'ok' : res.missing.join(' | ')));
|
||||
|
||||
if (res.ok) {
|
||||
if (hasLabel) {
|
||||
await removeLabel(github, context, item.number);
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: item.number, body: 'Thanks, this follows the format now.' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (context.payload.action === 'edited' && !hasLabel) return; // edits only matter for items already flagged
|
||||
if (hasLabel) return; // one note only
|
||||
await ensureLabel(github, context);
|
||||
await github.rest.issues.addLabels({ owner, repo, issue_number: item.number, labels: [LABEL] });
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: item.number, body: check.intakeComment(isPull, res.missing) });
|
||||
}
|
||||
|
||||
// issue_comment created, body starts with /reopen
|
||||
async function reopen({ github, context, core }) {
|
||||
const { owner, repo } = context.repo;
|
||||
const comment = context.payload.comment;
|
||||
const issue = context.payload.issue;
|
||||
if (!/^\s*\/reopen\b/.test(comment.body || '')) return;
|
||||
if (comment.user.type === 'Bot') return;
|
||||
const isAuthor = comment.user.login === issue.user.login;
|
||||
const isMaint = MAINTAINER.has(comment.author_association);
|
||||
if (!isAuthor && !isMaint) {
|
||||
core.info('ignored /reopen from ' + comment.user.login + ' (not the author or a maintainer)');
|
||||
return;
|
||||
}
|
||||
const isPull = Boolean(issue.pull_request);
|
||||
const say = (body) => github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body });
|
||||
const labels = labelNames(issue);
|
||||
const wasOpen = issue.state === 'open';
|
||||
|
||||
if (labels.some((l) => BLOCK_LABELS.has(l.toLowerCase())) && !isMaint) {
|
||||
await say('This one was closed on purpose (' + labels.filter((l) => BLOCK_LABELS.has(l.toLowerCase())).join(', ') + '). Please add your new evidence in a comment and a maintainer will decide.');
|
||||
return;
|
||||
}
|
||||
if (wasOpen && !labels.includes(LABEL)) {
|
||||
core.info('already open, nothing to do');
|
||||
return;
|
||||
}
|
||||
|
||||
// The bot reads the live item, not the possibly stale event copy.
|
||||
const live = (await github.rest.issues.get({ owner, repo, issue_number: issue.number })).data;
|
||||
const res = check.checkItem({ isPull, title: live.title, body: live.body, labels: labelNames(live), mode: 'reopen' });
|
||||
core.info('reopen check #' + live.number + ': ' + (res.ok ? 'ok' : res.missing.join(' | ')));
|
||||
|
||||
if (!res.ok) {
|
||||
await say(check.reopenMissingComment(isPull, res.missing));
|
||||
return;
|
||||
}
|
||||
if (!wasOpen) {
|
||||
try {
|
||||
await github.rest.issues.update({ owner, repo, issue_number: live.number, state: 'open' });
|
||||
} catch (e) {
|
||||
await say('The check passed, but GitHub would not reopen it (' + (e.message || e.status) + '). ' + (isPull ? 'If the branch was deleted, restore it or open a new pull request and link this one.' : 'Please open a new issue and link this one.'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
await removeLabel(github, context, live.number);
|
||||
let extra = '';
|
||||
if (isPull) {
|
||||
try {
|
||||
const pr = (await github.rest.pulls.get({ owner, repo, pull_number: live.number })).data;
|
||||
if (pr.mergeable_state === 'dirty') extra = '\n\nIt has conflicts with main now: please rebase it.';
|
||||
} catch (e) { /* best effort */ }
|
||||
}
|
||||
await say(check.reopenOkComment(isPull) + extra);
|
||||
}
|
||||
|
||||
async function labelTime(github, owner, repo, number, fallback) {
|
||||
const events = await github.paginate(github.rest.issues.listEvents, { owner, repo, issue_number: number, per_page: 100 });
|
||||
let t = null;
|
||||
for (const e of events) {
|
||||
if (e.event === 'labeled' && e.label && e.label.name === LABEL) t = e.created_at;
|
||||
}
|
||||
return t || fallback;
|
||||
}
|
||||
|
||||
// scheduled: close items that kept the needs-format label for more than 5 days
|
||||
async function sweep({ github, context, core }) {
|
||||
const { owner, repo } = context.repo;
|
||||
const cutoff = Date.now() - STALE_DAYS * 86400 * 1000;
|
||||
const items = await github.paginate(github.rest.issues.listForRepo, { owner, repo, state: 'open', labels: LABEL, per_page: 100 });
|
||||
core.info(items.length + ' open items with ' + LABEL);
|
||||
for (const item of items) {
|
||||
const since = new Date(await labelTime(github, owner, repo, item.number, item.created_at)).getTime();
|
||||
if (since > cutoff) continue;
|
||||
const isPull = Boolean(item.pull_request);
|
||||
// Re-check first: an edit that fixed it should have cleared the label already; do not close good items.
|
||||
const res = check.checkItem({ isPull, title: item.title, body: item.body, labels: labelNames(item), mode: 'intake' });
|
||||
if (res.ok) {
|
||||
await removeLabel(github, context, item.number);
|
||||
continue;
|
||||
}
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number: item.number, body: check.staleComment(isPull) });
|
||||
await github.rest.issues.update({ owner, repo, issue_number: item.number, state: 'closed', state_reason: 'not_planned' });
|
||||
core.info('closed #' + item.number);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { intake, reopen, sweep, LABEL, STALE_DAYS };
|
||||
@@ -0,0 +1,114 @@
|
||||
name: Docker images
|
||||
|
||||
# Builds the Strata container images of one release from its engine packages and pushes them to ghcr.io.
|
||||
# Run by hand only (Actions tab -> Docker images -> Run workflow). Nothing starts it on its own.
|
||||
#
|
||||
# For each flavor the job downloads the engine package from the release and checks it against the signed update feed
|
||||
# (docker/fetch-engines.py: the feed's Ed25519 signature, the archive's SHA-256, the manifest hash, the package signature,
|
||||
# every file's hash), builds the image with docker/build.sh and, only when "push" is on, pushes
|
||||
# ghcr.io/niko1221/strata:<version>-<flavor> and ghcr.io/niko1221/strata:latest-<flavor>
|
||||
# The first run with "push" off builds everything without publishing it (a dry run).
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: 'Release tag whose engine packages go into the images (for example v0.2.1)'
|
||||
required: true
|
||||
type: string
|
||||
flavors:
|
||||
description: 'Flavors to build, comma separated (empty: every flavor the release has a package for)'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
push:
|
||||
description: 'Push the images to ghcr.io (off: build only, nothing is published)'
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
latest:
|
||||
description: 'Also move the latest-<flavor> tags (turn off for a pre-release or a test)'
|
||||
required: true
|
||||
default: true
|
||||
type: boolean
|
||||
source_ref:
|
||||
description: 'Git ref of the Dockerfile and the Terminal sources (empty: the release tag)'
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
IMAGE: ghcr.io/niko1221/strata
|
||||
|
||||
jobs:
|
||||
plan:
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
flavors: ${{ steps.list.outputs.flavors }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.source_ref || inputs.release_tag }}
|
||||
- name: Which flavors does the release have a package for
|
||||
id: list
|
||||
env:
|
||||
WANT: ${{ inputs.flavors }}
|
||||
TAG: ${{ inputs.release_tag }}
|
||||
run: |
|
||||
set -eu
|
||||
args="--release-tag $TAG --list --skip-missing"
|
||||
[ -n "$WANT" ] && args="$args --flavors $WANT"
|
||||
python3 docker/fetch-engines.py $args > list.tsv
|
||||
cat list.tsv
|
||||
flavors=$(awk -F'\t' '$3 != "SKIP" {printf "%s\"%s\"", (n++ ? "," : ""), $1}' list.tsv)
|
||||
[ -n "$flavors" ] || { echo "the release has no engine package for any requested flavor"; exit 1; }
|
||||
echo "flavors=[$flavors]" >> "$GITHUB_OUTPUT"
|
||||
|
||||
image:
|
||||
needs: plan
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 3
|
||||
matrix:
|
||||
flavor: ${{ fromJSON(needs.plan.outputs.flavors) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.source_ref || inputs.release_tag }}
|
||||
- name: Free disk space (the runner has about 14 GB; a package plus its image needs about 10 GB)
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL || true
|
||||
docker system prune -af >/dev/null 2>&1 || true
|
||||
df -h /
|
||||
- name: Log in to ghcr.io
|
||||
if: ${{ inputs.push }}
|
||||
env:
|
||||
REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REGISTRY_USER: ${{ github.actor }}
|
||||
run: printf '%s' "$REGISTRY_TOKEN" | docker login ghcr.io -u "$REGISTRY_USER" --password-stdin
|
||||
- name: Fetch, verify, build (and push when push is on)
|
||||
env:
|
||||
TAG: ${{ inputs.release_tag }}
|
||||
FLAVOR: ${{ matrix.flavor }}
|
||||
PUSH: ${{ inputs.push && '--push' || '' }}
|
||||
LATEST: ${{ !inputs.latest && '--no-latest' || '' }}
|
||||
run: |
|
||||
docker/publish.sh --release-tag "$TAG" --flavors "$FLAVOR" --image "$IMAGE" $PUSH $LATEST
|
||||
- name: Summary
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
{
|
||||
echo "### ${{ matrix.flavor }}"
|
||||
echo "- release: ${{ inputs.release_tag }}, pushed: ${{ inputs.push }}"
|
||||
echo "- image: \`$IMAGE:${INPUT_TAG#v}-${{ matrix.flavor }}\`"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
env:
|
||||
INPUT_TAG: ${{ inputs.release_tag }}
|
||||
@@ -0,0 +1,58 @@
|
||||
name: Intake
|
||||
|
||||
# New issues and pull requests that do not follow CONTRIBUTING.md get the needs-format label and one friendly note.
|
||||
# Items that keep the label for 5 days are closed by the daily sweep, with a note on how to /reopen.
|
||||
#
|
||||
# pull_request_target is used instead of pull_request on purpose: a pull request from a fork gets a read-only
|
||||
# GITHUB_TOKEN under pull_request, so it could not be labelled or commented on. It is safe here because this
|
||||
# workflow never checks out or runs the pull request's code. It checks out the default branch only (the bot
|
||||
# scripts) and reads the title and description from the event payload and the REST API.
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
pull_request_target:
|
||||
types: [opened, edited]
|
||||
schedule:
|
||||
- cron: '17 4 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: intake-${{ github.event.issue.number || github.event.pull_request.number || 'sweep' }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
check:
|
||||
if: github.event_name == 'issues' || github.event_name == 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
sparse-checkout: .github/scripts
|
||||
persist-credentials: false
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const bot = require('./.github/scripts/reset-bot.js');
|
||||
await bot.intake({ github, context, core });
|
||||
|
||||
sweep:
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
sparse-checkout: .github/scripts
|
||||
persist-credentials: false
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const bot = require('./.github/scripts/reset-bot.js');
|
||||
await bot.sweep({ github, context, core });
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Reopen
|
||||
|
||||
# An author comments /reopen on their closed issue or pull request. The check runs and the item is reopened,
|
||||
# or the bot replies with what is still missing. GitHub does not let authors reopen items a maintainer closed,
|
||||
# so this workflow does it for them.
|
||||
#
|
||||
# issue_comment always runs the workflow file from the default branch with a write-capable token, which is what
|
||||
# we need for fork authors too. It never checks out or executes the pull request's code: it reads the live
|
||||
# title and description through the REST API. Only the author of the item or a maintainer can trigger it.
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: reopen-${{ github.event.issue.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
reopen:
|
||||
if: startsWith(github.event.comment.body, '/reopen') && github.event.comment.user.type != 'Bot'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
sparse-checkout: .github/scripts
|
||||
persist-credentials: false
|
||||
- uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const bot = require('./.github/scripts/reset-bot.js');
|
||||
await bot.reopen({ github, context, core });
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Scripts test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/scripts/**'
|
||||
- '.github/workflows/intake.yml'
|
||||
- '.github/workflows/reopen.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
node-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- run: node --test .github/scripts/item-check.test.js
|
||||
@@ -0,0 +1,65 @@
|
||||
name: Minimal serving runtime
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/terminal-minimal-server.yml
|
||||
- requirements.txt
|
||||
- strata.py
|
||||
- strata_cli/**
|
||||
- strata_control/**
|
||||
- serve/**
|
||||
- tests/terminal/test_runtime.py
|
||||
- tools/minimal_server_smoke.py
|
||||
- tools/strata_tokenizer.py
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/terminal-minimal-server.yml
|
||||
- requirements.txt
|
||||
- strata.py
|
||||
- strata_cli/**
|
||||
- strata_control/**
|
||||
- serve/**
|
||||
- tests/terminal/test_runtime.py
|
||||
- tools/minimal_server_smoke.py
|
||||
- tools/strata_tokenizer.py
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
fake-engine:
|
||||
name: Linux / runtime requirements only
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install only pinned runtime requirements
|
||||
shell: bash
|
||||
run: |
|
||||
python -m venv "$RUNNER_TEMP/strata-runtime-venv"
|
||||
"$RUNNER_TEMP/strata-runtime-venv/bin/python" -m pip install -r requirements.txt
|
||||
- name: Check fake-engine serving without terminal packages
|
||||
shell: bash
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
report = Path(os.environ['RUNNER_TEMP']) / 'minimal-server-smoke.json'
|
||||
python = str(Path(os.environ['RUNNER_TEMP']) / 'strata-runtime-venv' / 'bin' / 'python')
|
||||
subprocess.run([python, 'tools/minimal_server_smoke.py',
|
||||
'--report', str(report)], check=True)
|
||||
print(report.read_text())
|
||||
PY
|
||||
- name: Run full server suite without terminal packages
|
||||
shell: bash
|
||||
run: |
|
||||
"$RUNNER_TEMP/strata-runtime-venv/bin/python" -m unittest discover -s serve -p 'test_*.py'
|
||||
@@ -0,0 +1,115 @@
|
||||
name: Portable monitor client
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/terminal-portable-monitor.yml
|
||||
- packaging/monitor_bundle.py
|
||||
- strata.py
|
||||
- strata_cli/**
|
||||
- strata_control/**
|
||||
- tools/minimal_cli_smoke.py
|
||||
- tools/portable_monitor_smoke.py
|
||||
- tools/portable_monitor_pty_smoke.py
|
||||
- requirements-monitor.txt
|
||||
- LICENSE
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/terminal-portable-monitor.yml
|
||||
- packaging/monitor_bundle.py
|
||||
- strata.py
|
||||
- strata_cli/**
|
||||
- strata_control/**
|
||||
- tools/minimal_cli_smoke.py
|
||||
- tools/portable_monitor_smoke.py
|
||||
- tools/portable_monitor_pty_smoke.py
|
||||
- requirements-monitor.txt
|
||||
- LICENSE
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
client:
|
||||
name: ${{ matrix.os }} / Python ${{ matrix.python }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
python: ['3.10', '3.11']
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
- name: Check source CLI without optional packages
|
||||
shell: bash
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
report = Path(os.environ['RUNNER_TEMP']) / 'minimal-cli-smoke.json'
|
||||
subprocess.run([sys.executable, 'tools/minimal_cli_smoke.py',
|
||||
'--report', str(report)], check=True)
|
||||
print(report.read_text())
|
||||
PY
|
||||
- name: Build and smoke test without site packages
|
||||
shell: bash
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
bundle = Path(os.environ['RUNNER_TEMP']) / 'strata-monitor.pyz'
|
||||
report = bundle.with_name('monitor-stdlib-smoke.json')
|
||||
subprocess.run([sys.executable, 'packaging/monitor_bundle.py',
|
||||
'build', str(bundle)], check=True)
|
||||
subprocess.run([sys.executable, 'tools/portable_monitor_smoke.py',
|
||||
'--bundle', str(bundle), '--report', str(report)], check=True)
|
||||
print(report.read_text())
|
||||
PY
|
||||
- name: Install optional Textual dependency
|
||||
if: matrix.python == '3.11'
|
||||
run: python -m pip install -r requirements-monitor.txt
|
||||
- name: Smoke test packaged interactive Overview
|
||||
if: matrix.python == '3.11'
|
||||
shell: bash
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
bundle = Path(os.environ['RUNNER_TEMP']) / 'strata-monitor.pyz'
|
||||
report = bundle.with_name('monitor-interactive-smoke.json')
|
||||
subprocess.run([sys.executable, 'tools/portable_monitor_smoke.py',
|
||||
'--bundle', str(bundle), '--interactive',
|
||||
'--report', str(report)], check=True)
|
||||
print(report.read_text())
|
||||
PY
|
||||
- name: Check POSIX terminal restoration
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.python == '3.11'
|
||||
shell: bash
|
||||
run: |
|
||||
python - <<'PY'
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
bundle = Path(os.environ['RUNNER_TEMP']) / 'strata-monitor.pyz'
|
||||
report = bundle.with_name('monitor-pty-smoke.json')
|
||||
subprocess.run([sys.executable, 'tools/portable_monitor_pty_smoke.py',
|
||||
'--bundle', str(bundle), '--report', str(report)], check=True)
|
||||
print(report.read_text())
|
||||
PY
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Title area
|
||||
|
||||
# Checks that a pull request or issue title starts with its area: [Engine], [GUI], [TUI], [Benchmark], [Docs] or [Other].
|
||||
# It reads the title only: no checkout, no labels, no comments, no write access.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
issues:
|
||||
types: [opened, edited, reopened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
title:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check the title
|
||||
env:
|
||||
TITLE: ${{ github.event.pull_request.title || github.event.issue.title }}
|
||||
run: |
|
||||
case "$TITLE" in
|
||||
"[Engine] "?*|"[GUI] "?*|"[TUI] "?*|"[Benchmark] "?*|"[Docs] "?*|"[Other] "?*)
|
||||
echo "Title OK: $TITLE" ;;
|
||||
*)
|
||||
echo "::error::The title must start with its area and then a short description. Allowed areas: [Engine], [GUI], [TUI], [Benchmark], [Docs], [Other]. Example: \"[Engine] fix decode stop on long prompts\". Your title: $TITLE"
|
||||
exit 1 ;;
|
||||
esac
|
||||
@@ -48,6 +48,7 @@ build-vision-cuda12.bat
|
||||
|
||||
# setup.py outputs (engine, downloads) and release builds
|
||||
/engine/
|
||||
/engine-sycl/
|
||||
/engine-cuda12/
|
||||
/build-release*/
|
||||
/dist/
|
||||
@@ -64,3 +65,23 @@ build-vision-cuda12.bat
|
||||
|
||||
# local Windows HIP packaging output
|
||||
dist-hip/
|
||||
|
||||
# Terminal / Desktop split: local venvs, build output and archives never enter git
|
||||
.terminal-venv/
|
||||
.terminal-test-venv/
|
||||
node_modules/
|
||||
*.dmg
|
||||
/release/artifacts/
|
||||
/apps/host/dist/
|
||||
/apps/desktop/src-tauri/target/
|
||||
/apps/desktop/src-tauri/resources/node/*
|
||||
!/apps/desktop/src-tauri/resources/node/.gitignore
|
||||
# the Control component (bundled CPython + wheels + strata_control) is built by apps/desktop/scripts/stage-control.py
|
||||
/apps/desktop/src-tauri/resources/control/*
|
||||
!/apps/desktop/src-tauri/resources/control/.gitignore
|
||||
# the pruned DeepSeek Harness (MIT) is built by apps/desktop/scripts/stage-harness.mjs
|
||||
/apps/desktop/src-tauri/resources/harness/*
|
||||
!/apps/desktop/src-tauri/resources/harness/.gitignore
|
||||
# screenshots and checkpoints from the Mac rebuild live outside git (StrataSplit\evidence-archive)
|
||||
/apps/host/docs/evidence/
|
||||
/apps/host/docs/checkpoints/
|
||||
@@ -1,35 +1,84 @@
|
||||
# AGENTS.md
|
||||
|
||||
Strata runs the Qwen3.8-Flash-Next mixture-of-experts model (and its Coder, Swift 1.5 and Unsloth variants) on a
|
||||
normal PC: one NVIDIA or AMD graphics card plus system RAM, on Windows or Linux. It has a C++/CUDA/HIP engine
|
||||
(`src/`, `include/`), a Python server with an OpenAI- and Anthropic-compatible API and a web app (`serve/`), and a
|
||||
one-click installer (`setup.py`, started by `START-HERE.bat` / `setup.sh`).
|
||||
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.1, engine 0.1.46. 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.
|
||||
- **The Terminal** (`strata.py`, `strata_cli/`, `strata_control/`): the `strata` command. It sets up a model, serves
|
||||
it and watches it. Agents use this part.
|
||||
- **The engine and the server** (`src/`, `include/`, `metal/`, `sycl/` for the C++/CUDA/HIP/SYCL/Metal engine;
|
||||
`serve/` for the Python server with an OpenAI- and Anthropic-compatible API and a web page). `setup.py`, started by
|
||||
`START-HERE.bat` or `setup.sh`, is the older one-click installer from source.
|
||||
|
||||
## Installing Strata for a user
|
||||
|
||||
Follow **[docs/AI_SETUP.md](docs/AI_SETUP.md)**: check the PC, pick the model by RAM, run setup non-interactively,
|
||||
start and verify the server, and connect the user's apps. Never expose the server beyond `127.0.0.1` without
|
||||
`--api-key`. As an alternative to shell commands, Strata's MCP server ([docs/MCP_SERVER.md](docs/MCP_SERVER.md))
|
||||
offers the same steps as tools.
|
||||
Follow **[docs/AI_SETUP.md](docs/AI_SETUP.md)**. For an agent that has a shell, the short version is:
|
||||
|
||||
1. Install the Terminal. It needs no administrator rights.
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh # Linux, Mac
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex # Windows PowerShell
|
||||
```
|
||||
|
||||
On Windows, open a new terminal afterwards so that `strata` is on the PATH.
|
||||
2. Check what would happen. Nothing is changed:
|
||||
|
||||
```sh
|
||||
strata launch --plan --model q2_0
|
||||
```
|
||||
|
||||
3. Set up and serve without questions. It downloads the engine for the card and the model (tens of GB), starts the
|
||||
server, prints the addresses and returns. The server keeps running.
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
Useful flags (all of them: `strata launch --help`): `--family`, `--model-path` (files the user already has),
|
||||
`--data-dir`, `--context 32k|64k|128k`, `--vision off|on|gpu|cpu`, `--parallel N`, `--gpu 0,1`,
|
||||
`--access local|lan|cloudflare|tailscale`, `--port`, `--api-key-env NAME`, `--new-key`, `--save-preset NAME`,
|
||||
`--output json`. `strata launch --status` shows what is serving, `strata launch --stop` stops it, and
|
||||
`strata launch --show-key` prints the API key.
|
||||
4. Connect the user's apps: `strata connect` lists them, `strata connect <app> --check` sends one prompt and one tool
|
||||
call the way that app does.
|
||||
|
||||
Keep the server on `127.0.0.1` unless the user asks for more. Beyond it an API key is recommended: `--access lan` makes one for you, and `--no-api-key` serves without one (a warning, never a refusal; on a public tunnel the warning is stronger). The old route (clone
|
||||
the repository and run `START-HERE.bat --yes` or `./setup.sh --yes`) still works and is the fallback when the
|
||||
Terminal cannot run. Strata's MCP server ([docs/MCP_SERVER.md](docs/MCP_SERVER.md)) offers the same steps as tools.
|
||||
|
||||
## Working on the code
|
||||
|
||||
- How the engine works, every measured number, the API and all settings: [docs/DETAILS.md](docs/DETAILS.md) and
|
||||
the [paper](docs/paper/Strata-Paper.pdf).
|
||||
- AMD (HIP) build and validation: [docs/AMD_HIP.md](docs/AMD_HIP.md); multi-GPU: [docs/MULTI_GPU.md](docs/MULTI_GPU.md).
|
||||
- Setup's own tests run without a GPU or downloads: `python tools/test_setup_<name>.py` (for example
|
||||
`tools/test_setup_amd.py`, `tools/test_setup_choices.py`).
|
||||
- Keep the docs' style: plain words, measured numbers with what they were measured on, no claims without a
|
||||
measurement.
|
||||
- The Terminal: [docs/TERMINAL.md](docs/TERMINAL.md) and [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). The app,
|
||||
the packages and the update feed: [packaging/README.md](packaging/README.md) and [packaging/UPDATES.md](packaging/UPDATES.md).
|
||||
- AMD (HIP) build and validation: [docs/AMD_HIP.md](docs/AMD_HIP.md); multi-GPU: [docs/MULTI_GPU.md](docs/MULTI_GPU.md);
|
||||
Mac (Metal): [docs/MAC.md](docs/MAC.md).
|
||||
- Tests: the Terminal and Control tests are in `tests/terminal` (`python -m pytest tests/terminal`). The app's UI
|
||||
tests run with `npm test` in `apps/host`. Setup's own tests run without a GPU or downloads:
|
||||
`python tools/test_setup_<name>.py` (for example `tools/test_setup_amd.py`, `tools/test_setup_choices.py`).
|
||||
- Keep the docs' style: plain words, short sentences, written for non-technical users first. Give measured numbers
|
||||
with what they were measured on. Make no claim without a measurement.
|
||||
|
||||
## Contributing a change or report
|
||||
|
||||
- Search the open issues and pull requests first, and add to a thread that already covers your point.
|
||||
- Open an issue with the form that fits (bug report, feature request or question).
|
||||
- One change per pull request. Say what it changes and what it leaves alone.
|
||||
The rules are in **[CONTRIBUTING.md](CONTRIBUTING.md)**. Read it before you open an issue or a pull request, and
|
||||
follow it whether a person or an agent writes the text. The short version:
|
||||
|
||||
- Start every issue and pull request title with its area: `[Engine]`, `[GUI]`, `[TUI]`, `[Benchmark]`, `[Docs]` or
|
||||
`[Other]`, for example `[Engine] fix …`. A check on GitHub fails a pull request whose title has no area.
|
||||
- Search the open and closed issues and pull requests first, and add to a thread that already covers your point.
|
||||
- Agree the scope in an issue before a large feature, a refactor or a change to a default.
|
||||
- One problem per pull request. Lead with the problem and the result, then the approach, then the validation
|
||||
(passed, failed and not run, kept apart) and the limits.
|
||||
- A new feature is opt-in, and the default path stays byte-identical to the last release. Say how you checked.
|
||||
- Build every backend a file touches (CUDA, HIP, SYCL) before asking for review.
|
||||
- Change a default only where you measured it faster, and show the numbers with what they were measured on.
|
||||
- Never submit measurements, logs or reproduction steps you did not produce. Mark a finding from reading code as
|
||||
unconfirmed static analysis.
|
||||
- A report from hardware the maintainers do not have is welcome. Follow
|
||||
[docs/COMMUNITY_BENCHMARKS.md](docs/COMMUNITY_BENCHMARKS.md), compare against a same-day run of the build you are
|
||||
testing, and say what you did not test.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
rem Benchmark this Strata install the way it is used and write a report you can share (docs/COMMUNITY_BENCHMARKS.md).
|
||||
rem It starts the model with your settings (or uses the one already running), takes about 10 minutes and changes nothing.
|
||||
rem Options: --clients 4 (several requests at once), --long (a 32K prompt), --compare <older results folder>.
|
||||
rem This is the quick speed check. The standard benchmark (docs/BENCHMARK_PROTOCOL.md), whose results other people can compare with, is `strata bench run`.
|
||||
setlocal
|
||||
title Strata - benchmark
|
||||
cd /d "%~dp0"
|
||||
@@ -10,6 +11,8 @@ if not exist ".venv\Scripts\python.exe" (
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo Quick speed check. For results other people can compare with (the standard benchmark, 20 to 30 minutes), run: .venv\Scripts\python.exe strata.py bench run
|
||||
echo.
|
||||
".venv\Scripts\python.exe" tools\strata_bench.py %*
|
||||
set "RC=%ERRORLEVEL%"
|
||||
pause
|
||||
|
||||
@@ -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.45 LANGUAGES CXX)
|
||||
project(strata VERSION 0.1.46.3 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)
|
||||
@@ -48,6 +48,17 @@ if(STRATA_PORTABLE AND MSVC)
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded")
|
||||
endif()
|
||||
|
||||
# #3202: Windows hands a program its command line in the system's ANSI code page (cp1251 / cp866 on a Russian Windows, cp1252 on an
|
||||
# English one), but the engine reads every path as UTF-8 (the run configs and the server write UTF-8). A model folder with
|
||||
# Cyrillic letters arrived as other bytes, or as "?" where the page has no such letter, and the model did not open. This manifest
|
||||
# makes the process's ANSI code page UTF-8 (Windows 10 1903 and newer; older Windows ignores it and nothing changes), so the
|
||||
# command line, fopen and std::ifstream all take UTF-8.
|
||||
function(strata_utf8_manifest target)
|
||||
if(MSVC)
|
||||
target_sources(${target} PRIVATE ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/cmake/strata-utf8.manifest)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
@@ -76,6 +87,7 @@ option(STRATA_PREFILL_MMQ "Build the optional GGML MMQ prefill path for HIP" OFF
|
||||
# CUDA_MODULE_LOADING=EAGER, and these took ~3.8 MB of VRAM - 3 IQ3_XXS expert slots on a P100 - from every model); on
|
||||
# HIP a code object loads on first use, so they cost other models nothing there (R9700: the same slots). Used at run
|
||||
# time with STRATA_MMQ_KQ=1 (opt-in: it changes the prompt's rounding against the FP16 path).
|
||||
option(STRATA_MMQ_TQ_OVERLAY "Gyro trellis MMQ prompt kernels on an upstream llama.cpp: an overlay of its ggml-cuda headers written at configure time (cmake/strata_ggml_tq_overlay.cmake)" ON)
|
||||
option(STRATA_MMQ_KQUANTS "Build the MMQ prompt kernels for Q4_K / Q5_K / Q5_1 / Q6_K: the experts of Unsloth's K-quant files, and on HIP the dense GGUF projections (STRATA_DENSE_MMQ)" ON)
|
||||
# Q6_K gate/up in the grouped expert kernels (Unsloth's UD-Q6_K_XL, and layer 2 of UD-Q5_K_XL; docs/QUANTS.md). On by
|
||||
# default since MODEL-UPDATE Phase 1: one more instance per grouped kernel in iq_kernels.cu, +19 KB of sm_60 code,
|
||||
@@ -254,6 +266,9 @@ target_link_libraries(strata-dequant PRIVATE strata_artifact)
|
||||
# this file is now nothing but the CLI entry point, and its inherited guard spells that as a "selftest"
|
||||
target_compile_definitions(strata-dequant PRIVATE STRATA_DEQUANT_SELFTEST)
|
||||
if(STRATA_BUILD_TESTS)
|
||||
add_executable(batch_telemetry_test src/core/batch_telemetry_test.cpp)
|
||||
target_include_directories(batch_telemetry_test PRIVATE include)
|
||||
add_test(NAME batch_telemetry_test COMMAND batch_telemetry_test)
|
||||
add_executable(logit_bias_test src/core/logit_bias_test.cpp)
|
||||
target_include_directories(logit_bias_test PRIVATE include)
|
||||
add_test(NAME logit_bias_test COMMAND logit_bias_test)
|
||||
@@ -289,6 +304,11 @@ if(STRATA_BUILD_TESTS)
|
||||
target_include_directories(split_rules_test PRIVATE include)
|
||||
add_test(NAME split_rules_test COMMAND split_rules_test)
|
||||
|
||||
# the prompt planner's host-memory model (#2824): header-only, no GPU
|
||||
add_executable(prefill_plan_test tests/program/prefill_plan_test.cpp)
|
||||
target_include_directories(prefill_plan_test PRIVATE include)
|
||||
add_test(NAME prefill_plan_test COMMAND prefill_plan_test)
|
||||
|
||||
# the default CPU prefill share is not armed when every expert is resident (#1595): header-only rule, no GPU
|
||||
add_executable(share_rules_test tests/prefill/share_rules_test.cpp)
|
||||
target_include_directories(share_rules_test PRIVATE include)
|
||||
@@ -356,6 +376,7 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
# reference is the whole point - a kernel checked against another copy of itself proves nothing.
|
||||
add_library(strata_kernels STATIC src/kernels/cuda/dequant_s2.cu src/kernels/cuda/s2_gemv.cu
|
||||
src/kernels/cuda/s_gemv.cu
|
||||
src/kernels/cuda/s_gemv_pair.cu
|
||||
src/kernels/cuda/s2_gemv_quads.cu
|
||||
src/kernels/cuda/s2_gemv_fast.cu
|
||||
src/kernels/cuda/router_top10.cu
|
||||
@@ -424,6 +445,13 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_include_directories(qsa_prompt_attn_parity PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
|
||||
target_link_libraries(qsa_prompt_attn_parity PRIVATE strata_kernels CUDA::cudart)
|
||||
endif()
|
||||
# PR #600: the Volta (sm_70) prompt attention's latency pipeline against the FP32 kernel it replaces. Only in the
|
||||
# experimental sm_60/sm_70 build, where prompt_attn_v70_kernel is compiled in.
|
||||
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA AND STRATA_EXPERIMENTAL_SM60)
|
||||
add_executable(qsa_prompt_attn_sm70_bench bench/qsa_prompt_attn_sm70_bench.cpp)
|
||||
target_include_directories(qsa_prompt_attn_sm70_bench PRIVATE ${CMAKE_CUDA_TOOLKIT_INCLUDE_DIRECTORIES})
|
||||
target_link_libraries(qsa_prompt_attn_sm70_bench PRIVATE strata_kernels CUDA::cudart)
|
||||
endif()
|
||||
# #512: the top-k's active-block bound (Turing's dispatch, forced on any card by STRATA_TOPK_ACTIVE_ANY) against
|
||||
# the capacity rule, selected ids bitwise
|
||||
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA)
|
||||
@@ -553,7 +581,7 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
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/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/vmm.cpp src/core/conversation_file.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)
|
||||
target_link_libraries(strata_engine PUBLIC strata_core strata_kernels strata_kernels_cpu ${_strata_gpu_runtime_target})
|
||||
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA AND NOT STRATA_HIP_GFX906)
|
||||
@@ -683,6 +711,11 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
endif()
|
||||
target_include_directories(strata_prefill PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include ${_strata_gpu_include_directories})
|
||||
target_link_libraries(strata_prefill PUBLIC strata_engine ${_strata_gpu_blas_target} ${_strata_gpu_runtime_target})
|
||||
if(STRATA_ENABLE_CUDA)
|
||||
# v100/gdn-chunk: the chunked Volta GDN prompt recurrence. The dispatch (src/prefill/kernels.cu) is
|
||||
# Volta-only (cc 7.0) and gated on STRATA_EXPERIMENTAL_SM60; the path itself stays opt-in (STRATA_GDN_CHUNK).
|
||||
target_sources(strata_prefill PRIVATE src/prefill/gdn_chunk.cu)
|
||||
endif()
|
||||
if(STRATA_ENABLE_HIP AND STRATA_HIPBLASLT_AVAILABLE)
|
||||
target_compile_definitions(strata_prefill PRIVATE STRATA_HIPBLASLT_AVAILABLE=1)
|
||||
target_link_libraries(strata_prefill PRIVATE roc::hipblaslt)
|
||||
@@ -700,8 +733,16 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
endif()
|
||||
|
||||
add_executable(strata src/program/generate.cpp)
|
||||
strata_utf8_manifest(strata)
|
||||
target_include_directories(strata PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(strata PRIVATE strata_engine strata_prefill strata_spec ${_strata_gpu_runtime_target})
|
||||
if(WIN32 AND STRATA_ENABLE_HIP)
|
||||
# 0.2.1.2: the ROCm math libraries live in <package>\rocm\bin, which is on PATH only when the launcher starts the engine; load them on first
|
||||
# use (main() adds that folder to the DLL search first) so `strata.exe --version` works from the package folder. amdhip64_7.dll and the
|
||||
# C++ runtime sit beside the exe (tools/hip/package_windows.py) and stay normal imports.
|
||||
target_link_libraries(strata PRIVATE delayimp)
|
||||
target_link_options(strata PRIVATE "LINKER:/DELAYLOAD:hipblas.dll" "LINKER:/DELAYLOAD:libhipblaslt.dll" "LINKER:/DELAYLOAD:rocblas.dll")
|
||||
endif()
|
||||
|
||||
# NEW-MODELS-PLAN.md T1: Qwen3.6-35B-A3B's reference path (one GPU, the model resident, plain kernels), checked
|
||||
# against llama.cpp by tools/qwen35/; the engine itself does not run the family yet
|
||||
@@ -710,7 +751,10 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(strata-q36ref PRIVATE strata_kernels strata_warnings ${_strata_gpu_runtime_target})
|
||||
# the engine path for the same family (serve protocol, prompt chunks, decode graph): src/qwen35/q36_engine.cu, a
|
||||
# library that `strata` switches to for a qwen35moe model (the family switch at the top of its main) and that
|
||||
# strata-q36 wraps on its own
|
||||
# strata-q36 wraps on its own. Not on gfx906 (#3172, #3271): it calls cuBLAS entry points the wave64 compat layer does
|
||||
# not map (cublasSgemm, cublasGemmStridedBatchedEx) and its kernels assume 32-lane warps; main() guards every use with
|
||||
# STRATA_HAVE_QWEN35, so a gfx906 build links and runs the Qwen3.8 family, and `strata --families` says so
|
||||
if(NOT STRATA_HIP_GFX906)
|
||||
add_library(strata_qwen35 STATIC src/qwen35/q36_engine.cu)
|
||||
target_include_directories(strata_qwen35 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(strata_qwen35 PRIVATE strata_prefill strata_kernels strata_warnings ${_strata_gpu_runtime_target})
|
||||
@@ -720,9 +764,11 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_compile_definitions(strata_qwen35 PRIVATE STRATA_Q36_CPU_POOL=1)
|
||||
endif()
|
||||
add_executable(strata-q36 src/qwen35/q36_main.cpp)
|
||||
strata_utf8_manifest(strata-q36)
|
||||
target_link_libraries(strata-q36 PRIVATE strata_qwen35 ${_strata_gpu_runtime_target})
|
||||
target_link_libraries(strata PRIVATE strata_qwen35)
|
||||
target_compile_definitions(strata PRIVATE STRATA_HAVE_QWEN35=1)
|
||||
endif()
|
||||
|
||||
if(STRATA_ENABLE_HIP)
|
||||
enable_testing()
|
||||
@@ -800,6 +846,9 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
add_test(NAME hip_prefill_hcd_exact_parity COMMAND hip_prefill_hcd_exact_parity)
|
||||
set_tests_properties(hip_prefill_hcd_exact_parity PROPERTIES SKIP_RETURN_CODE 77)
|
||||
list(APPEND _strata_hip_tests hip_prefill_hcd_exact_parity)
|
||||
# the HC write-back kernel timed at the prompt path's shapes with an output checksum (not a ctest; STRATA_GRW_V picks a variant)
|
||||
add_executable(hip_prefill_gr_write_bench tests/hip/prefill_gr_write_bench.cpp)
|
||||
target_link_libraries(hip_prefill_gr_write_bench PRIVATE strata_prefill strata_hip_runtime)
|
||||
# S6: the AMD fast router against the portable kernel, bitwise
|
||||
add_executable(hip_router_fast tests/hip/router_fast.cpp)
|
||||
target_link_libraries(hip_router_fast PRIVATE strata_kernels strata_hip_runtime)
|
||||
@@ -943,6 +992,15 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_include_directories(iq_multi_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(iq_multi_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME iq_multi_parity COMMAND iq_multi_parity)
|
||||
set_tests_properties(iq_multi_parity PROPERTIES ENVIRONMENT "STRATA_KQ_SPLIT=1") # the Q4_K / Q5_K / Q8_0 Split path against the old kernels
|
||||
# the same Split path on the other routes it can meet: the generic multi kernels (no V2K) and the fused gate/up + SwiGLU kernel, and the sub-16-row
|
||||
# gate/up route switched off (a K-quant row has 160 dots, not the 80 the sub-16 rows assume: STRATA_KQ_SPLIT=1 must stay bitwise on all of them)
|
||||
add_test(NAME iq_multi_parity_kq_nov2k COMMAND iq_multi_parity)
|
||||
set_tests_properties(iq_multi_parity_kq_nov2k PROPERTIES ENVIRONMENT "STRATA_KQ_SPLIT=1;STRATA_EXPERT_V2K=0")
|
||||
add_test(NAME iq_multi_parity_kq_swiglu COMMAND iq_multi_parity)
|
||||
set_tests_properties(iq_multi_parity_kq_swiglu PROPERTIES ENVIRONMENT "STRATA_KQ_SPLIT=1;STRATA_GU_SWIGLU=1")
|
||||
add_test(NAME iq_multi_parity_kq_nosub16 COMMAND iq_multi_parity)
|
||||
set_tests_properties(iq_multi_parity_kq_nosub16 PROPERTIES ENVIRONMENT "STRATA_KQ_SPLIT=1;STRATA_NO_SUB16_GU=1")
|
||||
# the prompt path's DeltaNet recurrence: gdn_rec_kh_kernel against gdn_rec_cols_pipe_kernel, bitwise (GPU,
|
||||
# synthetic, no model; --bench adds timings). NVIDIA only: cp.async and the CUDA occupancy API (not on gfx906)
|
||||
if(STRATA_ENABLE_CUDA AND NOT STRATA_HIP_GFX906)
|
||||
@@ -998,6 +1056,14 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
add_test(NAME gfx906_gdn_rec_head COMMAND gfx906_gdn_rec_head)
|
||||
set_tests_properties(gfx906_gdn_rec_head PROPERTIES TIMEOUT 120)
|
||||
endif()
|
||||
if(STRATA_ENABLE_CUDA)
|
||||
# the Volta prompt experts (gemm_iq_f16_grouped, STRATA_PF_WMMA) against a double reference; skipped below sm_70
|
||||
add_executable(pf_wmma_parity src/kernels/pf_wmma_parity.cpp)
|
||||
target_include_directories(pf_wmma_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(pf_wmma_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME pf_wmma_parity COMMAND pf_wmma_parity)
|
||||
set_tests_properties(pf_wmma_parity PROPERTIES SKIP_RETURN_CODE 77)
|
||||
endif()
|
||||
add_executable(shared_expert_parity src/kernels/shared_expert_parity.cpp)
|
||||
target_include_directories(shared_expert_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(shared_expert_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
@@ -1032,6 +1098,22 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(gdn_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME gdn_parity COMMAND gdn_parity --selftest)
|
||||
|
||||
# v100/gdn-chunk: the chunked GDN prompt recurrence vs the token recurrence and a host reference (GPU,
|
||||
# synthetic, no model). The chunked path changes the summation order - FP32-level, not bit-exact - so the
|
||||
# test asserts measured error bounds, and --selftest pins them.
|
||||
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA)
|
||||
add_executable(gdn_chunk_parity src/kernels/gdn_chunk_parity.cpp)
|
||||
target_include_directories(gdn_chunk_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(gdn_chunk_parity PRIVATE strata_prefill ${_strata_gpu_runtime_target})
|
||||
add_test(NAME gdn_chunk_parity COMMAND gdn_chunk_parity --selftest)
|
||||
set_tests_properties(gdn_chunk_parity PROPERTIES SKIP_RETURN_CODE 77)
|
||||
|
||||
# v100/gdn-chunk: the micro benchmark (CUDA events, real GDN shapes): chunked vs recurrent prompt path.
|
||||
add_executable(gdn_chunk_bench bench/gdn_chunk_bench.cpp)
|
||||
target_include_directories(gdn_chunk_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(gdn_chunk_bench PRIVATE strata_prefill ${_strata_gpu_runtime_target})
|
||||
endif()
|
||||
|
||||
add_executable(s2_gemv_q8_parity src/kernels/s2_gemv_q8_parity.cpp)
|
||||
target_include_directories(s2_gemv_q8_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(s2_gemv_q8_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
@@ -1139,6 +1221,16 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(bf16_gemv_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME bf16_gemv_parity COMMAND bf16_gemv_parity --selftest)
|
||||
|
||||
# the V100 fusion work: every fused kernel against its unfused pipeline, bitwise, and the before/after
|
||||
# CUDA-event timings at decode shapes (`--bench`). It is a .cu because it carries the unfused reference
|
||||
# kernels it compares against.
|
||||
if(STRATA_BUILD_TESTS AND STRATA_ENABLE_CUDA)
|
||||
add_executable(fusions_parity bench/micro/fusions_parity.cu)
|
||||
target_include_directories(fusions_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(fusions_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME fusions_parity COMMAND fusions_parity)
|
||||
endif()
|
||||
|
||||
add_executable(s_gemv_q8k_parity src/kernels/s_gemv_q8k_parity.cpp)
|
||||
target_include_directories(s_gemv_q8k_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(s_gemv_q8k_parity PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
@@ -1241,6 +1333,11 @@ if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
target_link_libraries(platform_memory_test PRIVATE strata_core ${_strata_gpu_runtime_target})
|
||||
add_test(NAME platform_memory_test COMMAND platform_memory_test)
|
||||
|
||||
# The pinned-memory headroom (pin_budget.hpp) against a simulated ceiling; skips without a GPU.
|
||||
add_executable(pin_budget_test src/core/pin_budget_test.cpp)
|
||||
target_link_libraries(pin_budget_test PRIVATE strata_core ${_strata_gpu_runtime_target})
|
||||
add_test(NAME pin_budget_test COMMAND pin_budget_test)
|
||||
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
add_executable(pinned_shared_test src/core/pinned_shared_test.cpp)
|
||||
target_link_libraries(pinned_shared_test PRIVATE strata_core ${_strata_gpu_runtime_target})
|
||||
@@ -1359,7 +1456,7 @@ if(STRATA_BUILD_TESTS OR STRATA_BUILD_CONVERSATION_TESTS)
|
||||
endif()
|
||||
|
||||
add_library(strata_kernels_cpu STATIC src/kernels/cpu/expert.cpp src/kernels/cpu/pool.cpp
|
||||
src/kernels/cpu/expert_layout.cpp src/kernels/cpu/q2_avx2.cpp)
|
||||
src/kernels/cpu/expert_layout.cpp src/kernels/cpu/q2_avx2.cpp src/kernels/cpu/hadamard.cpp)
|
||||
target_include_directories(strata_kernels_cpu PUBLIC include)
|
||||
# Plan v0.3 P6: only the kernel files get the AVX-512 / AVX2 flags; the pool and the probe run on any x86-64,
|
||||
# and the engine picks the kernels at run time (cpu_avx512_ok).
|
||||
@@ -1467,7 +1564,8 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
target_sources(strata_kernels_cpu PRIVATE src/kernels/cpu/native_expert.cpp src/kernels/cpu/iq_avx512.cpp
|
||||
src/kernels/cpu/iq_avx2.cpp src/kernels/cpu/kq_avx2.cpp
|
||||
src/kernels/cpu/kq_avx1.cpp src/kernels/cpu/q2_avx1.cpp src/kernels/cpu/iq_avx1.cpp
|
||||
src/kernels/cpu/nvfp4_avx512.cpp src/kernels/cpu/nvfp4_avx2.cpp)
|
||||
src/kernels/cpu/nvfp4_avx512.cpp src/kernels/cpu/nvfp4_avx2.cpp
|
||||
src/kernels/cpu/tq_cpu.cpp src/kernels/cpu/tq_avx2.cpp src/kernels/cpu/tq_avx512.cpp)
|
||||
target_include_directories(strata_kernels_cpu PRIVATE third_party/ggml)
|
||||
if(MSVC)
|
||||
set_source_files_properties(src/kernels/cpu/iq_avx512.cpp src/kernels/cpu/nvfp4_avx512.cpp
|
||||
@@ -1475,6 +1573,8 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
set_source_files_properties(src/kernels/cpu/iq_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2")
|
||||
set_source_files_properties(src/kernels/cpu/kq_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2")
|
||||
set_source_files_properties(src/kernels/cpu/nvfp4_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2")
|
||||
set_source_files_properties(src/kernels/cpu/tq_avx2.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX2")
|
||||
set_source_files_properties(src/kernels/cpu/tq_avx512.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX512")
|
||||
# the router lookahead's AVX1 dot (older CPUs; called behind cpu_avx1_ok): no FMA, no 256-bit integer ops
|
||||
set_source_files_properties(src/kernels/cpu/kq_avx1.cpp PROPERTIES COMPILE_OPTIONS "/arch:AVX")
|
||||
# the older CPUs' Q2_0 rows and activation quantizer (q2_avx1.cpp, issue #1699): 128-bit integer lanes,
|
||||
@@ -1488,6 +1588,10 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
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")
|
||||
# the trellis kernels: AVX2 + FMA, and AVX-512 F/BW/VL/VBMI/VNNI (exactly what cpu_avx512_ok() checks)
|
||||
set_source_files_properties(src/kernels/cpu/tq_avx2.cpp PROPERTIES COMPILE_OPTIONS "-mavx2;-mfma;-mf16c")
|
||||
set_source_files_properties(src/kernels/cpu/tq_avx512.cpp PROPERTIES COMPILE_OPTIONS
|
||||
"-mavx512f;-mavx512bw;-mavx512vl;-mavx512vnni;-mavx512vbmi;-mavx2;-mfma;-mf16c")
|
||||
set_source_files_properties(src/kernels/cpu/kq_avx1.cpp PROPERTIES COMPILE_OPTIONS "-mavx")
|
||||
# the older CPUs' Q2_0 rows and activation quantizer (q2_avx1.cpp, issue #1699): SSE4.2 + AVX with
|
||||
# AVX2/FMA/F16C explicitly off, so a -march=native build host cannot leak AVX2 code into the file that
|
||||
@@ -1551,6 +1655,34 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
add_test(NAME q8k_quant_parity_avx COMMAND ${CMAKE_COMMAND} -E env STRATA_FORCE_ISA=avx $<TARGET_FILE:q8k_quant_parity>)
|
||||
endif()
|
||||
set(_strata_mmq_types q2_0 iq2_xxs iq2_xs iq2_s iq3_xxs iq3_s iq4_nl iq4_xs q8_0)
|
||||
# APR: the agention llama.cpp fork has MMQ instances for its trellis expert types (f62fdead0); any other ggml
|
||||
# tree leaves them to the FP16 prompt path
|
||||
if(STRATA_GGML_DIR AND EXISTS "${STRATA_GGML_DIR}/ggml/src/ggml-cuda/template-instances/mmq-instance-tqk6.cu")
|
||||
list(APPEND _strata_mmq_types tq2_t tqk6 tqk7)
|
||||
set(_strata_mmq_tq ON)
|
||||
elseif(STRATA_MMQ_TQ_OVERLAY AND (STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP))
|
||||
# an upstream llama.cpp: the trellis MMQ instances come from an overlay of the pinned ggml-cuda headers written at
|
||||
# configure time (cmake/strata_ggml_tq_overlay.cmake); off with a message if the llama.cpp tree is not the pinned one
|
||||
if(STRATA_GGML_DIR)
|
||||
set(_strata_tq_llama "${STRATA_GGML_DIR}")
|
||||
else()
|
||||
set(_strata_tq_llama "${strata_llamacpp_SOURCE_DIR}")
|
||||
endif()
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/strata_ggml_tq_overlay.cmake)
|
||||
set(_strata_tq_overlay_dir "${CMAKE_BINARY_DIR}/ggml-tq-overlay")
|
||||
strata_ggml_tq_overlay("${_strata_tq_llama}" "${_strata_tq_overlay_dir}")
|
||||
if(STRATA_TQ_OVERLAY_OK)
|
||||
configure_file(third_party/ggml/ggml-common-tq.h "${_strata_tq_overlay_dir}/ggml-common-tq.h" COPYONLY)
|
||||
configure_file(src/kernels/cuda/tq_lut_i8.cuh "${_strata_tq_overlay_dir}/tq_lut_i8.cuh" COPYONLY)
|
||||
set(_strata_mmq_tq ON)
|
||||
set(_strata_mmq_tq_overlay ON)
|
||||
message(STATUS "trellis MMQ (Gyro prompts): overlay on the pinned ggml-cuda in ${_strata_tq_overlay_dir}")
|
||||
if(STRATA_BUILD_TESTS)
|
||||
add_test(NAME ggml_tq_overlay COMMAND ${CMAKE_COMMAND} -DLLAMA=${_strata_tq_llama}
|
||||
-DOUT=${CMAKE_BINARY_DIR}/ggml-tq-overlay-test -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/ggml_tq_overlay_test.cmake)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
# #296: OrcaRouter Q4_K_S's Q5_0 experts (Q4_K and Q5_1 come with STRATA_MMQ_KQUANTS on CUDA, S5's branch)
|
||||
if(STRATA_ORCA_Q4KS_MMQ)
|
||||
list(APPEND _strata_mmq_types q5_0)
|
||||
@@ -1565,11 +1697,15 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
set(_strata_gg ${strata_llamacpp_SOURCE_DIR}/ggml)
|
||||
endif()
|
||||
set(_strata_mmq ${_strata_gg}/src/ggml-cuda)
|
||||
set(_strata_quantize_cu ${_strata_mmq}/quantize.cu) # the overlay's copy when the trellis overlay is on: it includes mmq.cuh
|
||||
if(_strata_mmq_tq_overlay)
|
||||
set(_strata_quantize_cu ${_strata_tq_overlay_dir}/quantize.cu)
|
||||
endif()
|
||||
# #136: the Q2_0 pack's prompt experts on Strata's own int8 kernels (moe_fused.cu, opt-in STRATA_PF_FUSED=1; CUDA
|
||||
# only: the HIP branch below keeps MMQ, and an sm_75 device or an sm_75-only build reports them unavailable)
|
||||
add_library(strata_mmq STATIC src/prefill/moe_mmq.cu src/prefill/moe_fused.cu src/prefill/moe_fused_iq.cu
|
||||
src/prefill/mmq_nvfp4_w4a8.cu
|
||||
src/prefill/ggml_cuda_host.cu ${_strata_mmq}/quantize.cu)
|
||||
src/prefill/ggml_cuda_host.cu ${_strata_quantize_cu})
|
||||
# q8_0: the drafter's matrices (E-9); STRATA_ORCA_Q4KS_MMQ adds q5_0 (#296, in _strata_mmq_types), and
|
||||
# STRATA_MMQ_KQUANTS q4_k q5_k q5_1 q6_k, the experts of Unsloth's UD-Q4/Q5/Q6_K_XL (docs/QUANTS.md; the dispatch
|
||||
# follows eddoursul/Strata 7706c1b). MODEL-UPDATE Phase 1: on CUDA they are a library of their own, strata_kq
|
||||
@@ -1587,6 +1723,13 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
foreach(_t IN LISTS _strata_mmq_cuda_types)
|
||||
target_sources(strata_mmq PRIVATE ${_strata_mmq}/template-instances/mmq-instance-${_t}.cu)
|
||||
endforeach()
|
||||
if(_strata_mmq_tq_overlay)
|
||||
foreach(_t tq2_t tqk6 tqk7)
|
||||
target_sources(strata_mmq PRIVATE ${_strata_tq_overlay_dir}/template-instances/mmq-instance-${_t}.cu)
|
||||
endforeach()
|
||||
# first on the include path: the patched mmq.cuh / ggml.h shadow the pinned ones for this target only
|
||||
target_include_directories(strata_mmq BEFORE PRIVATE ${_strata_tq_overlay_dir} ${_strata_tq_overlay_dir}/include)
|
||||
endif()
|
||||
# NVFP4: W4A8 (default) in mmq_nvfp4_w4a8.cu with the plain architectures; the opt-in W4A4 needs RTX 50's FP4 MMA,
|
||||
# which exists only in the arch-specific target, so its one unit alone is built with 12x -> 12xa (the default
|
||||
# binary is unchanged and runs on sm_121; a card without that image falls back to W4A8 at run time)
|
||||
@@ -1690,6 +1833,9 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
if(STRATA_ORCA_Q4KS_MMQ)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_ORCA_Q4KS_MMQ=1)
|
||||
endif()
|
||||
if(_strata_mmq_tq)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_MMQ_TQ=1)
|
||||
endif()
|
||||
target_link_libraries(strata_prefill PUBLIC strata_mmq)
|
||||
target_compile_definitions(strata_prefill PUBLIC STRATA_PREFILL_MMQ=1 STRATA_PREFILL_FUSED=1)
|
||||
if(STRATA_BUILD_TESTS)
|
||||
@@ -1743,6 +1889,10 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
set(_strata_gg "${strata_llamacpp_SOURCE_DIR}/ggml")
|
||||
endif()
|
||||
set(_strata_mmq "${_strata_gg}/src/ggml-cuda")
|
||||
set(_strata_quantize_cu "${_strata_mmq}/quantize.cu") # the overlay's copy when the trellis overlay is on: it includes mmq.cuh
|
||||
if(_strata_mmq_tq_overlay)
|
||||
set(_strata_quantize_cu "${_strata_tq_overlay_dir}/quantize.cu")
|
||||
endif()
|
||||
if(NOT EXISTS "${_strata_mmq}/common.cuh" OR NOT EXISTS "${_strata_mmq}/quantize.cu")
|
||||
message(FATAL_ERROR "The selected GGML source is missing the ggml-cuda MMQ sources: ${_strata_mmq}")
|
||||
endif()
|
||||
@@ -1753,7 +1903,7 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
src/prefill/moe_fused_iq.cu
|
||||
src/prefill/moe_f16_kq.cu
|
||||
src/prefill/ggml_cuda_host.cu
|
||||
"${_strata_mmq}/quantize.cu")
|
||||
"${_strata_quantize_cu}")
|
||||
if(STRATA_MMQ_KQUANTS)
|
||||
# the same K-quant instances as the CUDA branch: on HIP they serve the dense GGUF projections
|
||||
# of Gemm::native (STRATA_DENSE_MMQ=1), the mixed-quant packs' attention/PLE tensors
|
||||
@@ -1762,6 +1912,11 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
foreach(_t IN LISTS _strata_mmq_types)
|
||||
list(APPEND _strata_mmq_sources "${_strata_mmq}/template-instances/mmq-instance-${_t}.cu")
|
||||
endforeach()
|
||||
if(_strata_mmq_tq_overlay)
|
||||
foreach(_t tq2_t tqk6 tqk7)
|
||||
list(APPEND _strata_mmq_sources "${_strata_tq_overlay_dir}/template-instances/mmq-instance-${_t}.cu")
|
||||
endforeach()
|
||||
endif()
|
||||
set_source_files_properties(${_strata_mmq_sources} PROPERTIES LANGUAGE HIP)
|
||||
# moe_fused.cu is Strata's own CUDA-shaped source: it takes the HIP compat headers, ggml's MMQ sources do not
|
||||
set_source_files_properties(src/prefill/moe_fused.cu src/prefill/moe_fused_iq.cu src/prefill/moe_f16_kq.cu
|
||||
@@ -1770,6 +1925,9 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
target_include_directories(strata_mmq
|
||||
PRIVATE "${_strata_gg}/include" "${_strata_gg}/src" "${_strata_mmq}"
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
if(_strata_mmq_tq_overlay)
|
||||
target_include_directories(strata_mmq BEFORE PRIVATE ${_strata_tq_overlay_dir} ${_strata_tq_overlay_dir}/include)
|
||||
endif()
|
||||
target_compile_definitions(strata_mmq PRIVATE GGML_USE_HIP=1)
|
||||
if(STRATA_MMQ_KQUANTS)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_MMQ_KQUANTS=1)
|
||||
@@ -1777,6 +1935,9 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
if(STRATA_ORCA_Q4KS_MMQ)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_ORCA_Q4KS_MMQ=1)
|
||||
endif()
|
||||
if(_strata_mmq_tq)
|
||||
target_compile_definitions(strata_mmq PRIVATE STRATA_MMQ_TQ=1)
|
||||
endif()
|
||||
target_link_libraries(strata_mmq PUBLIC ggml-base hip::host roc::hipblas)
|
||||
target_link_libraries(strata_prefill PUBLIC strata_mmq)
|
||||
target_compile_definitions(strata_prefill PUBLIC STRATA_PREFILL_MMQ=1 STRATA_PREFILL_FUSED=1)
|
||||
@@ -1825,7 +1986,8 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
if(STRATA_ENABLE_CUDA OR STRATA_ENABLE_HIP)
|
||||
add_executable(native_expert_parity src/kernels/native_expert_parity.cpp)
|
||||
target_include_directories(native_expert_parity PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(native_expert_parity PRIVATE strata_kernels strata_kernels_cpu strata_artifact ${_strata_gpu_runtime_target})
|
||||
# strata_engine: the --load-check mode runs the engine's start-up checks (NativeDense, check_experts_gguf)
|
||||
target_link_libraries(native_expert_parity PRIVATE strata_engine strata_kernels strata_kernels_cpu strata_artifact ${_strata_gpu_runtime_target})
|
||||
add_executable(native_expert_bench src/kernels/native_expert_bench.cpp)
|
||||
target_include_directories(native_expert_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(native_expert_bench PRIVATE strata_kernels strata_kernels_cpu strata_artifact ${_strata_gpu_runtime_target})
|
||||
@@ -1864,6 +2026,16 @@ if(STRATA_NATIVE_EXPERTS)
|
||||
set_tests_properties(native_expert_parity_${_name} PROPERTIES TIMEOUT 120)
|
||||
endforeach()
|
||||
add_test(NAME native_expert_parity_q5_1_min COMMAND native_expert_parity --q5_1-min)
|
||||
# APR: the trellis expert types and the Hadamard activation rotation, when ggml comes from the agention fork
|
||||
# (the only tree with these types; marked by its ggml-cuda/tq.cuh)
|
||||
if(STRATA_GGML_DIR AND EXISTS "${STRATA_GGML_DIR}/ggml/src/ggml-cuda/tq.cuh")
|
||||
foreach(_pair tqk6/tqk7 tq2_t/tq2_t)
|
||||
string(REPLACE "/" "_" _name "${_pair}")
|
||||
add_test(NAME native_expert_parity_${_name} COMMAND native_expert_parity --synthetic ${_pair})
|
||||
add_test(NAME native_expert_parity_hadamard_${_name} COMMAND native_expert_parity --synthetic-hadamard ${_pair})
|
||||
set_tests_properties(native_expert_parity_${_name} native_expert_parity_hadamard_${_name} PROPERTIES TIMEOUT 300)
|
||||
endforeach()
|
||||
endif()
|
||||
# #290: the BF16 token embedding (--embd-gguf) gathered and dequantized bit for bit
|
||||
add_test(NAME native_expert_parity_bf16_embd COMMAND native_expert_parity --bf16-embd)
|
||||
endif()
|
||||
@@ -1917,6 +2089,22 @@ if(STRATA_BUILD_TESTS)
|
||||
set_tests_properties(q2_bitplane_parity PROPERTIES ENVIRONMENT "STRATA_Q2_BITPLANE=1")
|
||||
endif()
|
||||
|
||||
if(STRATA_NATIVE_EXPERTS)
|
||||
# APR port: a Hadamard-folded trellis expert (TQ2_T / TQK6 / TQK7) on the CPU path vs llama.cpp's graph for the
|
||||
# same weights (synthetic; needs STRATA_GGML_DIR = a ggml with the trellis types, else it reports and fails)
|
||||
add_executable(native_hadamard_test src/kernels/cpu/native_hadamard_test.cpp)
|
||||
target_link_libraries(native_hadamard_test PRIVATE strata_kernels_cpu strata_artifact)
|
||||
add_test(NAME native_hadamard_test COMMAND native_hadamard_test)
|
||||
set_tests_properties(native_hadamard_test PROPERTIES TIMEOUT 300)
|
||||
# the CPU trellis kernels (tq_cpu.hpp): AVX2 / AVX-512 vs the scalar kernel bit for bit, vs ggml-cpu's vec_dot
|
||||
# and to_float, plain and with the Hadamard rotation; `tq_cpu_parity --bench` is the micro-benchmark
|
||||
add_executable(tq_cpu_parity src/kernels/cpu/tq_cpu_parity.cpp)
|
||||
target_link_libraries(tq_cpu_parity PRIVATE strata_kernels_cpu)
|
||||
add_test(NAME tq_cpu_parity COMMAND tq_cpu_parity)
|
||||
add_test(NAME tq_cpu_parity_avx2 COMMAND ${CMAKE_COMMAND} -E env STRATA_FORCE_AVX2=1 $<TARGET_FILE:tq_cpu_parity>)
|
||||
set_tests_properties(tq_cpu_parity tq_cpu_parity_avx2 PROPERTIES TIMEOUT 300)
|
||||
endif()
|
||||
|
||||
add_executable(expert_parity src/kernels/cpu/expert_parity.cpp)
|
||||
target_link_libraries(expert_parity PRIVATE strata_kernels_cpu)
|
||||
# The pack is a REQUIRED input, not an optional one: this test compares against a real expert blob, and a
|
||||
@@ -1927,6 +2115,14 @@ if(STRATA_BUILD_TESTS)
|
||||
add_executable(pool_test src/kernels/cpu/pool_test.cpp)
|
||||
target_link_libraries(pool_test PRIVATE strata_kernels_cpu)
|
||||
add_test(NAME pool_test COMMAND pool_test --selftest WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
# the default CPU expert worker count on mock CPUs (8c/16t, 16c/32t, 64c/128t, 2-socket 128c/256t, ...)
|
||||
add_executable(pool_plan_test src/kernels/cpu/pool_plan_test.cpp)
|
||||
target_include_directories(pool_plan_test PRIVATE include)
|
||||
add_test(NAME pool_plan_test COMMAND pool_plan_test)
|
||||
# #3204: when the HIP start-up warm-up of hipBLAS may run (pure policy, no GPU)
|
||||
add_executable(warm_policy_test src/prefill/warm_policy_test.cpp)
|
||||
target_include_directories(warm_policy_test PRIVATE include)
|
||||
add_test(NAME warm_policy_test COMMAND warm_policy_test)
|
||||
# issue #29: tiny batches with sleeping workers, as on a big-VRAM card; a hang fails it
|
||||
add_executable(pool_stress src/kernels/cpu/pool_stress.cpp)
|
||||
target_link_libraries(pool_stress PRIVATE strata_kernels_cpu)
|
||||
@@ -1996,6 +2192,23 @@ if(STRATA_HIP_GFX906)
|
||||
file(GLOB _strata_mmq_cu ${_strata_mmq}/*.cu ${_strata_mmq}/template-instances/*.cu)
|
||||
list(APPEND _strata_cu ${_strata_mmq_cu})
|
||||
target_compile_definitions(strata_mmq PRIVATE GGML_USE_HIP)
|
||||
# #3172: strata_mmq_w4a4 is built from llama.cpp's headers too; without GGML_USE_HIP its common.cuh took the CUDA vendor header
|
||||
# (cuda.h, cuda_bf16.h: not found). Here it only reports that W4A4 (RTX 50's FP4 MMA) is not available.
|
||||
if(TARGET strata_mmq_w4a4)
|
||||
target_compile_definitions(strata_mmq_w4a4 PRIVATE GGML_USE_HIP)
|
||||
endif()
|
||||
# #3271: three more link errors of a 0.1.44+ gfx906 build, all sources that strata_mmq lists but the glob above never saw:
|
||||
# the trellis overlay's quantize.cu and instances (they are generated into the build tree, so the glob missed them: CMake drops a
|
||||
# .cu source whose language is not enabled) and moe_f16_kq.cu, which moe_fused_iq.cu calls (a stub here: no gfx11 device code, so
|
||||
# f16kq::covers() answers false and the int8 kernels run, as on every other card)
|
||||
if(_strata_mmq_tq_overlay)
|
||||
set(_strata_tq_hip ${_strata_tq_overlay_dir}/quantize.cu)
|
||||
foreach(_t tq2_t tqk6 tqk7)
|
||||
list(APPEND _strata_tq_hip ${_strata_tq_overlay_dir}/template-instances/mmq-instance-${_t}.cu)
|
||||
endforeach()
|
||||
list(APPEND _strata_cu ${_strata_tq_hip})
|
||||
endif()
|
||||
target_sources(strata_mmq PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/prefill/moe_f16_kq.cu)
|
||||
endif()
|
||||
set_source_files_properties(${_strata_cu} PROPERTIES LANGUAGE HIP)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# Contributing to Strata
|
||||
|
||||
**AI assistance is welcome. The contributor is responsible for understanding the change and supplying real evidence.**
|
||||
|
||||
These rules apply to issues, pull requests, benchmark reports, and the agents helping prepare them.
|
||||
You should be able to explain every changed line and answer review questions about it.
|
||||
|
||||
## Before starting
|
||||
|
||||
1. Search [issues](https://github.com/Niko1221/Strata/issues) and
|
||||
[pull requests](https://github.com/Niko1221/Strata/pulls), including closed discussions. Add new evidence to an
|
||||
existing thread when it covers your problem.
|
||||
2. Read [AGENTS.md](AGENTS.md) and the documentation for the component you want to change.
|
||||
3. Get maintainer agreement on scope before substantial features, refactors, architecture changes, or changes to
|
||||
defaults. Describe the user problem, proposed behavior, and tradeoffs in an issue first. Agreement on scope
|
||||
does not guarantee a merge. Focused fixes and benchmark reports can go straight to a PR.
|
||||
4. Keep one problem per PR. Update that PR after feedback; do not open competing attempts for the same change.
|
||||
|
||||
Do not resubmit a declined proposal under a different title or through another agent. If new evidence changes
|
||||
the situation, bring it to the original discussion and let a maintainer decide whether to reopen it.
|
||||
|
||||
## Report a problem people can investigate
|
||||
|
||||
Use the matching [issue form](https://github.com/Niko1221/Strata/issues/new/choose). A bug report should include:
|
||||
|
||||
- The smallest steps or command that reproduce the problem, and the expected and actual behavior.
|
||||
- Strata version or commit, OS, GPU and VRAM, and relevant model, quantization, and configuration.
|
||||
- The exact error and relevant logs, with credentials and private content removed.
|
||||
- Whether you reproduced it yourself. Label a finding based only on reading code or a scanner as
|
||||
**unconfirmed static analysis**, and explain the suspected path without presenting it as an observed failure.
|
||||
|
||||
Do not invent reproduction steps, errors, affected hardware, or severity. If you cannot reproduce something,
|
||||
say what you inspected and what would verify the hypothesis. Feature requests should explain a concrete user
|
||||
need and the current workaround. Questions should say what you tried and where you got stuck.
|
||||
|
||||
Example structure, with placeholders to replace with actual observations:
|
||||
|
||||
```text
|
||||
Problem: <one observed behavior>
|
||||
Environment: <commit, OS, GPU/VRAM, model/quant, relevant settings>
|
||||
Reproduce: <exact command or short sequence>
|
||||
Expected: <documented or intended behavior>
|
||||
Actual: <observed behavior and exact error>
|
||||
Evidence: <sanitized log attachment>
|
||||
Reproduction status: <reproduced locally OR unconfirmed static analysis>
|
||||
```
|
||||
|
||||
## Titles
|
||||
|
||||
Every issue and pull request title starts with its area in square brackets, then a short description: `[Engine]`, `[GUI]`, `[TUI]`,
|
||||
`[Benchmark]`, `[Docs]` or `[Other]`. For example `[Engine] fix decode stop on long prompts` or `[GUI] keep the model list after a tab
|
||||
switch`. The issue forms ask for the area and a check on the title fails when the prefix is missing.
|
||||
|
||||
## Prepare a focused pull request
|
||||
|
||||
Lead with the problem and resulting behavior. Explain why the approach is needed, the important limits, and
|
||||
any compatibility effects. Link the issue and scope agreement when applicable. A file-by-file narration or
|
||||
an AI-generated summary of the diff is not a substitute for this explanation.
|
||||
|
||||
- Keep unrelated formatting, cleanup, dependency changes, and speculative abstractions out of the PR.
|
||||
- Add documentation where it changes what a user or developer needs to know. Avoid repeated summaries,
|
||||
speculative roadmaps, and generated documentation with no actionable information.
|
||||
- Preserve existing behavior outside the intended change. New features are opt-in; follow the default-path
|
||||
and performance requirements in [AGENTS.md](AGENTS.md).
|
||||
- Read the complete diff, including generated files, before submitting. Remove debugging output, secrets,
|
||||
private paths, and accidental files.
|
||||
- Prefer a small relevant regression check over tests that only restate the implementation. Documentation-only
|
||||
changes need link, example, and consistency checks; they do not require GPU builds.
|
||||
|
||||
## Show the validation you actually performed
|
||||
|
||||
List exact commands, the revision and environment tested, and observed outcomes. Keep **passed**, **failed**,
|
||||
and **not run** separate. Include relevant output or a log attachment. A command you suggest running is not
|
||||
evidence that it passed; a CI run on a different revision is not validation of the submitted code.
|
||||
|
||||
Build every backend a code change touches, including CUDA, HIP, and SYCL where applicable, before requesting
|
||||
review. For changes that need hardware execution, report that execution separately from compilation. A mock,
|
||||
static check, or successful build does not establish hardware correctness. If you lack a required environment,
|
||||
keep the PR as a draft, identify the missing checks, and ask for targeted help in the same thread. Do not claim
|
||||
compatibility you have not tested.
|
||||
|
||||
Performance claims need before/after runs on the same hardware and workload, with every changed setting
|
||||
identified. Preserve the individual runs, failures, and variability. Follow the
|
||||
[community measurement guide](docs/COMMUNITY_BENCHMARKS.md); standard submissions will use the
|
||||
[benchmark protocol](docs/BENCHMARK_PROTOCOL.md) once it is activated. One fast run does not justify changing a
|
||||
default or claiming a speedup for other machines.
|
||||
|
||||
Example PR body; placeholders are not test results:
|
||||
|
||||
```text
|
||||
Problem and result: <trigger; behavior before and after the change>
|
||||
Related discussion: <issue URL and scope agreement, when required>
|
||||
Approach: <why this fix; relevant compatibility effects>
|
||||
Validation:
|
||||
Passed: <exact command, revision/environment, observed result, log>
|
||||
Failed: <exact command and failure, or none observed>
|
||||
Not run: <check, reason, and what remains unverified>
|
||||
Limits: <known limitations or remaining work>
|
||||
```
|
||||
|
||||
## Rules for AI-assisted work
|
||||
|
||||
Use agents to help investigate, implement, and explain work that you can review. Before submitting, verify
|
||||
their claims against the source, actual command output, or a cited primary reference.
|
||||
|
||||
Never submit fabricated measurements, invented test results, fake logs, or copied claims you have not checked.
|
||||
Do not turn a repository scan into a batch of speculative issues or PRs. Consolidate related findings, establish
|
||||
their effect, and submit the useful evidence. Do not present an agent's confidence or another model's approval
|
||||
as validation. Keep review replies concise and address the requested change or evidence directly.
|
||||
|
||||
Maintainers may request a smaller change, return a PR to draft, or close a submission that lacks evidence,
|
||||
duplicates existing work, or ignores agreed scope. A smaller, understandable contribution with honest limits
|
||||
is easier to review and maintain.
|
||||
|
||||
## Share a benchmark
|
||||
|
||||
A results-only PR should contain the human report, raw measurements, sanitized configuration, and supporting
|
||||
logs. Keep it separate from engine changes and include poor results and failed requests.
|
||||
|
||||
The [current community benchmark guide](docs/COMMUNITY_BENCHMARKS.md) remains available for measurements made
|
||||
with today's tools. The [new protocol](docs/BENCHMARK_PROTOCOL.md) defines the draft standard and future approval
|
||||
workflow. Its draft status must remain visible: existing reports are not automatically standard or approved.
|
||||
@@ -1,119 +1,103 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Strata, built FROM SOURCE (NVIDIA CUDA 13). The engine is compiled here, packaged as an engine component package and then
|
||||
# installed into the same runtime image as the ready-made ones (docker/Dockerfile): same Terminal, same entrypoint, same /data volume.
|
||||
#
|
||||
# Strata: Qwen3.8-Flash-Next on NVIDIA GPUs (RTX 30/40/50, 12+ GB VRAM; two or
|
||||
# three cards can share one model, 8 GB each - docs/MULTI_GPU.md).
|
||||
# Most people want the ready-made images instead (nothing compiled): docs/DOCKER.md.
|
||||
#
|
||||
# The engine is compiled during docker build, so the first container start only
|
||||
# downloads the model (~70 GB) and starts the server. docker build has no GPU,
|
||||
# so the CUDA architectures are fixed here instead of read from nvidia-smi: the
|
||||
# engine is a fat binary with a cubin per listed arch, and the runtime picks the
|
||||
# one matching your card. Narrow CUDA_ARCHITECTURES to your card for a faster
|
||||
# build; a card outside the set needs a rebuild with its own arch.
|
||||
# docker build -t strata:src . # RTX 20/30/40/50 (compiles for each, ~25 min)
|
||||
# docker build -t strata:src --build-arg CUDA_ARCHITECTURES=86 . # one card only (faster); 86 = RTX 30, 89 = RTX 40, 120 = RTX 50
|
||||
# docker run --rm --gpus all -p 127.0.0.1:8080:8080 --ulimit memlock=-1 --shm-size 2g --stop-timeout 120 \
|
||||
# -v strata-data:/data -e MODEL=IQ3_XXS -e API_KEY=change-me-please-123 strata:src
|
||||
#
|
||||
# Build:
|
||||
# docker build -t strata .
|
||||
# docker build -t strata --build-arg CUDA_ARCHITECTURES=89 . # RTX 40 only
|
||||
# docker build has no GPU, so the CUDA architectures are given here instead of read from nvidia-smi. The engine is a fat binary with
|
||||
# a machine-code part per listed architecture (plus PTX for the newest, which later cards can JIT), and the runtime picks the one
|
||||
# matching your card. Narrow CUDA_ARCHITECTURES to your card for a faster build. Native code for the CPU that builds the image is NOT
|
||||
# used (STRATA_PORTABLE=ON), so the image runs on any x86-64 CPU with AVX2.
|
||||
#
|
||||
# Run (host needs an NVIDIA driver >= 580 and nvidia-container-toolkit):
|
||||
# docker run --rm --gpus all \
|
||||
# -p 8080:8080 \
|
||||
# --ulimit memlock=-1 \
|
||||
# -v strata-data:/data \
|
||||
# -e MODEL=IQ2_XS \
|
||||
# strata
|
||||
#
|
||||
# Setup choices are env vars, read by docker-entrypoint.sh: FAMILY, MODEL, CONTEXT,
|
||||
# VISION (no | yes | cpu), KV (int8 | q4_0 | k8v4), GPU (one card) or GPUS ("0,2"
|
||||
# or "all", with LAYER_SPLIT), LOW_RAM (auto | on | off), HOST, PORT, API_KEY,
|
||||
# GGUF_DIR (GGUF files you already have), RESIDENT_BUDGET_GIB, KV_STREAMING.
|
||||
#
|
||||
# Only the model files, the prepared pack, the MTP layer and the install config
|
||||
# live in the /data volume; the engine is part of the image. Strata loads 32-62 GB
|
||||
# into RAM, so a capped container needs -e LOW_RAM=on: setup.py reads the RAM from
|
||||
# /proc/meminfo, which here is the host's total, not the container's limit. Add an
|
||||
# API key before exposing the port to a network: -e API_KEY=<secret>. Pass
|
||||
# -e REINSTALL=1 to change the model settings later.
|
||||
#
|
||||
# --gpus all on a host with two usable cards: setup takes both (the layer split is
|
||||
# its recommended default). Pin one card with -e GPU=0, or name them with
|
||||
# -e GPUS=0,2. A volume set up for one card switches to the pair on its first start
|
||||
# on a two-card host unless GPU or GPUS pins it. LOW_RAM=on runs on one card.
|
||||
# Plain legacy-builder features only (no heredocs, no --mount). AMD (HIP) and Intel (SYCL) source builds: docs/AMD_HIP.md, docs/INTEL.md.
|
||||
|
||||
FROM nvidia/cuda:13.0.0-devel-ubuntu24.04
|
||||
|
||||
# STRATA_EXECV=1: setup.py replaces itself with the server, so the server is PID 1
|
||||
# and docker stop's SIGTERM reaches it (see setup.start). Normal Linux starts, which
|
||||
# don't set it, keep spawning the server as a child.
|
||||
ENV DEBIAN_FRONTEND=noninteractive PYTHONUNBUFFERED=1 LANG=C.UTF-8 STRATA_EXECV=1
|
||||
ARG CUDA_IMAGE=nvidia/cuda:13.0.0-devel-ubuntu24.04
|
||||
ARG RUNTIME_BASE=ubuntu:24.04
|
||||
|
||||
# ---- 1. compile the engine and make its component package
|
||||
FROM ${CUDA_IMAGE} AS engine-build
|
||||
ENV DEBIAN_FRONTEND=noninteractive PYTHONUNBUFFERED=1 PATH=/venv/bin:$PATH
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential ca-certificates curl git libatomic1 libgomp1 \
|
||||
python3 python3-pip python3-venv unzip \
|
||||
build-essential ca-certificates git libgomp1 libatomic1 python3 python3-pip python3-venv unzip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /opt/strata
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
# RTX 20 (75), RTX 30 (86), RTX 40 (89), RTX 50 (120), plus 80 for A-series. CMakeLists
|
||||
# refuses anything below 75. BUILD_VISION=0 skips the image encoder build.
|
||||
ARG CUDA_ARCHITECTURES=75;80;86;89;120
|
||||
ARG CUDA_ARCHITECTURES="75;80;86;89;120"
|
||||
ARG BUILD_VISION=1
|
||||
ARG ENGINE_VERSION=
|
||||
RUN python3 -m venv /venv && /venv/bin/pip install --no-cache-dir -r requirements.txt -r requirements-build.txt
|
||||
# llama.cpp at the pinned commit (ggml for the engine, mtmd for pictures), exactly as setup.py fetches it
|
||||
RUN /venv/bin/python -c "import setup; print(setup.get_llama_cpp())"
|
||||
RUN set -e; \
|
||||
real=""; for a in $(echo "$CUDA_ARCHITECTURES" | tr ';,' ' '); do real="$real;${a}-real"; done; real=${real#;}; \
|
||||
last=$(echo "$CUDA_ARCHITECTURES" | tr ';,' ' ' | awk '{print $NF}'); \
|
||||
/venv/bin/cmake -G Ninja -S . -B build -DCMAKE_BUILD_TYPE=Release -DSTRATA_ENABLE_CUDA=ON -DSTRATA_BUILD_TESTS=OFF \
|
||||
-DSTRATA_NATIVE_EXPERTS=ON -DSTRATA_PORTABLE=ON -DSTRATA_Q6K_EXPERTS=ON -DSTRATA_MMQ_KQUANTS=ON \
|
||||
"-DCMAKE_CUDA_ARCHITECTURES=${real};${last}-virtual" -DSTRATA_GGML_DIR=/src/third_party/llama.cpp; \
|
||||
/venv/bin/cmake --build build --target strata strata_kq -j "$(nproc)"; \
|
||||
if [ "$BUILD_VISION" = "1" ]; then \
|
||||
/venv/bin/cmake -G Ninja -S tools/vision -B build-vision -DCMAKE_BUILD_TYPE=Release -DLLAMA_DIR=/src/third_party/llama.cpp \
|
||||
-DSTRATA_VISION_CUDA=ON -DSTRATA_PORTABLE=ON -DGGML_CUDA_NCCL=OFF "-DCMAKE_CUDA_ARCHITECTURES=${real};${last}-virtual"; \
|
||||
/venv/bin/cmake --build build-vision --target strata-vision -j "$(nproc)"; \
|
||||
fi
|
||||
RUN set -e; \
|
||||
mkdir -p /stage /engine; \
|
||||
cp build/strata /stage/; \
|
||||
[ -f build/libstrata_kq.so ] && cp build/libstrata_kq.so /stage/ || true; \
|
||||
[ -f build-vision/bin/strata-vision ] && cp build-vision/bin/strata-vision /stage/ || true; \
|
||||
ver="${ENGINE_VERSION:-$(sed -n 's/^[[:space:]]*project([^)]*VERSION[[:space:]]*\([0-9][0-9.]*\).*/\1/p' CMakeLists.txt | head -1)}"; \
|
||||
echo "{\"version\": \"$ver\", \"source\": \"docker build\", \"archs\": \"$CUDA_ARCHITECTURES\", \"cuda\": \"13.0\", \"portable\": true}" > /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}'); \
|
||||
python3 packaging/package_engine.py --src /stage --backend cuda --version "$ver" --out /engine $archs --arch "compute_$last" \
|
||||
--min-driver nvidia=580.65 --variant cuda13 --platform linux-x86_64 --build-id "docker-source-${ver}-cuda13.0" --include SYSTEM-LIBS.txt --include 'libstrata_kq*' \
|
||||
--include strata-vision; \
|
||||
for d in /engine/*/; do python3 packaging/package_engine.py --verify "$d"; python3 packaging/binary_deps.py "$d"; done
|
||||
|
||||
RUN python3 -m venv .venv \
|
||||
&& .venv/bin/pip install --no-cache-dir --upgrade pip \
|
||||
&& .venv/bin/pip install --no-cache-dir -r requirements.txt \
|
||||
&& chmod +x setup.sh docker-entrypoint.sh
|
||||
|
||||
# llama.cpp at the pinned commit, then the engine and the image encoder, built
|
||||
# exactly the way setup.py builds them: native code for the CPU that builds the
|
||||
# image, so build it on the PC it runs on. BUILD.json is what setup.py reads to
|
||||
# decide whether an engine is current: source=local with a matching src hash
|
||||
# means the first start reuses it instead of recompiling.
|
||||
RUN .venv/bin/python - <<'PYEOF'
|
||||
import json, os, pathlib, shutil
|
||||
import setup
|
||||
|
||||
llama = setup.get_llama_cpp()
|
||||
nvcc, _ = setup.find_nvcc()
|
||||
arch = os.environ.get("CUDA_ARCHITECTURES", "75;80;86;89;120").strip().strip('"').replace(",", ";")
|
||||
vision = "gpu" if os.environ.get("BUILD_VISION", "1") == "1" else "none"
|
||||
|
||||
setup.cmake_build(setup.ROOT, setup.ROOT / "build", "strata",
|
||||
["-DSTRATA_ENABLE_CUDA=ON", "-DSTRATA_Q6K_EXPERTS=ON", "-DSTRATA_MMQ_KQUANTS=ON", "-DSTRATA_BUILD_TESTS=OFF",
|
||||
f"-DCMAKE_CUDA_ARCHITECTURES={arch}", f"-DCMAKE_CUDA_COMPILER={nvcc}",
|
||||
f"-DSTRATA_GGML_DIR={llama}"], None, "build-strata.bat")
|
||||
if vision != "none":
|
||||
setup.cmake_build(setup.ROOT / "tools" / "vision", setup.ROOT / "build-vision", "strata-vision",
|
||||
[f"-DLLAMA_DIR={llama}", "-DSTRATA_VISION_CUDA=ON", "-DSTRATA_PORTABLE=OFF",
|
||||
f"-DCMAKE_CUDA_ARCHITECTURES={arch}", f"-DCMAKE_CUDA_COMPILER={nvcc}"], None, "build-vision.bat")
|
||||
|
||||
eng = setup.ROOT / "engine"
|
||||
eng.mkdir(exist_ok=True)
|
||||
shutil.copy2(setup.ROOT / "build" / setup.EXE, eng / setup.EXE)
|
||||
if (setup.ROOT / "build" / setup.KQ_LIB).exists(): # the K-quant MMQ prompt library (UD-Q4/Q5/Q6_K_XL files)
|
||||
shutil.copy2(setup.ROOT / "build" / setup.KQ_LIB, eng / setup.KQ_LIB)
|
||||
if vision != "none":
|
||||
shutil.copy2(setup.ROOT / "build-vision" / "bin" / setup.VEXE, eng / setup.VEXE)
|
||||
bindir = pathlib.Path(nvcc).parent
|
||||
meta = {"source": "local", "version": setup.source_version(),
|
||||
"archs": [int(a.split("-")[0]) for a in arch.split(";") if a.split("-")[0].isdigit()], "vision": vision,
|
||||
"cuda_dirs": [str(d) for d in (bindir, bindir / "x64", bindir.parent / "lib64") if d.is_dir()],
|
||||
"src": setup.source_hash(setup.ENGINE_SOURCES),
|
||||
"vision_src": setup.source_hash(setup.VISION_SOURCES) if vision != "none" else None}
|
||||
(eng / "BUILD.json").write_text(json.dumps(meta, indent=1))
|
||||
PYEOF
|
||||
|
||||
# the cmake trees are build-time only; the engine itself is what the container needs
|
||||
RUN rm -rf build build-vision
|
||||
# ---- 2. the Terminal (the release archive, built here)
|
||||
FROM ${RUNTIME_BASE} AS terminal
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends python3 ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
COPY . /src/
|
||||
RUN python3 /src/packaging/package_terminal.py --out /out --cache /cache \
|
||||
&& mkdir -p /opt/strata \
|
||||
&& tar -xzf /out/strata-terminal-*-linux-x86_64.tar.gz -C /opt/strata \
|
||||
&& mv /opt/strata/strata-terminal-* /opt/strata/terminal \
|
||||
&& /opt/strata/terminal/bin/strata --version
|
||||
# llama.cpp at the commit setup.py pins: fetched here, so the first start does not depend on GitHub. Only gguf-py (the model preparation
|
||||
# tools) and the file setup.py looks for (ggml/CMakeLists.txt) are kept: 170 MB become a few
|
||||
RUN cd /opt/strata/terminal && STRATA_CONTROL_INSTALL=1 STRATA_INSTALL_DIR=/opt/strata/seed PYTHONNOUSERSITE=1 \
|
||||
/opt/strata/terminal/python/bin/python3 -c "import setup; print(setup.get_llama_cpp())" \
|
||||
&& rm -rf /opt/strata/seed/third_party/*.zip* \
|
||||
&& cd /opt/strata/seed/third_party/llama.cpp \
|
||||
&& find . -mindepth 1 -maxdepth 1 ! -name gguf-py ! -name ggml ! -name 'LICENSE*' -exec rm -rf {} + \
|
||||
&& find ggml -mindepth 1 -depth ! -type d ! -name CMakeLists.txt -delete && find ggml -mindepth 1 -type d -empty -delete \
|
||||
&& du -sh /opt/strata/seed
|
||||
|
||||
# ---- 3. the runtime image (the same as docker/Dockerfile's, FLAVOR=cuda)
|
||||
FROM ${RUNTIME_BASE}
|
||||
ARG STRATA_VERSION=dev
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends tini ca-certificates libgomp1 libatomic1 libnuma1 zlib1g \
|
||||
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/*
|
||||
COPY --from=terminal /opt/strata/terminal /opt/strata/terminal
|
||||
COPY --from=terminal /opt/strata/seed /opt/strata/seed
|
||||
COPY --from=engine-build /engine/ /opt/strata/components/engine/
|
||||
COPY docker/entrypoint.sh docker/healthcheck.sh /opt/strata/
|
||||
COPY docker/strata-wrapper.sh /usr/local/bin/strata
|
||||
RUN chmod 0755 /opt/strata/entrypoint.sh /opt/strata/healthcheck.sh /usr/local/bin/strata
|
||||
ENV STRATA_COMPONENTS_DIR=/opt/strata/components STRATA_INSTALL_DIR=/data/install XDG_STATE_HOME=/data/state \
|
||||
XDG_CONFIG_HOME=/data/config HOME=/data/home PORT=8080 LANG=C.UTF-8 PYTHONUNBUFFERED=1 PYTHONNOUSERSITE=1 STRATA_IN_DOCKER=1 \
|
||||
NVIDIA_VISIBLE_DEVICES=all NVIDIA_DRIVER_CAPABILITIES=compute,utility
|
||||
VOLUME ["/data"]
|
||||
EXPOSE 8080
|
||||
|
||||
# /health is answered before the API key gate, so it works with or without one.
|
||||
# The port only opens after the model loads (1-3 minutes, longer on a first run),
|
||||
# so the start period is generous: a too short one marks a still-loading container
|
||||
# unhealthy and a restart policy would kill it mid-download.
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=600s --retries=3 \
|
||||
CMD curl -fs "http://127.0.0.1:${PORT:-8080}/health" || exit 1
|
||||
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
LABEL org.opencontainers.image.title="Strata" com.strata.flavor="cuda" com.strata.build="from-source" org.opencontainers.image.version="${STRATA_VERSION}"
|
||||
HEALTHCHECK --interval=30s --timeout=8s --start-period=2h --retries=3 CMD ["/opt/strata/healthcheck.sh"]
|
||||
STOPSIGNAL SIGTERM
|
||||
ENTRYPOINT ["/usr/bin/tini", "--", "/opt/strata/entrypoint.sh"]
|
||||
CMD ["run"]
|
||||
@@ -1,239 +1,404 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · **Deutsch** · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md)
|
||||
<p align="center"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <b>Deutsch</b> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<p align="center"><b>Ein KI-Modell mit 125 Milliarden Parametern auf deinem eigenen Gaming-PC</b><br>
|
||||
NVIDIA- oder AMD-Grafikkarte (ab 12 GB) · Windows oder Linux · kostenlos und Open Source</p>
|
||||
<h3 align="center">Ein Spitzenmodell auf deinem eigenen Gaming-PC</h3>
|
||||
|
||||
<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 in Strata geschrieben hat, läuft im Browser"></a><br>
|
||||
<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>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<sub>Ein Voxel-Pagodengarten aus einem einzigen Prompt, auf einer RTX 5070 mit Strata (IQ3_S, 128K Kontext) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">ganzes Video (49 s)</a></sub></p>
|
||||
|
||||
Strata lässt **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** auf einem normalen PC laufen.
|
||||
Das ist ein großes, kluges KI-Modell, das sonst meist einen Server braucht. Es chattet, schreibt Code, versteht
|
||||
Bilder und arbeitet mit deinen Apps und Coding-Agents zusammen. Nichts verlässt deinen PC.
|
||||
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.
|
||||
|
||||
## Wie schnell ist es?
|
||||
**Neu hier?** Sieh dir an, [was du brauchst](#was-du-brauchst), und [installiere dann die App](#option-a-die-strata-app).
|
||||
|
||||
Wir haben es auf zwei ganz normalen Gaming-PCs gemessen. Ein Token ist etwa ¾ eines Wortes.
|
||||
- **Läuft auf einem Gaming-PC.** 32 GB Arbeitsspeicher (RAM) und Grafikspeicher zusammen reichen für den Anfang.
|
||||
- **Passt zu deinen Werkzeugen.** Strata spricht die OpenAI- und die Anthropic-API, deshalb verbinden sich Claude Code, Codex CLI, OpenCode und andere Agents damit.
|
||||
- **Liest Bilder.** Das ist standardmäßig eingeschaltet.
|
||||
- **Kostenlos.** MIT-Lizenz, kein Konto, keine Cloud.
|
||||
- **Privat.** Keine Telemetrie. Was Strata herunterlädt, wann es das Internet nutzt und wie es Downloads prüft, steht in [docs/SAFETY.md](docs/SAFETY.md).
|
||||
|
||||
- **Schreibt Antworten:** wie schnell die Antwort in einem kurzen Chat erscheint. 60 Tokens pro Sekunde sind
|
||||
schneller, als du lesen kannst.
|
||||
- **Liest deinen Prompt:** wie schnell es aufnimmt, was du schickst (hier ein Dokument, Code oder Chatverlauf mit
|
||||
32K Tokens).
|
||||
|
||||
<table>
|
||||
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM</th></tr>
|
||||
<tr><td>
|
||||
|
||||
| Größe | Schreibt Antworten | Liest deinen Prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 94 Tokens/s | 2,650 Tokens/s |
|
||||
| **IQ2_XS** | 79 Tokens/s | 2,090 Tokens/s |
|
||||
| **IQ3_XXS** | 62 Tokens/s | 1,750 Tokens/s |
|
||||
| **IQ3_S** | 53 Tokens/s | 1,620 Tokens/s |
|
||||
| **Coder** | 55 Tokens/s | 2,180 Tokens/s |
|
||||
|
||||
</td><td>
|
||||
|
||||
| Größe | Schreibt Antworten | Liest deinen Prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 60 Tokens/s | 1,160 Tokens/s |
|
||||
| **IQ2_XS** | 52 Tokens/s | 1,110 Tokens/s |
|
||||
| **Coder** | 44 Tokens/s | 1,420 Tokens/s |
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
NVIDIA: Q2_0 mit Engine 0.1.36, die anderen Zeilen mit 0.1.26 (4K Antworten, 32K Prompts). Die vollständigen
|
||||
Tabellen stehen in [DETAILS.md](docs/DETAILS.md#speed-measured). Eine Karte mit mehr VRAM ist schneller: Eine
|
||||
RTX 3090 (24 GB) sollte etwa 100-140 Tokens pro Sekunde schreiben. Lange Chats und andere Karten:
|
||||
[Tempo jedes Modells](docs/MODELS.md#how-fast-is-each-size), [Ergebnisse aus der Community](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
|
||||
<sub>Strata ist kostenlos. Wenn es auf deinem PC gut läuft, hält ein Kaffee die Arbeit daran am Laufen.</sub></p>
|
||||
Version 0.2.1 · Engine 0.1.46
|
||||
|
||||
## 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.
|
||||
|
||||
- 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 |
|
||||
| --- | --- |
|
||||
| **Grafikkarte** | **NVIDIA** GeForce RTX 20, 30, 40 oder 50 Serie, oder **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 oder RX 6800 / 6900 Serie. Sie braucht **12 GB VRAM oder mehr**. |
|
||||
| **RAM** | 32 GB oder mehr. Dein RAM entscheidet, [welches Modell](#welches-modell-soll-ich-nehmen) passt. Mit 64 GB laufen alle Größen. |
|
||||
| **Festplatte** | Etwa 80 GB frei. Nimm wenn möglich eine SSD: Der erste Start geht dann viel schneller. |
|
||||
| **System** | Windows 10 / 11 oder Linux und ein aktueller Grafiktreiber von NVIDIA oder AMD. |
|
||||
| **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) |
|
||||
|
||||
Alles andere richtet der Installer ein. Zwei oder drei Karten können sich das Modell teilen ([Multi-GPU](docs/MULTI_GPU.md)).
|
||||
Außerdem nötig:
|
||||
|
||||
Experimentell, von Community-Mitgliedern auf ihren eigenen Rechnern geschrieben und getestet:
|
||||
- etwa 80 GB freier Speicherplatz (mit einer SSD startet es beim ersten Mal viel schneller)
|
||||
- Windows 10 / 11, Linux oder macOS
|
||||
- ein aktueller Grafiktreiber
|
||||
|
||||
- **Ältere Grafikkarten** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Ältere GPUs](docs/OLDER_GPUS.md).
|
||||
- **Intel Arc**, unter Linux aus dem Quellcode gebaut: [Intel Arc](docs/INTEL_ARC.md).
|
||||
- **AMD Ryzen AI Max (Strix Halo)**, unter Linux aus dem Quellcode gebaut: [Strix Halo](docs/STRIX_HALO.md).
|
||||
- **Ältere Prozessoren ohne AVX2**: Sie funktionieren, aber langsam. [Ältere CPUs](docs/INSTALL.md#older-cpus-experimental).
|
||||
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).
|
||||
|
||||
Die vollständige Liste: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
## Installation
|
||||
|
||||
## Installieren
|
||||
Such dir eine Option aus. Alle starten dieselbe Engine und nutzen dieselben Modelle.
|
||||
|
||||
### Lass deine KI es einrichten
|
||||
### Option A: die Strata-App
|
||||
|
||||
Nutzt du einen KI-Coding-Assistenten (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Füge dort diesen Text ein:
|
||||
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.
|
||||
|
||||
| System | Deine Grafikkarte | Datei zum Herunterladen |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, Treiber 580 oder neuer | [`Strata-0.2.1-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda13.exe) |
|
||||
| | NVIDIA, ältere Karte oder Treiber älter als 580 | [`Strata-0.2.1-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda12.exe) |
|
||||
| | AMD Radeon RX 9000 Serie oder Radeon AI PRO R9700 | [`Strata-0.2.1-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip.exe) |
|
||||
| | AMD Radeon RX 7000 Serie oder Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, Treiber 580 oder neuer | [`Strata-0.2.1-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, ältere Karte oder Treiber älter als 580 | [`Strata-0.2.1-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon RX 9000 Serie oder Radeon AI PRO R9700 | [`Strata-0.2.1-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon RX 7000 Serie | [`Strata-0.2.1-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 oder neuer) | eingebaut | [`Strata-0.2.1-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-mac.dmg) |
|
||||
|
||||
**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).
|
||||
|
||||
Danach:
|
||||
|
||||
- **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".
|
||||
|
||||
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.
|
||||
|
||||
### Option B: das Terminal (eine Zeile)
|
||||
|
||||
Das Terminal ist der Befehl `strata`, gedacht für Skripte, Server und KI-Agents.
|
||||
|
||||
Linux und Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
Das Skript prüft deinen PC, lädt das Terminal herunter, kontrolliert die SHA-256-Prüfsumme und entpackt es in deinen
|
||||
Home-Ordner. Es braucht keine Administratorrechte. Öffne unter Windows danach ein neues Terminalfenster. Dann startest du
|
||||
Strata:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
Es stellt dir eine Frage nach der anderen: Modell, Kontext, Bilder, wer sich verbinden darf. Danach lädt es die Engine für
|
||||
deine Grafikkarte herunter (signiert und vor der Nutzung probeweise gestartet), dazu das Modell, und beginnt zu laufen.
|
||||
Für Skripte und KI-Agents gibt es Flags:
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
Wenn du das Installationsskript vor dem Ausführen lesen willst, lade es zuerst mit `curl -fsSL <address> -o install.sh`
|
||||
herunter, lies es und führe dann `sh install.sh` aus. Alle Bildschirme und Flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### Option C: deine KI richtet es ein
|
||||
|
||||
Füge das in Claude Code, Cursor, Codex, GitHub Copilot oder einen anderen Coding-Assistenten ein:
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
Er prüft deine Grafikkarte, deinen RAM und deine Festplatte und wählt das passende Modell. Dann installiert und
|
||||
startet er es und sagt dir, wie du deine Apps verbindest. KI-Tools können Strata auch über seinen
|
||||
Der Assistent prüft deine Grafikkarte, deinen Arbeitsspeicher und den Speicherplatz, wählt das passende Modell, installiert
|
||||
und startet es und sagt dir, wie du deine Apps verbindest. KI-Werkzeuge können Strata auch über den
|
||||
[MCP-Server](docs/MCP_SERVER.md) installieren, starten und stoppen.
|
||||
|
||||
### Oder mach es selbst
|
||||
Du willst es lieber selbst bauen? Siehe [Aus dem Quellcode bauen](#aus-dem-quellcode-bauen).
|
||||
|
||||
[Lade Strata herunter](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) und entpacke es (oder nutze
|
||||
`git clone`). **Windows:** Doppelklick auf **`START-HERE.bat`**. **Linux:** Führe im Strata-Ordner **`./setup.sh`** aus.
|
||||
## Modelle
|
||||
|
||||
**Mac (Apple Silicon, M1 bis M5, macOS 15 oder neuer):** Installiere einmal Apples Command Line Tools (`xcode-select --install` im Terminal; Xcode selbst brauchst du nicht). Öffne dann das Terminal im Strata-Ordner und führe **`./setup.sh`** aus. Es baut die Mac-Engine auf deinem Mac (etwa 7 Minuten), stellt dieselben Fragen, lädt das Modell und startet es. Beim nächsten Mal startet `./setup.sh` sofort. Welche Größe zu deinem Mac passt und wie schnell es ist: [docs/MAC.md](docs/MAC.md).
|
||||
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 Schritte sind für NVIDIA und AMD gleich. Der Installer erkennt deine Karte und richtet die passende Engine
|
||||
dafür ein. Er stellt dir ein paar Fragen:
|
||||
### Wenig Speicher? Probier Gyro
|
||||
|
||||
- welches Modell und welche Größe,
|
||||
- wie viel Kontext (wie viel Text sich das Modell merkt),
|
||||
- ob es Bilder lesen soll.
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (von agentionai) ist die kleinste Möglichkeit, dieses Modell auszuführen.
|
||||
|
||||
Drück jedes Mal Enter, um die empfohlene Antwort zu nehmen. Dann lädt er das Modell herunter (etwa 70 GB) und
|
||||
startet es. Bricht der Download ab, starte ihn einfach neu: Er macht dort weiter, wo er aufgehört hat. Dein Browser
|
||||
öffnet die Strata-App unter `http://127.0.0.1:8080`.
|
||||
| Größe | Benötigter Speicher (RAM + VRAM) | Download | Was es ist |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27,6 GB | 58,5 GB | Für PCs, auf denen nichts anderes läuft. Die Qualität ist geringer als bei IQ2_XS. |
|
||||
| **Gyro-M** | 35 GB | 92,0 GB | Ungefähr die Qualität von IQ3_XXS bei etwa 20 % weniger GPU-Speicher. Es schreibt Antworten auf einer 12-GB-Karte und auf einer R9700 17-21 % langsamer als Q2_0. |
|
||||
|
||||
> **Während das Modell startet, kann dein PC 1-3 Minuten langsam sein oder nicht reagieren** (beim ersten Mal am
|
||||
> längsten). Strata lädt 35-55 GB in deinen RAM und reserviert einen Teil davon für die Grafikkarte. Das ist normal.
|
||||
> Warte und schließ das Fenster nicht. Im Fenster siehst du, was Strata gerade tut.
|
||||
Gyro läuft auf NVIDIA- und AMD-Karten (bei AMD experimentell), nicht auf Intel Arc und nicht auf dem Mac. Details und Messwerte:
|
||||
[docs/GYRO.md](docs/GYRO.md). Starte es mit `strata launch --model gyro-s`.
|
||||
|
||||
**Beim nächsten Mal** startest du wieder `START-HERE.bat` (oder `./setup.sh`). Es startet sofort und lädt nichts
|
||||
doppelt herunter. Schließ das Fenster, um das Modell zu stoppen. `UPDATE.bat` (`./update.sh`) aktualisiert Strata,
|
||||
ohne es zu starten. Updates, Docker, mehrere Karten, wo die Dateien landen und alle Optionen:
|
||||
[docs/INSTALL.md](docs/INSTALL.md).
|
||||
### Die anderen Größen
|
||||
|
||||
## Welches Modell soll ich nehmen?
|
||||
|
||||
Der Installer empfiehlt eins passend zu deinem RAM. Dasselbe Modell gibt es in mehreren Größen, mehr oder weniger
|
||||
stark komprimiert. Kleinere Größen sind schneller. Größere sind etwas klüger.
|
||||
|
||||
| Dein RAM | Nimm | Warum |
|
||||
| Dein Speicher (RAM + VRAM) | Nimm | Hinweise |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Coder** | passt in 32 GB und ist für Code gemacht (mit einer 24-GB-Karte laufen auch Q2_0 und IQ2_XS) |
|
||||
| **48 GB** | **IQ2_XS** (oder Q2_0, das schnellste) | die größeren Größen passen nicht |
|
||||
| **64 GB** | **IQ2_XS** (empfohlen) oder IQ3_XXS / IQ3_S | alle Größen passen; IQ3_S ist das beste und das langsamste |
|
||||
| **96 GB oder mehr** | **IQ3_S** oder Unsloths UD-IQ4_XS (~4 Bit) | Platz für die größten Größen, auch wenn alles andere offen ist |
|
||||
| **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 17-21 % langsamer als Q2_0 |
|
||||
| **48 GB** | **IQ3_XXS** (47,0 GB) | Langsamer, besser |
|
||||
| **64 GB** | **IQ3_S** (54,8 GB), das Original oder Swift 1.5 | Am langsamsten, am besten. Es erreicht in den veröffentlichten Tests das volle Modell |
|
||||
| **96 GB oder mehr** | **UD-IQ4_XS** von Unsloth (etwa 72 GB RAM) bis **Q8_0** (etwa 136 GB), 4 bis 8 Bit | Näher am vollen Modell. Bei weniger RAM liest Strata einen Teil von der SSD, das ist viel langsamer |
|
||||
|
||||
- **[Coder](docs/MODELS.md#coder):** eine Version für Code, bei der die Hälfte der Experten entfernt wurde. Sie
|
||||
erreicht 91 % des SWE-bench-Verified-Werts des vollen Modells (von ihren Autoren gemessen) und passt in 32 GB RAM.
|
||||
Außerhalb von Code ist sie schwächer, auch bei chinesischem und anderem CJK-Text (#438). Nimm dafür Q2_0, IQ2_XS
|
||||
oder IQ3_S, die alle Experten behalten.
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15):** ein Fine-Tune, das viel kürzer nachdenkt, bevor es antwortet. Du
|
||||
bekommst die Antwort früher, bei etwa gleicher Qualität.
|
||||
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** die ~4-Bit-Version von Unsloth, in der Qualität
|
||||
zwischen IQ3_S und UD-Q4_K_XL. 94 GB Download. Mit weniger als ~80 GB RAM liest Strata einen Teil davon während
|
||||
der Antwort von der SSD, dort ist es also langsamer (eine NVMe-SSD hilft).
|
||||
- **[Unsloth UD-Q4_K_XL, UD-Q5_K_XL, UD-Q6_K_XL und Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):** 4 bis 8 Bit, näher am vollen Modell
|
||||
(93-95 % das gleiche nächste Token wie Q8_0). Schnell, wenn alle Experten in den RAM passen (~88 GB für UD-Q4_K_XL
|
||||
bis ~136 GB für Q8_0); mit weniger RAM liest Strata einen Teil davon während der Antwort von der SSD, was viel
|
||||
langsamer ist (7-8.5 Tokens/s für UD-Q4_K_XL auf einem PC mit 64 GB).
|
||||
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** Das richtest du von Hand
|
||||
ein. Es steht nicht im Menü des Installers.
|
||||
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.
|
||||
|
||||
Größen, Downloads und was wohin passt: [docs/MODELS.md](docs/MODELS.md). Um später ein weiteres Modell
|
||||
hinzuzufügen, starte `SETUP.bat` (Linux: `./setup.sh --setup`).
|
||||
Weitere Auswahl:
|
||||
|
||||
## So benutzt du es
|
||||
- **[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.
|
||||
|
||||
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="Der Monitor-Tab der Strata-App neben einem Coding-Agent"><br>
|
||||
<sub>Der <b>Monitor</b> der Strata-App (links), während ein Coding-Agent den Pagodengarten aus dem Video schreibt (rechts)</sub></p>
|
||||
Alle Größen, Downloads und was wo läuft: [docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
- **Im Browser:** Öffne `http://127.0.0.1:8080`. Dort gibt es **Chat**, einen Live-**Monitor** für das Modell und
|
||||
deine GPU/CPU/RAM und **About** mit den Einstellungen und Adressen.
|
||||
- **Deine Apps und Coding-Agents:** Füge einen „OpenAI-kompatiblen“ Anbieter mit der Basis-URL
|
||||
**`http://127.0.0.1:8080/v1`** hinzu. Jeder API-Key und jeder Modellname funktioniert.
|
||||
- Apps, die die API von Anthropic nutzen: `http://127.0.0.1:8080/v1/messages` (Claude Code:
|
||||
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
|
||||
- Codex CLI und andere Apps, die die OpenAI Responses API nutzen: `/v1/responses`
|
||||
([Einrichtung](docs/DETAILS.md#the-responses-api-and-codex-cli)).
|
||||
- **Nachdenken:** Wähle **aus, niedrig, mittel oder hoch** im Chat-Menü oder unter „Reasoning Effort“ in deiner
|
||||
App. Aus ist am schnellsten. Hoch ist am besten für schwierige Fragen.
|
||||
- **Bilder:** Antworte im Setup mit Ja auf „Images?“. Klick dann im Chat auf **Picture** oder hänge Bilder in deiner
|
||||
App an. AMD-Karten lesen Bilder unter Linux über den Prozessor; unter Windows geht das noch nicht.
|
||||
- **Vom Handy oder einem anderen PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Setz immer einen
|
||||
Key.
|
||||
- **Eine Anfrage nach der anderen:** Standardmäßig beantwortet Strata eine Anfrage, die anderen warten. Um mehrere
|
||||
gleichzeitig zu beantworten, setz `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Auf einer 12-GB-Karte wird
|
||||
dadurch jede Antwort langsamer.
|
||||
- **Lange Prompts:** Strata liest die erste Nachricht eines Chats komplett, etwa 1 Minute pro 30,000 Tokens.
|
||||
Folgenachrichten starten in Sekunden.
|
||||
## Wie schnell ist es?
|
||||
|
||||
Mehr: [wo deine Chats gespeichert werden](docs/INSTALL.md#where-things-are-stored), [die API](docs/DETAILS.md#using-it).
|
||||
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).
|
||||
|
||||
### Optional: mehr Tempo mit einer Grafikkarte, wenn das ganze Modell in den RAM passt
|
||||
| Modell | Antworten erscheinen mit | Liest deinen Prompt mit |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████████` 94 Tokens/s | `████████████` 2.650 Tokens/s |
|
||||
| **IQ2_XS** | `██████████░░` 79 Tokens/s | `█████████░░░` 2.090 Tokens/s |
|
||||
| **IQ3_XXS** | `████████░░░░` 62 Tokens/s | `████████░░░░` 1.750 Tokens/s |
|
||||
| **IQ3_S** | `███████░░░░░` 53 Tokens/s | `███████░░░░░` 1.620 Tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 Tokens/s | `██████████░░` 2.180 Tokens/s |
|
||||
|
||||
Zwei Opt-in-Einstellungen (von RubenTorney, #1904 und #1905). Sie helfen, wenn du eine Grafikkarte nutzt und dein RAM alle Experten hält (Stratas Arena-Modus, zum Beispiel Q2_0 oder IQ3_XXS auf einem PC mit 64 GB). Öffne deine Konfiguration `strata-<model>.json` im Strata-Datenordner ([wo](docs/INSTALL.md#where-things-are-stored)) und füge hinzu:
|
||||
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.
|
||||
|
||||
- **Schnellere Ausgabe:** `"--adapt-async", "1"` in die Liste `"args"`. Der Experten-Cache wird dann in einem Hilfs-Thread aktualisiert, statt die Antwort anzuhalten. Gemessen auf einer RTX 5070 (Windows, Q2_0): +4-5 % Ausgabe, +7-10 % bei Code und langen Dokumenten. Antworten können leicht vom Standard abweichen (sie bleiben von Lauf zu Lauf gleich).
|
||||
- **Schnelleres Prompt-Lesen unter Windows:** `"STRATA_PIN_BY_NEED": "1"` im Block `"env"`. Windows kann nur etwa den halben RAM für die Grafikkarte sperren; damit werden zuerst die Experten gesperrt, die die Karte wirklich braucht. Gemessen auf der RTX 5070: +4,4 % Prompt-Lesen.
|
||||
Gemessen mit einer AMD-Karte mit 16 GB (RX 9070 XT, Ryzen 9 3900X, 47 GB RAM):
|
||||
|
||||
| Modell | Antworten erscheinen mit | Liest deinen Prompt mit |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████░░░░` 60 Tokens/s | `█████░░░░░░░` 1.160 Tokens/s |
|
||||
| **IQ2_XS** | `███████░░░░░` 52 Tokens/s | `█████░░░░░░░` 1.110 Tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 Tokens/s | `██████░░░░░░` 1.420 Tokens/s |
|
||||
|
||||
Die Balken haben in beiden Tabellen denselben Maßstab. Eine Karte mit mehr VRAM ist schneller. Eine RTX 3090 (24 GB) sollte
|
||||
etwa 100-140 Tokens pro Sekunde schreiben. Die Engine-Version hinter jeder Zahl steht in [DETAILS.md](docs/DETAILS.md#speed-measured).
|
||||
|
||||
## Benutzung
|
||||
|
||||
### In der App
|
||||
|
||||
Die App hat einen **Chat**, einen **Monitor** für das Modell sowie für GPU, CPU und RAM, eine Seite **Modelle** und die
|
||||
**Einstellungen**. Der Chat kann in einem Ordner auf deinem PC arbeiten. Im Modus **Manuell** fragt er nach, bevor er eine
|
||||
Datei ändert oder einen Befehl ausführt. Mit `/loop 10m <what to do>` wiederholst du eine Nachricht alle 10 Minuten, solange
|
||||
der Chat geöffnet ist.
|
||||
|
||||
Der Chat kann das Web mit DuckDuckGo (oder Bing) durchsuchen, ohne Konto. Unter **Einstellungen, Tools** kannst du einen
|
||||
Suchschlüssel hinzufügen. Die Suche ist die einzige Funktion, die etwas von deinem PC sendet (die Suchbegriffe).
|
||||
|
||||
Um zwei Modelle auszuführen, schalte **Mehrere Modelle ausführen** ein und gib jedem Modell eine eigene Karte desselben
|
||||
Herstellers.
|
||||
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
|
||||
<sub>Ein Chat in der Strata-App: Der Agent hat einen kleinen Voxel-Pagodengarten als eine HTML-Datei geschrieben, und die Seite öffnet sich in der Vorschau neben dem Gespräch</sub></p>
|
||||
|
||||
### Im Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<sub><code>strata monitor</code>, während das Modell antwortet: Geschwindigkeit, GPU, Speicher und die letzten Anfragen</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided screens: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
|
||||
strata launch --plan --model q2_0 # show what would happen; nothing changes
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
|
||||
strata update --check # is there a new engine? (strata update installs it)
|
||||
strata doctor # check the installed engine files
|
||||
```
|
||||
|
||||
`strata --help` listet alles auf. Starten und Bereitstellen laden nichts herunter. Nur die Installation, `strata launch`
|
||||
(wenn du ein Modell wählst) und `strata update` tun das. Um Strata von deinem Handy oder einem anderen PC aus zu erreichen,
|
||||
nutze `strata launch --access lan`. Der Befehl erzeugt einen API-Schlüssel und startet in einem Netzwerk nicht ohne einen.
|
||||
Ein Browser öffnet sich nur, wenn du `--open` angibst. Anleitung: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Der ganze Client: [docs/TERMINAL.md](docs/TERMINAL.md).
|
||||
|
||||
### Mit KI-Agents
|
||||
|
||||
Ein Agent installiert Strata mit dem Einzeiler aus Option B und führt
|
||||
`strata launch --model NAME --yes --no-tui` aus. Die Schritte, Flags und Exit-Codes stehen in [docs/AI_SETUP.md](docs/AI_SETUP.md),
|
||||
die Regeln für das Repository in [AGENTS.md](AGENTS.md).
|
||||
|
||||
### Andere Apps verbinden
|
||||
|
||||
| App-Typ | Basis-URL |
|
||||
| --- | --- |
|
||||
| OpenAI-kompatibel (die meisten Apps und Agents) | `http://127.0.0.1:8080/v1` (beliebiger API-Schlüssel, beliebiger Modellname) |
|
||||
| Anthropic-API (Claude Code und andere) | `http://127.0.0.1:8080`, ohne `/v1`. Diese Apps hängen `/v1/messages` selbst an. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([Einrichtung](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` gibt die genauen Einstellungen für OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent und OpenClaw aus.
|
||||
`strata connect opencode --write` schreibt die Datei nach einer Rückfrage und legt eine Sicherung an.
|
||||
|
||||
### Bilder
|
||||
|
||||
Bilder sind für Modelle, die sie lesen können, standardmäßig eingeschaltet. Hänge ein Bild im Chat an oder füge es ein, oder
|
||||
sende es aus deiner App. Der Bildleser läuft auf dem Prozessor, wenn deine Grafikkarte weniger als 24 GB hat (einige Sekunden
|
||||
pro Bild, ohne Speicher der Karte zu belegen), und ab 24 GB auf der Grafikkarte. Die Regel gilt für NVIDIA und AMD gleich.
|
||||
Du kannst Bilder pro Modell ausschalten oder den Leser mit `--vision off`, `--vision gpu` oder `--vision cpu` wählen.
|
||||
|
||||
Die Zahlen hinter der Regel: Auf einer Radeon AI PRO R9700 brauchte der Bildleser der Karte 57 ms für ein Bild mit 260 Tokens,
|
||||
der des Prozessors 3,1 s. Auf Karten mit 16 GB (Tesla P100 und RTX A4000, Linux) kostete der Bildleser der Karte 3-5 % des
|
||||
Texttempos, weil er etwa 1,4 GB belegt, die sonst das Modell nutzen würde. Auf dem Mac liest die GPU, egal wie viel Speicher er hat (0,45 s für ein Bild mit 260 Tokens auf einem M4 mit 32 GB, etwa 1,2 GB des gemeinsamen
|
||||
Speichers); der Prozessor braucht dort etwa 50 s pro Bild und liest deshalb nur mit `--vision cpu`.
|
||||
|
||||
### Gut zu wissen
|
||||
|
||||
- Das Denkniveau (**aus, niedrig, mittel oder hoch**) stellst du im Chat-Menü oder unter "reasoning effort" in deiner App ein. Aus ist am schnellsten.
|
||||
- Strata beantwortet standardmäßig eine Anfrage nach der anderen. Für mehrere gleichzeitig nutze `--parallel N` in `strata launch` oder
|
||||
`"parallel": 2` in der Konfiguration ([BATCHING.md](docs/BATCHING.md)). Auf einer Karte mit 12 GB wird dadurch jede Antwort langsamer.
|
||||
- Strata liest die erste Nachricht eines Chats komplett, etwa 1 Minute pro 30.000 Tokens. Folgenachrichten starten in Sekunden.
|
||||
- Wo deine Chats gespeichert werden und die ganze API: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
|
||||
|
||||
## Aus dem Quellcode bauen
|
||||
|
||||
<details>
|
||||
<summary>Windows, Linux und Mac aus dem Repository</summary>
|
||||
|
||||
[Lade Strata herunter](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) und entpacke es, oder nutze `git clone`.
|
||||
**Windows:** Doppelklick auf `START-HERE.bat`. **Linux:** Führe `./setup.sh` im Strata-Ordner aus.
|
||||
|
||||
**Mac (Apple Silicon, macOS 15 oder neuer):** Installiere einmal die Command Line Tools von Apple mit `xcode-select --install`
|
||||
(Xcode selbst brauchst du nicht). Öffne das Terminal im Strata-Ordner und führe `./setup.sh` aus. Das Skript baut die
|
||||
Mac-Engine auf deinem Mac (etwa 7 Minuten), stellt dieselben Fragen, lädt das Modell herunter und startet es. Beim nächsten
|
||||
Mal startet `./setup.sh` sofort. Welche Größe zu deinem Mac passt: [docs/MAC.md](docs/MAC.md).
|
||||
|
||||
Der Installer erkennt deine Karte und richtet die passende Engine ein. Er fragt, welches Modell und welche Größe du willst,
|
||||
wie viel Kontext (wie viel Text sich das Modell merkt) und ob es Bilder lesen soll. Drück Enter für die empfohlene Antwort.
|
||||
Dann lädt er das Modell herunter (etwa 70 GB) und startet es. Wenn der Download abbricht, führe das Skript erneut aus, dann
|
||||
geht es weiter. Dein Browser öffnet die Strata-Seite unter `http://127.0.0.1:8080`.
|
||||
|
||||
> Während das Modell startet, kann dein PC 1-3 Minuten langsam sein oder nicht reagieren (beim ersten Mal am längsten). Strata
|
||||
> lädt 35-55 GB in deinen RAM und sperrt einen Teil davon für die Grafikkarte. Warte ab und schließe das Fenster nicht.
|
||||
|
||||
Starte beim nächsten Mal wieder `START-HERE.bat` (oder `./setup.sh`). Es startet sofort und lädt nichts doppelt herunter.
|
||||
Schließ sein Fenster, um das Modell zu beenden. `UPDATE.bat` (`./update.sh`) aktualisiert Strata, ohne es zu starten.
|
||||
Aktualisieren, Docker, mehrere Karten, wohin die Dateien kommen und alle Optionen: [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
Um deinen PC zu messen, führe `BENCH.bat` (`./bench.sh`) aus. Das dauert 5-10 Minuten und schreibt einen Bericht, den du teilen
|
||||
kannst ([so geht's](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
|
||||
|
||||
**Optional: mehr Tempo auf einer Karte, wenn das ganze Modell in den RAM passt.** Zwei Einstellungen zum Einschalten von
|
||||
RubenTorney (#1904, #1905). Sie helfen, wenn du eine Grafikkarte nutzt und dein RAM alle Experten aufnimmt, zum Beispiel bei
|
||||
Q2_0 oder IQ3_XXS auf einem PC mit 64 GB. Öffne `strata-<model>.json` im Strata-Datenordner ([wo](docs/INSTALL.md#where-things-are-stored)) und ergänze:
|
||||
|
||||
- `"--adapt-async", "1"` in der Liste `"args"`. Der Expertencache wird dann in einem Hilfsthread aktualisiert, statt die
|
||||
Antwort anzuhalten. Gemessen auf einer RTX 5070 (Windows, Q2_0): +4-5 % beim Decodieren, +7-10 % bei Code und langen
|
||||
Dokumenten. Die Antworten können sich leicht von der Standardeinstellung unterscheiden und bleiben von Lauf zu Lauf gleich.
|
||||
- `"STRATA_PIN_BY_NEED": "1"` im Block `"env"` (Windows). Windows kann nur etwa die Hälfte des RAM für die Grafikkarte sperren,
|
||||
und diese Einstellung sperrt zuerst die Experten, die die Karte am dringendsten braucht. Gemessen auf der RTX 5070: +4,4 % beim Lesen des Prompts.
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
Starte Strata danach neu (`START-HERE.bat` oder `./setup.sh`). Zum Rückgängigmachen entferne die beiden Einträge.
|
||||
Starte Strata nach der Änderung neu. Zum Rückgängigmachen entfernst du die beiden Einträge.
|
||||
|
||||
## Etwas ist schiefgelaufen?
|
||||
</details>
|
||||
|
||||
- **Mein PC ist beim ersten Start von Strata eingefroren.** Das ist normal, während das Modell lädt. Warte und
|
||||
schließ das Fenster nicht. Nach 10 Minuten immer noch eingefroren? Starte den PC neu, schließ andere Programme und
|
||||
versuch es noch einmal, oder nimm eine kleinere Größe.
|
||||
- **Es hat beim Herunterladen oder Installieren aufgehört.** Starte `START-HERE.bat` (oder `./setup.sh`) noch
|
||||
einmal. Es macht dort weiter, wo es aufgehört hat.
|
||||
- **Es ist sehr langsam und die Festplattenlampe blinkt ständig, oder es meldet „the engine stopped
|
||||
unexpectedly“.** Dein PC hat nicht genug freien RAM. Schließ andere Programme (Browser brauchen viel) oder nimm
|
||||
eine kleinere Größe (Q2_0 oder IQ2_XS).
|
||||
- **Es meldet, dass Port 8080 schon belegt ist.** Strata läuft bereits. Such nach seinem Fenster.
|
||||
## Wie funktioniert das?
|
||||
|
||||
Weitere Probleme und ihre Lösungen: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Kommst du nicht weiter?
|
||||
Öffne ein [Issue](https://github.com/Niko1221/Strata/issues) und hänge `strata-<model>.log` aus dem Strata-Ordner
|
||||
an. Du hast ein Sicherheitsproblem gefunden? Melde es vertraulich: [SECURITY.md](SECURITY.md).
|
||||
Modelle wie dieses laufen normalerweise auf Servern mit Hunderten Gigabyte Grafikspeicher. Deine Grafikkarte hat 12-24 GB.
|
||||
Strata bringt das Modell auf deinen PC, indem es die Arbeit auf den ganzen Rechner verteilt. Stell dir eine Bibliothek vor.
|
||||
|
||||
## Wie funktioniert es?
|
||||
- Das Modell ist ein Team aus 24.576 kleinen Spezialisten ("Experten"), wie 24.576 Nachschlagewerke. Für jedes Wort braucht es nur 10 davon.
|
||||
- **Deine Grafikkarte ist der Schreibtisch.** Er ist klein und schnell. Die etwa 4.000 am häufigsten genutzten Bücher liegen
|
||||
dort aufgeschlagen.
|
||||
- **Dein RAM sind die Regale im Lesesaal.** Alle 24.576 Bücher stehen dort. Braucht ein Wort ein Buch, das nicht auf dem
|
||||
Schreibtisch liegt, holt dein Prozessor es sofort aus dem Regal, während die Karte weiterarbeitet.
|
||||
- **Deine SSD ist das Archiv.** Sie enthält eine Nachschlagetabelle von 29 GB. Das Modell liest pro Wort nur wenige Zeilen
|
||||
daraus, deshalb muss sie nicht im RAM liegen.
|
||||
|
||||
Modelle wie dieses laufen normalerweise auf Servern mit Hunderten Gigabyte Grafikspeicher. Deine Grafikkarte hat
|
||||
12-24 GB. Strata bringt das Modell trotzdem unter, indem es **die Arbeit auf deinen ganzen PC verteilt**. Stell dir
|
||||
eine Küche vor: Was du ständig brauchst, bleibt auf der Arbeitsfläche, der Rest wartet in der Vorratskammer.
|
||||
<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.
|
||||
|
||||
- **Das Modell ist ein Team aus 24,576 kleinen Spezialisten („Experten“).** Jedes Wort braucht nur 10 davon.
|
||||
- **Deine Grafikkarte** hält die paar tausend Experten, die am häufigsten gebraucht werden. **Dein RAM** hält alle,
|
||||
und **dein Prozessor** rechnet gleichzeitig am Rest. **Deine SSD** hält eine große Nachschlagetabelle.
|
||||
<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 alle auf einmal und behält die richtigen"></p>
|
||||
|
||||
- **Raten, dann prüfen:** Ein kleiner Helfer rät die nächsten paar Wörter. Das große Modell prüft sie alle auf
|
||||
einmal. Du bekommst dieselbe Antwort, nur 1.6-1.8x schneller.
|
||||
- **Lange Texte werden in großen Stücken gelesen** (bis zu 8,192 Tokens auf einmal), mit über 1,000 Tokens pro
|
||||
Sekunde.
|
||||
|
||||
Die längere Erklärung: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Jeder Teil und seine Zahlen:
|
||||
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).
|
||||
|
||||
## Etwas ist schiefgegangen?
|
||||
|
||||
- **Mein PC ist beim ersten Start von Strata eingefroren.** Strata lädt das Modell in den RAM. Warte ab und schließe das
|
||||
Fenster nicht. Nach 10 Minuten immer noch eingefroren? Starte den PC neu, schließe andere Programme und versuch es noch
|
||||
einmal, oder wähle eine kleinere Größe.
|
||||
- **Es ist beim Herunterladen oder Installieren stehen geblieben.** Starte die App oder `strata launch` noch einmal, oder
|
||||
führe `START-HERE.bat` (oder `./setup.sh`) erneut aus. Es macht dort weiter, wo es aufgehört hat.
|
||||
- **Es ist sehr langsam, die Festplattenanzeige blinkt ständig, oder es meldet "the engine stopped unexpectedly".** Dein PC
|
||||
hat nicht genug freien RAM. Schließe andere Programme (Browser brauchen viel) oder wähle eine kleinere Größe wie Q2_0 oder IQ2_XS.
|
||||
- **Es meldet, dass es keine Engine für diesen PC gibt.** Öffne in der App **Einstellungen, Engine** und drücke **Engine
|
||||
aktualisieren**. Führe im Terminal `strata update` aus. `strata doctor` zeigt, welche Engine-Dateien installiert sind.
|
||||
- **Es meldet, dass Port 8080 schon belegt ist.** Strata läuft bereits. Such das Fenster.
|
||||
|
||||
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).
|
||||
|
||||
## 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)
|
||||
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). Ein
|
||||
paar Teile und alle Modelle haben eigene Lizenzen ([welche](docs/HOW_IT_WORKS.md#license)).
|
||||
[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
|
||||
|
||||
Du hast einen Fehler gefunden oder willst eine Änderung oder einen Benchmark einsenden? Lies zuerst [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
Dort steht, was dazugehört, damit andere deine Arbeit nachprüfen können.
|
||||
|
||||
## Strata unterstützen
|
||||
|
||||
Strata ist kostenlos und Open Source. Wenn es dir nützt, kannst du die Entwicklung unterstützen:
|
||||
Strata ist kostenlos und steht unter der MIT-Lizenz. Ich baue es und teste es auf jeder Hardware, die ich bekommen kann. Wenn es auf deinem PC gut läuft, hilft mir ein Kaffee,
|
||||
weiter neue Karten zu testen und das zu beheben, was ihr meldet. Es besteht kein Druck. Strata zu nutzen und zu melden, was dir auffällt, hilft genauso viel.
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a></p>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
|
||||
|
||||
Du möchtest Strata sponsern? Schreib mir über [GitHub](https://github.com/Niko1221).
|
||||
@@ -1,233 +1,403 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · **Español** · [Português](README.pt-BR.md)
|
||||
<p align="center"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <b>Español</b> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<p align="center"><b>Ejecuta un modelo de IA de 125 mil millones de parámetros en tu propio PC gaming</b><br>
|
||||
Tarjeta gráfica NVIDIA o AMD (12 GB o más) · Windows o Linux · libre y de código abierto</p>
|
||||
<h3 align="center">Ejecuta un modelo de última generación en tu propio PC gaming</h3>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<sub>Un jardín con una pagoda de vóxeles, hecho con un solo prompt en una RTX 5070 con Strata (IQ3_S, contexto de 128K) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">vídeo completo (49 s)</a></sub></p>
|
||||
|
||||
Strata ejecuta **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** en un PC normal. Es un
|
||||
modelo de IA grande e inteligente que normalmente necesita un servidor. Conversa, escribe código, entiende imágenes
|
||||
y funciona con tus aplicaciones y agentes de programación. Nada sale de tu PC.
|
||||
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.
|
||||
|
||||
## ¿Qué velocidad tiene?
|
||||
**¿Eres nuevo?** Mira [lo que necesitas](#lo-que-necesitas) y luego [instala la aplicación](#opción-a-la-aplicación-strata).
|
||||
|
||||
Lo medimos en dos PC gaming normales. Un token equivale más o menos a ¾ de una palabra.
|
||||
- **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).
|
||||
|
||||
- **Escribe respuestas:** lo rápido que aparece la respuesta en un chat corto. 60 tokens por segundo es más rápido de lo que puedes leer.
|
||||
- **Lee tu prompt:** lo rápido que procesa lo que le envías (aquí, un documento, código o historial de chat de 32K tokens).
|
||||
Versión 0.2.1 · motor 0.1.46
|
||||
|
||||
<table>
|
||||
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM</th></tr>
|
||||
<tr><td>
|
||||
## Lo que necesitas
|
||||
|
||||
| Tamaño | Escribe respuestas | Lee tu prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
|
||||
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
|
||||
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
|
||||
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
|
||||
| **Coder** | 55 tokens/s | 2,180 tokens/s |
|
||||
**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.
|
||||
|
||||
</td><td>
|
||||
- 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.
|
||||
|
||||
| Tamaño | Escribe respuestas | Lee tu prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
|
||||
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
|
||||
| **Coder** | 44 tokens/s | 1,420 tokens/s |
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
NVIDIA: Q2_0 con el motor 0.1.36, las demás filas con 0.1.26 (respuestas de 4K, prompts de 32K). Las tablas
|
||||
completas están en [DETAILS.md](docs/DETAILS.md#speed-measured). 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. Chats largos y otras tarjetas:
|
||||
[velocidad de cada modelo](docs/MODELS.md#how-fast-is-each-size), [resultados de la comunidad](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
|
||||
<sub>Strata es gratis. Si funciona bien en tu PC, un café ayuda a seguir trabajando en él.</sub></p>
|
||||
|
||||
## Qué necesitas
|
||||
|
||||
| | |
|
||||
| Tu ordenador | Detalles |
|
||||
| --- | --- |
|
||||
| **Tarjeta gráfica** | **NVIDIA** GeForce serie RTX 20, 30, 40 o 50, o **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 o serie RX 6800 / 6900. Necesita **12 GB de VRAM o más**. |
|
||||
| **RAM** | 32 GB o más. Tu RAM decide [qué modelo](#qué-modelo-elijo) cabe. Con 64 GB funcionan todos los tamaños. |
|
||||
| **Disco** | Unos 80 GB libres. Usa un SSD si puedes: el primer arranque es mucho más rápido. |
|
||||
| **Sistema** | Windows 10 / 11 o Linux, y un controlador gráfico actual de NVIDIA o AMD. |
|
||||
| **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) |
|
||||
|
||||
El instalador prepara todo lo demás. Dos o tres tarjetas pueden repartirse el modelo ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
También hace falta:
|
||||
|
||||
Experimental, escrito y probado por miembros de la comunidad en sus propios equipos:
|
||||
- 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
|
||||
|
||||
- **Tarjetas gráficas antiguas** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Older GPUs](docs/OLDER_GPUS.md).
|
||||
- **Intel Arc**, compilado desde el código fuente en Linux: [Intel Arc](docs/INTEL_ARC.md).
|
||||
- **AMD Ryzen AI Max (Strix Halo)**, compilado desde el código fuente en Linux: [Strix Halo](docs/STRIX_HALO.md).
|
||||
- **Procesadores antiguos sin AVX2**: funcionan, pero despacio. [Older CPUs](docs/INSTALL.md#older-cpus-experimental).
|
||||
|
||||
La lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
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).
|
||||
|
||||
## Instalación
|
||||
|
||||
### Deja que tu IA lo instale
|
||||
Elige una opción. Todas arrancan el mismo motor y usan los mismos modelos.
|
||||
|
||||
¿Usas un asistente de programación con IA (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Pega esto en él:
|
||||
### Opción A: la aplicación Strata
|
||||
|
||||
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.
|
||||
|
||||
| Sistema | Tu tarjeta gráfica | Archivo que descargar |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, controlador 580 o posterior | [`Strata-0.2.1-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda13.exe) |
|
||||
| | NVIDIA, tarjeta más antigua o controlador anterior a 580 | [`Strata-0.2.1-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda12.exe) |
|
||||
| | AMD Radeon serie RX 9000 o Radeon AI PRO R9700 | [`Strata-0.2.1-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip.exe) |
|
||||
| | AMD Radeon serie RX 7000 o Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, controlador 580 o posterior | [`Strata-0.2.1-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, tarjeta más antigua o controlador anterior a 580 | [`Strata-0.2.1-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon serie RX 9000 o Radeon AI PRO R9700 | [`Strata-0.2.1-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon serie RX 7000 | [`Strata-0.2.1-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 o posterior) | integrada | [`Strata-0.2.1-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-mac.dmg) |
|
||||
|
||||
**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).
|
||||
|
||||
Después:
|
||||
|
||||
- **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.
|
||||
|
||||
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.
|
||||
|
||||
### Opción B: la Terminal (una línea)
|
||||
|
||||
La Terminal es el comando `strata`, pensado para scripts, servidores y agentes de IA.
|
||||
|
||||
Linux y Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
El script analiza tu PC, descarga la Terminal, verifica su SHA-256 y la descomprime en tu carpeta personal. No necesita
|
||||
permisos de administrador. En Windows, abre una ventana de terminal nueva después. Luego arranca Strata:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
Te hace una pregunta cada vez: modelo, contexto, imágenes, quién puede conectarse. Después descarga el motor para tu tarjeta
|
||||
gráfica (firmado y arrancado como prueba antes de usarlo) y el modelo, y empieza a servir. Para scripts y agentes de IA, usa
|
||||
opciones:
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
Si quieres leer el script de instalación antes de ejecutarlo, descárgalo primero con `curl -fsSL <address> -o install.sh`,
|
||||
léelo y luego ejecuta `sh install.sh`. Todas las pantallas y opciones: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### Opción C: que lo instale tu IA
|
||||
|
||||
Pega esto en Claude Code, Cursor, Codex, GitHub Copilot u otro asistente de programación:
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
Revisa tu tarjeta gráfica, tu RAM y tu disco, y elige el modelo que cabe. Luego lo instala, lo arranca y te explica
|
||||
cómo conectar tus aplicaciones. Las herramientas de IA también pueden instalar, arrancar y detener Strata con su
|
||||
El asistente revisa tu tarjeta gráfica, tu RAM y tu disco, elige el modelo adecuado, lo instala y lo arranca, y te explica
|
||||
cómo conectar tus aplicaciones. Las herramientas de IA también pueden instalar, arrancar y detener Strata mediante su
|
||||
[servidor MCP](docs/MCP_SERVER.md).
|
||||
|
||||
### O hazlo tú mismo
|
||||
¿Prefieres compilarlo tú? Mira [Compilar desde el código fuente](#compilar-desde-el-código-fuente).
|
||||
|
||||
[Descarga Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) y descomprímelo (o usa `git clone`).
|
||||
**Windows:** haz doble clic en **`START-HERE.bat`**. **Linux:** ejecuta **`./setup.sh`** en la carpeta de Strata.
|
||||
## Modelos
|
||||
|
||||
**Mac (Apple Silicon, M1 a M5, macOS 15 o posterior):** instala una vez las Command Line Tools de Apple (`xcode-select --install` en Terminal; no hace falta Xcode). Luego abre Terminal en la carpeta de Strata y ejecuta **`./setup.sh`**. Compila el motor de Mac en tu Mac (unos 7 minutos), hace las mismas preguntas, descarga el modelo y lo arranca. La próxima vez, `./setup.sh` arranca enseguida. Qué tamaño cabe en tu Mac y qué velocidad tiene: [docs/MAC.md](docs/MAC.md).
|
||||
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.
|
||||
|
||||
Los pasos son los mismos para NVIDIA y AMD. El instalador detecta tu tarjeta y prepara el motor adecuado para ella.
|
||||
Te hace unas pocas preguntas:
|
||||
### ¿Poca memoria? Prueba Gyro
|
||||
|
||||
- qué modelo y qué tamaño,
|
||||
- cuánto contexto (cuánto texto tiene presente el modelo),
|
||||
- si debe entender imágenes.
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (de agentionai) es la forma más pequeña de ejecutar este modelo.
|
||||
|
||||
Pulsa Enter cada vez para usar la respuesta recomendada. Después descarga el modelo (unos 70 GB) y lo arranca. Si la
|
||||
descarga se detiene, vuelve a ejecutarlo: sigue donde lo dejó. Tu navegador abre la aplicación de Strata en
|
||||
`http://127.0.0.1:8080`.
|
||||
| Tamaño | Memoria que necesita (RAM + VRAM) | Descarga | Qué es |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27,6 GB | 58,5 GB | Para PC donde no cabe nada más. La calidad es menor que la de IQ2_XS. |
|
||||
| **Gyro-M** | 35 GB | 92,0 GB | Calidad parecida a IQ3_XXS con un 20 % menos de memoria de GPU. Escribe las respuestas un 17-21 % más despacio que Q2_0 en una tarjeta de 12 GB y en una R9700. |
|
||||
|
||||
> **Mientras el modelo arranca, tu PC puede ir lento o dejar de responder durante 1-3 minutos** (más la primera vez).
|
||||
> Strata carga 35-55 GB en tu RAM y reserva una parte para la tarjeta gráfica. Es normal. Espera y no cierres la
|
||||
> ventana. La ventana muestra lo que está haciendo Strata.
|
||||
Gyro funciona en tarjetas NVIDIA y AMD (en AMD es experimental), no en Intel Arc ni en el Mac. Detalles y mediciones:
|
||||
[docs/GYRO.md](docs/GYRO.md). Arráncalo con `strata launch --model gyro-s`.
|
||||
|
||||
**La próxima vez**, vuelve a ejecutar `START-HERE.bat` (o `./setup.sh`). Arranca enseguida y no descarga nada dos
|
||||
veces. Cierra su ventana para detener el modelo. `UPDATE.bat` (`./update.sh`) actualiza Strata sin arrancarlo.
|
||||
Actualizaciones, Docker, varias tarjetas, dónde se guardan los archivos y todas las opciones: [docs/INSTALL.md](docs/INSTALL.md).
|
||||
### Los demás tamaños
|
||||
|
||||
## ¿Qué modelo elijo?
|
||||
|
||||
El instalador te recomienda uno según tu RAM. El mismo modelo viene en varios tamaños, más o menos comprimidos. Los
|
||||
tamaños pequeños son más rápidos. Los grandes son algo más inteligentes.
|
||||
|
||||
| Tu RAM | Elige | Por qué |
|
||||
| Tu memoria (RAM + VRAM) | Elige | Notas |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Coder** | cabe en 32 GB y está hecho para programar (con una tarjeta de 24 GB también funcionan Q2_0 e IQ2_XS) |
|
||||
| **48 GB** | **IQ2_XS** (o Q2_0, el más rápido) | los tamaños más grandes no caben |
|
||||
| **64 GB** | **IQ2_XS** (recomendado), o IQ3_XXS / IQ3_S | caben todos los tamaños; IQ3_S es el mejor y el más lento |
|
||||
| **96 GB o más** | **IQ3_S**, o el UD-IQ4_XS de Unsloth (~4 bits) | espacio para los tamaños más grandes con todo lo demás abierto |
|
||||
| **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 17-21 % más despacio que Q2_0 |
|
||||
| **48 GB** | **IQ3_XXS** (47,0 GB) | Más lento, mejor |
|
||||
| **64 GB** | **IQ3_S** (54,8 GB), el original o Swift 1.5 | El más lento, el mejor. Iguala al modelo completo en las pruebas publicadas |
|
||||
| **96 GB o más** | **UD-IQ4_XS** de Unsloth (unos 72 GB de RAM) hasta **Q8_0** (unos 136 GB), de 4 a 8 bits | Más cerca del modelo completo. Con menos RAM, Strata lee una parte desde el SSD, que es mucho más lento |
|
||||
|
||||
- **[Coder](docs/MODELS.md#coder):** una versión para programar a la que se le quitó la mitad de los expertos. Alcanza
|
||||
el 91% de la puntuación SWE-bench Verified del modelo completo (medido por sus autores) y cabe en 32 GB de RAM. Es
|
||||
más flojo fuera del código, también con chino y otros textos CJK (#438). Para eso, elige Q2_0, IQ2_XS o IQ3_S, que
|
||||
conservan todos los expertos.
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15):** un ajuste fino que piensa mucho menos tiempo antes de responder. Recibes
|
||||
la respuesta antes, con más o menos la misma calidad.
|
||||
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** la versión de ~4 bits de Unsloth, entre IQ3_S y
|
||||
UD-Q4_K_XL en calidad. Una descarga de 94 GB. Con menos de ~80 GB de RAM, Strata lee una parte desde el SSD
|
||||
mientras responde, así que ahí es más lento (un SSD NVMe ayuda).
|
||||
- **[Unsloth UD-Q4_K_XL, UD-Q5_K_XL, UD-Q6_K_XL y Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):** de 4 a 8 bits, más cerca del modelo completo
|
||||
(93-95 % el mismo token siguiente que Q8_0). Rápidos cuando todos los expertos caben en la RAM (~88 GB para
|
||||
UD-Q4_K_XL hasta ~136 GB para Q8_0); con menos RAM, Strata lee una parte desde el SSD mientras responde, lo que es
|
||||
mucho más lento (7-8.5 tokens/s para UD-Q4_K_XL en un PC con 64 GB).
|
||||
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** se instala a mano. No está en
|
||||
el menú del instalador.
|
||||
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.
|
||||
|
||||
Tamaños, descargas y qué cabe dónde: [docs/MODELS.md](docs/MODELS.md). Para añadir otro modelo más adelante, ejecuta
|
||||
`SETUP.bat` (Linux: `./setup.sh --setup`).
|
||||
Más opciones:
|
||||
|
||||
## Cómo usarlo
|
||||
- **[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.
|
||||
|
||||
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="La pestaña Monitor de la aplicación de Strata junto a un agente de programación"><br>
|
||||
<sub>El <b>Monitor</b> de la aplicación de Strata (izquierda) mientras un agente de programación escribe el jardín de la pagoda del vídeo (derecha)</sub></p>
|
||||
Todos los tamaños, descargas y lo que cabe en cada equipo: [docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
- **En el navegador:** abre `http://127.0.0.1:8080`. Tiene **Chat**, un **Monitor** en vivo del modelo y de tu
|
||||
GPU/CPU/RAM, y **About** con los ajustes y las direcciones.
|
||||
- **Tus aplicaciones y agentes de programación:** añade un proveedor "compatible con OpenAI" con la URL base
|
||||
**`http://127.0.0.1:8080/v1`**. Sirve cualquier clave de API y cualquier nombre de modelo.
|
||||
- Aplicaciones que usan la API de Anthropic: `http://127.0.0.1:8080/v1/messages` (Claude Code:
|
||||
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
|
||||
- Codex CLI y otras aplicaciones que usan la API Responses de OpenAI: `/v1/responses`
|
||||
([configuración](docs/DETAILS.md#the-responses-api-and-codex-cli)).
|
||||
- **Razonamiento:** elige **off, low, medium o high** en el menú del chat o en el "reasoning effort" de tu aplicación.
|
||||
Off es lo más rápido. High es lo mejor para preguntas difíciles.
|
||||
- **Imágenes:** responde que sí a "Images?" en la instalación. Luego haz clic en **Picture** en el chat, o adjunta
|
||||
imágenes en tu aplicación. Las tarjetas AMD leen imágenes en Linux a través del procesador; en Windows todavía no.
|
||||
- **Desde tu móvil u otro PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Pon siempre una clave.
|
||||
- **Una petición cada vez:** por defecto Strata responde una petición y las demás esperan. Para responder varias a la
|
||||
vez, pon `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). En una tarjeta de 12 GB, cada respuesta va más lenta.
|
||||
- **Prompts largos:** Strata lee entero el primer mensaje de un chat, más o menos 1 minuto por cada 30,000 tokens. Los
|
||||
mensajes siguientes empiezan en segundos.
|
||||
## ¿Qué velocidad tiene?
|
||||
|
||||
Más: [dónde se guardan tus chats](docs/INSTALL.md#where-things-are-stored), [la API](docs/DETAILS.md#using-it).
|
||||
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).
|
||||
|
||||
### Opcional: más velocidad con una sola tarjeta cuando todo el modelo cabe en la RAM
|
||||
| Modelo | Las respuestas aparecen a | Lee tu prompt a |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████████` 94 tokens/s | `████████████` 2.650 tokens/s |
|
||||
| **IQ2_XS** | `██████████░░` 79 tokens/s | `█████████░░░` 2.090 tokens/s |
|
||||
| **IQ3_XXS** | `████████░░░░` 62 tokens/s | `████████░░░░` 1.750 tokens/s |
|
||||
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1.620 tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2.180 tokens/s |
|
||||
|
||||
Dos ajustes opcionales (de RubenTorney, #1904 y #1905). Ayudan si usas una tarjeta gráfica y tu RAM guarda todos los expertos (el modo arena de Strata, por ejemplo Q2_0 o IQ3_XXS en un PC con 64 GB). Abre tu configuración `strata-<model>.json` en la carpeta de datos de Strata ([dónde](docs/INSTALL.md#where-things-are-stored)) y añade:
|
||||
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.
|
||||
|
||||
- **Generación más rápida:** `"--adapt-async", "1"` en la lista `"args"`. La caché de expertos se actualiza entonces en un hilo auxiliar en vez de pausar la respuesta. Medido en una RTX 5070 (Windows, Q2_0): +4-5 % de generación, +7-10 % en código y documentos largos. Las respuestas pueden diferir un poco de las por defecto (son iguales de una ejecución a otra).
|
||||
- **Lectura del prompt más rápida en Windows:** `"STRATA_PIN_BY_NEED": "1"` en el bloque `"env"`. Windows solo puede bloquear cerca de la mitad de la RAM para la tarjeta; esto bloquea primero los expertos que la tarjeta necesita de verdad. Medido en la RTX 5070: +4,4 % en la lectura del prompt.
|
||||
Medido con una tarjeta AMD de 16 GB (RX 9070 XT, Ryzen 9 3900X, 47 GB de RAM):
|
||||
|
||||
| Modelo | Las respuestas aparecen a | Lee tu prompt a |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████░░░░` 60 tokens/s | `█████░░░░░░░` 1.160 tokens/s |
|
||||
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1.110 tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1.420 tokens/s |
|
||||
|
||||
Las barras usan la misma escala en las dos tablas. Una tarjeta con más VRAM es más rápida. Una RTX 3090 (24 GB) debería
|
||||
escribir unos 100-140 tokens por segundo. La versión del motor detrás de cada cifra está en [DETAILS.md](docs/DETAILS.md#speed-measured).
|
||||
|
||||
## Uso
|
||||
|
||||
### En la aplicación
|
||||
|
||||
La aplicación tiene un **Chat**, un **Monitor** del modelo y de tu GPU, CPU y RAM, una página de **Modelos** y los
|
||||
**Ajustes**. El chat puede trabajar en una carpeta de tu PC. En modo **Manual** pregunta antes de cambiar un archivo o
|
||||
ejecutar un comando. Escribe `/loop 10m <what to do>` para repetir un mensaje cada 10 minutos mientras el chat esté abierto.
|
||||
|
||||
El chat puede buscar en la web con DuckDuckGo (o Bing), sin cuenta. Puedes añadir una clave de búsqueda en **Ajustes,
|
||||
Herramientas**. La búsqueda es la única función que envía algo fuera de tu PC (las palabras que buscas).
|
||||
|
||||
Para ejecutar dos modelos, activa **Ejecutar varios modelos** y dale a cada modelo su propia tarjeta de la misma marca.
|
||||
|
||||
<p align="center"><img src="docs/media/app-chat.png" width="900" alt="The Strata app: a chat where the agent built a small voxel pagoda garden, with the page open in the preview on the right"><br>
|
||||
<sub>Un chat en la app de Strata: el agente escribió un pequeño jardín de pagoda de vóxeles en un solo archivo HTML, y la página se abre en la vista previa junto a la conversación</sub></p>
|
||||
|
||||
### En la Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<sub><code>strata monitor</code> mientras el modelo responde: velocidad, GPU, memoria y las últimas solicitudes</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided screens: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
|
||||
strata launch --plan --model q2_0 # show what would happen; nothing changes
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
|
||||
strata update --check # is there a new engine? (strata update installs it)
|
||||
strata doctor # check the installed engine files
|
||||
```
|
||||
|
||||
`strata --help` lo lista todo. Arrancar y servir no descargan nada. Solo lo hacen la instalación, `strata launch` (cuando
|
||||
eliges un modelo) y `strata update`. Para llegar a Strata desde tu móvil u otro PC, usa `strata launch --access lan`. Crea
|
||||
una clave de API y no arranca en una red sin ella. No se abre ningún navegador salvo que añadas `--open`. Guía:
|
||||
[docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). El cliente completo: [docs/TERMINAL.md](docs/TERMINAL.md).
|
||||
|
||||
### Con agentes de IA
|
||||
|
||||
Un agente instala Strata con el comando de una línea de la opción B y ejecuta
|
||||
`strata launch --model NAME --yes --no-tui`. Los pasos, las opciones y los códigos de salida están en [docs/AI_SETUP.md](docs/AI_SETUP.md),
|
||||
y las reglas del repositorio en [AGENTS.md](AGENTS.md).
|
||||
|
||||
### Conecta tus otras aplicaciones
|
||||
|
||||
| Tipo de aplicación | URL base |
|
||||
| --- | --- |
|
||||
| Compatible con OpenAI (la mayoría de aplicaciones y agentes) | `http://127.0.0.1:8080/v1` (cualquier clave de API, cualquier nombre de modelo) |
|
||||
| API de Anthropic (Claude Code y otros) | `http://127.0.0.1:8080`, sin `/v1`. Estas aplicaciones añaden `/v1/messages` por su cuenta. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| API OpenAI Responses (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([configuración](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` muestra los ajustes exactos para OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent y OpenClaw.
|
||||
`strata connect opencode --write` escribe el archivo después de preguntar y guarda una copia de seguridad.
|
||||
|
||||
### Imágenes
|
||||
|
||||
Las imágenes vienen activadas por defecto en los modelos que pueden leerlas. Adjunta o pega una imagen en el chat, o envíala
|
||||
desde tu aplicación. El lector de imágenes funciona en el procesador cuando tu tarjeta gráfica tiene menos de 24 GB (unos
|
||||
segundos por imagen, sin quitar memoria a la tarjeta) y en la tarjeta gráfica a partir de 24 GB. La regla es la misma para
|
||||
NVIDIA y AMD. Puedes desactivar las imágenes por modelo, o elegir el lector con `--vision off`, `--vision gpu` o
|
||||
`--vision cpu`.
|
||||
|
||||
Las cifras detrás de la regla: en una Radeon AI PRO R9700, el lector de la tarjeta tardó 57 ms en una imagen de 260 tokens y
|
||||
el del procesador 3,1 s. En tarjetas de 16 GB (Tesla P100 y RTX A4000, Linux), el lector de la tarjeta costó un 3-5 % de
|
||||
velocidad en texto, porque ocupa unos 1,4 GB que de otro modo usaría el modelo. En un Mac lee la GPU, tenga la memoria que tenga (0,45 s para una imagen de 260 tokens en un M4 de 32 GB, unos 1,2 GB de la memoria compartida);
|
||||
el procesador tarda unos 50 s por imagen, así que solo lee con `--vision cpu`.
|
||||
|
||||
### Conviene saber
|
||||
|
||||
- Elige el nivel de razonamiento (**desactivado, bajo, medio o alto**) en el menú del chat o en "reasoning effort" de tu aplicación. Desactivado es lo más rápido.
|
||||
- Strata responde a una petición a la vez por defecto. Para varias a la vez, usa `--parallel N` en `strata launch` o
|
||||
`"parallel": 2` en la configuración ([BATCHING.md](docs/BATCHING.md)). En una tarjeta de 12 GB, esto hace más lenta cada respuesta.
|
||||
- Strata lee entero el primer mensaje de un chat, unos 1 minuto por cada 30.000 tokens. Los mensajes siguientes empiezan en segundos.
|
||||
- Dónde se guardan tus chats y la API completa: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
|
||||
|
||||
## Compilar desde el código fuente
|
||||
|
||||
<details>
|
||||
<summary>Windows, Linux y Mac desde el repositorio</summary>
|
||||
|
||||
[Descarga Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) y descomprímelo, o haz `git clone`.
|
||||
**Windows:** haz doble clic en `START-HERE.bat`. **Linux:** ejecuta `./setup.sh` en la carpeta de Strata.
|
||||
|
||||
**Mac (Apple Silicon, macOS 15 o posterior):** instala una vez las Command Line Tools de Apple con `xcode-select --install`
|
||||
(no hace falta Xcode). Abre la Terminal en la carpeta de Strata y ejecuta `./setup.sh`. El script compila el motor para Mac
|
||||
en tu Mac (unos 7 minutos), hace las mismas preguntas, descarga el modelo y lo arranca. La próxima vez, `./setup.sh` lo
|
||||
arranca enseguida. Qué tamaño cabe en tu Mac: [docs/MAC.md](docs/MAC.md).
|
||||
|
||||
El instalador detecta tu tarjeta y prepara el motor adecuado. Pregunta qué modelo y qué tamaño quieres, cuánto contexto
|
||||
(cuánto texto mantiene el modelo en mente) y si debe leer imágenes. Pulsa Enter para aceptar la respuesta recomendada.
|
||||
Luego descarga el modelo (unos 70 GB) y lo arranca. Si la descarga se detiene, vuelve a ejecutar el script y continúa. Tu
|
||||
navegador abre la página de Strata en `http://127.0.0.1:8080`.
|
||||
|
||||
> Mientras el modelo arranca, tu PC puede ir lento o dejar de responder durante 1-3 minutos (más tiempo la primera vez).
|
||||
> Strata carga 35-55 GB en tu RAM y bloquea una parte para la tarjeta gráfica. Espera y no cierres la ventana.
|
||||
|
||||
La próxima vez, ejecuta de nuevo `START-HERE.bat` (o `./setup.sh`). Arranca enseguida y no descarga nada dos veces. Cierra
|
||||
su ventana para detener el modelo. `UPDATE.bat` (`./update.sh`) actualiza Strata sin arrancarlo. Actualización, Docker,
|
||||
varias tarjetas, dónde van los archivos y todas las opciones: [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
Para medir tu PC, ejecuta `BENCH.bat` (`./bench.sh`). Tarda 5-10 minutos y escribe un informe que puedes compartir
|
||||
([cómo](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
|
||||
|
||||
**Opcional: más velocidad en una tarjeta cuando todo el modelo cabe en la RAM.** Dos ajustes que se activan a mano, de
|
||||
RubenTorney (#1904, #1905). Ayudan cuando usas una sola tarjeta gráfica y tu RAM guarda todos los expertos, por ejemplo con
|
||||
Q2_0 o IQ3_XXS en un PC de 64 GB. Abre `strata-<model>.json` en la carpeta de datos de Strata ([dónde](docs/INSTALL.md#where-things-are-stored)) y añade:
|
||||
|
||||
- `"--adapt-async", "1"` a la lista `"args"`. Así la caché de expertos se actualiza en un hilo auxiliar en vez de detener la
|
||||
respuesta. Medido en una RTX 5070 (Windows, Q2_0): +4-5 % en decodificación, +7-10 % en código y documentos largos. Las
|
||||
respuestas pueden diferir un poco de las del ajuste por defecto y son iguales de una ejecución a otra.
|
||||
- `"STRATA_PIN_BY_NEED": "1"` en el bloque `"env"` (Windows). Windows solo puede bloquear aproximadamente la mitad de la RAM
|
||||
para la tarjeta gráfica, y este ajuste bloquea primero los expertos que la tarjeta necesita antes. Medido en la RTX 5070: +4,4 % en lectura del prompt.
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
Reinicia Strata (`START-HERE.bat` o `./setup.sh`) después del cambio. Para deshacerlo, quita las dos entradas.
|
||||
Reinicia Strata después del cambio. Para deshacerlo, quita las dos entradas.
|
||||
|
||||
## ¿Algo salió mal?
|
||||
|
||||
- **Mi PC se congeló la primera vez que arrancó Strata.** Es normal mientras carga el modelo. Espera y no cierres la
|
||||
ventana. ¿Sigue congelado después de 10 minutos? Reinicia el PC, cierra otros programas y vuelve a intentarlo, o
|
||||
elige un tamaño más pequeño.
|
||||
- **Se detuvo durante la descarga o la instalación.** Vuelve a ejecutar `START-HERE.bat` (o `./setup.sh`). Sigue
|
||||
donde se detuvo.
|
||||
- **Va muy lento y la luz del disco no deja de parpadear, o dice "the engine stopped unexpectedly".** Tu PC no tiene
|
||||
suficiente RAM libre. Cierra otros programas (los navegadores usan mucha), o elige un tamaño más pequeño (Q2_0 o
|
||||
IQ2_XS).
|
||||
- **Dice que el puerto 8080 ya está en uso.** Strata ya está en marcha. Busca su ventana.
|
||||
|
||||
Más problemas y sus soluciones: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). ¿Sigues atascado? Abre un
|
||||
[issue](https://github.com/Niko1221/Strata/issues) y adjunta `strata-<model>.log` de la carpeta de Strata. ¿Has
|
||||
encontrado un problema de seguridad? Infórmalo en privado: [SECURITY.md](SECURITY.md).
|
||||
</details>
|
||||
|
||||
## ¿Cómo funciona?
|
||||
|
||||
Los modelos como este suelen funcionar en servidores con cientos de gigabytes de memoria gráfica. Tu tarjeta gráfica
|
||||
tiene 12-24 GB. Strata hace que el modelo quepa **repartiendo el trabajo por todo tu PC**. Piensa en una cocina: lo
|
||||
que usas todo el rato se queda en la encimera, y el resto espera en la despensa.
|
||||
Los modelos como este suelen ejecutarse en servidores con cientos de gigabytes de memoria gráfica. Tu tarjeta gráfica tiene
|
||||
12-24 GB. Strata hace que el modelo quepa repartiendo el trabajo por todo tu PC. Imagina una biblioteca.
|
||||
|
||||
<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 y una tabla de consulta en el SSD"></p>
|
||||
- El modelo es un equipo de 24.576 pequeños especialistas ("expertos"), como 24.576 libros de consulta. Cada palabra necesita solo 10 de ellos.
|
||||
- **Tu tarjeta gráfica es el escritorio.** Es pequeño y rápido. Los unos 4.000 libros que más se usan están abiertos encima.
|
||||
- **Tu RAM son las estanterías de la sala de lectura.** Los 24.576 libros están ahí. Cuando una palabra necesita un libro que
|
||||
no está en el escritorio, tu procesador lo coge enseguida de la estantería mientras la tarjeta sigue trabajando.
|
||||
- **Tu SSD es el archivo.** Guarda una tabla de consulta de 29 GB. El modelo lee unas pocas filas por palabra, así que no
|
||||
hace falta que esté en la RAM.
|
||||
|
||||
- **El modelo es un equipo de 24,576 pequeños especialistas ("expertos").** Cada palabra solo necesita 10 de ellos.
|
||||
- **Tu tarjeta gráfica** guarda los pocos miles de expertos que más se usan. **Tu RAM** los tiene todos, y **tu
|
||||
procesador** trabaja con el resto al mismo tiempo. **Tu SSD** guarda una gran tabla de consulta.
|
||||
<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/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>
|
||||
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.
|
||||
|
||||
- **Adivinar y comprobar:** un pequeño ayudante adivina las siguientes palabras. El modelo grande las comprueba todas
|
||||
a la vez. Recibes la misma respuesta, 1.6-1.8x antes.
|
||||
- **Los textos largos se leen en trozos grandes** (hasta 8,192 tokens cada 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>
|
||||
|
||||
La explicación larga: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Cada parte y sus números:
|
||||
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).
|
||||
|
||||
## ¿Algo ha ido mal?
|
||||
|
||||
- **Mi PC se congeló la primera vez que arrancó Strata.** Carga el modelo en la RAM. Espera y no cierres la ventana. ¿Sigue
|
||||
congelado después de 10 minutos? Reinicia el PC, cierra otros programas e inténtalo de nuevo, o elige un tamaño más pequeño.
|
||||
- **Se detuvo durante la descarga o la instalación.** Abre de nuevo la aplicación o `strata launch`, o vuelve a ejecutar
|
||||
`START-HERE.bat` (o `./setup.sh`). Continúa donde se quedó.
|
||||
- **Va muy lento, la luz del disco parpadea sin parar, o dice "the engine stopped unexpectedly".** Tu PC no tiene bastante
|
||||
RAM libre. Cierra otros programas (los navegadores gastan mucha) o elige un tamaño más pequeño, como Q2_0 o IQ2_XS.
|
||||
- **Dice que no hay motor para este PC.** En la aplicación, abre **Ajustes, Motor** y pulsa **Actualizar motor**. En la
|
||||
Terminal, ejecuta `strata update`. `strata doctor` muestra qué archivos del motor están instalados.
|
||||
- **Dice que el puerto 8080 ya está en uso.** Strata ya se está ejecutando. Busca su ventana.
|
||||
|
||||
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).
|
||||
|
||||
## 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)
|
||||
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 cada modelo tienen sus propias licencias ([cuáles](docs/HOW_IT_WORKS.md#license)).
|
||||
[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)).
|
||||
|
||||
## Apoya a Strata
|
||||
## Contribuir
|
||||
|
||||
Strata es libre y de código abierto. Si te resulta útil, puedes apoyar su desarrollo:
|
||||
¿Has encontrado un error, o quieres enviar un cambio o un benchmark? Lee primero [CONTRIBUTING.md](CONTRIBUTING.md). Explica
|
||||
qué incluir para que otros puedan comprobar tu trabajo.
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a></p>
|
||||
## Apoya Strata
|
||||
|
||||
Strata es gratis y tiene licencia MIT. Lo construyo y lo pruebo en el hardware que consigo. Si te funciona bien en tu PC, un café me ayuda
|
||||
a seguir probando tarjetas nuevas y a arreglar lo que nos cuentas. Sin presión. Usarlo y contar lo que encuentras ayuda igual de mucho.
|
||||
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
|
||||
|
||||
¿Te interesa patrocinar Strata? Escríbeme a través de [GitHub](https://github.com/Niko1221).
|
||||
@@ -1,238 +1,405 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · **Français** · [Español](README.es.md) · [Português](README.pt-BR.md)
|
||||
<p align="center"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <b>Français</b> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<p align="center"><b>Faites tourner un modèle d'IA de 125 milliards de paramètres sur votre propre PC de jeu</b><br>
|
||||
Carte graphique NVIDIA ou AMD (12 Go ou plus) · Windows ou Linux · gratuit et open source</p>
|
||||
<h3 align="center">Faites tourner un modèle de pointe sur votre propre PC de jeu</h3>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<sub>Un jardin de pagode en voxels, généré en une seule requête sur une RTX 5070 avec Strata (IQ3_S, contexte de 128K) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">vidéo complète (49 s)</a></sub></p>
|
||||
|
||||
Strata fait tourner **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** sur un PC normal. C'est
|
||||
un grand modèle d'IA, très capable, qui demande d'habitude un serveur. Il discute, écrit du code, lit des images et
|
||||
fonctionne avec vos applications et vos agents de code. Rien ne quitte votre PC.
|
||||
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.
|
||||
|
||||
## Quelle est sa vitesse ?
|
||||
**Nouveau ici ?** Regardez [ce qu'il vous faut](#ce-quil-vous-faut), puis [installez l'application](#option-a--lapplication-strata).
|
||||
|
||||
Nous l'avons mesurée sur deux PC de jeu ordinaires. Un token correspond à environ ¾ d'un mot.
|
||||
- **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).
|
||||
|
||||
- **Écrit les réponses :** la vitesse à laquelle la réponse s'affiche dans une courte discussion. 60 tokens par seconde, c'est plus rapide que votre lecture.
|
||||
- **Lit votre prompt :** la vitesse à laquelle il absorbe ce que vous envoyez (ici un document, du code ou un historique de discussion de 32K tokens).
|
||||
|
||||
<table>
|
||||
<tr><th>NVIDIA : RTX 5070 (12 Go), Ryzen 5 7600, 64 Go de RAM</th><th>AMD : RX 9070 XT (16 Go), Ryzen 9 3900X, 47 Go de RAM</th></tr>
|
||||
<tr><td>
|
||||
|
||||
| Taille | Écrit les réponses | Lit votre prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
|
||||
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
|
||||
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
|
||||
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
|
||||
| **Coder** | 55 tokens/s | 2,180 tokens/s |
|
||||
|
||||
</td><td>
|
||||
|
||||
| Taille | Écrit les réponses | Lit votre prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
|
||||
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
|
||||
| **Coder** | 44 tokens/s | 1,420 tokens/s |
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
NVIDIA : Q2_0 avec le moteur 0.1.36, les autres lignes avec 0.1.26 (réponses de 4K, prompts de 32K). Les tableaux
|
||||
complets sont dans [DETAILS.md](docs/DETAILS.md#speed-measured). Une carte avec plus de VRAM va plus vite : une
|
||||
RTX 3090 (24 Go) devrait écrire environ 100-140 tokens par seconde. Longues discussions et autres cartes :
|
||||
[vitesse de chaque modèle](docs/MODELS.md#how-fast-is-each-size), [résultats de la communauté](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
|
||||
<sub>Strata est gratuit. S'il tourne bien sur votre PC, un café aide à poursuivre le travail.</sub></p>
|
||||
Version 0.2.1 · moteur 0.1.46
|
||||
|
||||
## 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.
|
||||
|
||||
- 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 |
|
||||
| --- | --- |
|
||||
| **Carte graphique** | **NVIDIA** GeForce RTX série 20, 30, 40 ou 50, ou **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 ou série RX 6800 / 6900. Il faut **12 Go de VRAM ou plus**. |
|
||||
| **RAM** | 32 Go ou plus. Votre RAM décide [quel modèle](#quel-modèle-choisir-) tient. Avec 64 Go, toutes les tailles tournent. |
|
||||
| **Disque** | Environ 80 Go libres. Prenez un SSD si possible : le premier démarrage est bien plus rapide. |
|
||||
| **Système** | Windows 10 / 11 ou Linux, avec un pilote graphique NVIDIA ou AMD à jour. |
|
||||
| **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) |
|
||||
|
||||
L'installateur s'occupe de tout le reste. Deux ou trois cartes peuvent se partager le modèle ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
Il faut aussi :
|
||||
|
||||
Expérimental, écrit et testé par des membres de la communauté sur leurs propres machines :
|
||||
- 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
|
||||
|
||||
- **Cartes graphiques plus anciennes** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT) : [Older GPUs](docs/OLDER_GPUS.md).
|
||||
- **Intel Arc**, compilé depuis les sources sous Linux : [Intel Arc](docs/INTEL_ARC.md).
|
||||
- **AMD Ryzen AI Max (Strix Halo)**, compilé depuis les sources sous Linux : [Strix Halo](docs/STRIX_HALO.md).
|
||||
- **Processeurs plus anciens sans AVX2** : ça marche, mais lentement. [Older CPUs](docs/INSTALL.md#older-cpus-experimental).
|
||||
|
||||
La liste complète : [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
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).
|
||||
|
||||
## Installation
|
||||
|
||||
### Laissez votre IA l'installer
|
||||
Choisissez une option. Elles lancent toutes le même moteur et utilisent les mêmes modèles.
|
||||
|
||||
Vous utilisez un assistant de code IA (Claude Code, Cursor, Codex, GitHub Copilot, ...) ? Collez-lui ceci :
|
||||
### Option A : l'application Strata
|
||||
|
||||
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.
|
||||
|
||||
| Système | Votre carte graphique | Fichier à télécharger |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, pilote 580 ou plus récent | [`Strata-0.2.1-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda13.exe) |
|
||||
| | NVIDIA, carte plus ancienne ou pilote antérieur à 580 | [`Strata-0.2.1-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda12.exe) |
|
||||
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.1-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip.exe) |
|
||||
| | AMD Radeon série RX 7000 ou Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, pilote 580 ou plus récent | [`Strata-0.2.1-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, carte plus ancienne ou pilote antérieur à 580 | [`Strata-0.2.1-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.1-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon série RX 7000 | [`Strata-0.2.1-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 ou plus récent) | intégrée | [`Strata-0.2.1-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-mac.dmg) |
|
||||
|
||||
**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).
|
||||
|
||||
Ensuite :
|
||||
|
||||
- **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.
|
||||
|
||||
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.
|
||||
|
||||
### Option B : le Terminal (une ligne)
|
||||
|
||||
Le Terminal est la commande `strata`, pensée pour les scripts, les serveurs et les agents d'IA.
|
||||
|
||||
Linux et Mac :
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
```
|
||||
|
||||
Windows (PowerShell) :
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
Le script analyse votre PC, télécharge le Terminal, vérifie son empreinte SHA-256 et le décompresse dans votre dossier
|
||||
personnel. Il ne demande pas de droits d'administrateur. Sous Windows, ouvrez ensuite une nouvelle fenêtre de terminal.
|
||||
Puis lancez Strata :
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
Il pose une question à la fois : modèle, contexte, images, qui peut se connecter. Ensuite il télécharge le moteur pour votre
|
||||
carte graphique (signé, et lancé à titre d'essai avant usage) ainsi que le modèle, puis commence à servir. Pour les scripts
|
||||
et les agents d'IA, utilisez des options :
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
Pour lire le script d'installation avant de l'exécuter, téléchargez-le d'abord avec `curl -fsSL <address> -o install.sh`,
|
||||
lisez-le, puis lancez `sh install.sh`. Tous les écrans et toutes les options : [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### Option C : laissez votre IA l'installer
|
||||
|
||||
Collez ceci dans Claude Code, Cursor, Codex, GitHub Copilot ou un autre assistant de programmation :
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
Il vérifie votre carte graphique, votre RAM et votre disque, et choisit le modèle qui convient. Ensuite il l'installe,
|
||||
le démarre et vous explique comment connecter vos applications. Les outils d'IA peuvent aussi installer, démarrer et
|
||||
arrêter Strata grâce à son [serveur MCP](docs/MCP_SERVER.md).
|
||||
L'assistant vérifie votre carte graphique, votre RAM et votre disque, choisit le modèle adapté, l'installe, le démarre et
|
||||
vous explique comment connecter vos applications. Les outils d'IA peuvent aussi installer, démarrer et arrêter Strata via son
|
||||
[serveur MCP](docs/MCP_SERVER.md).
|
||||
|
||||
### Ou faites-le vous-même
|
||||
Vous préférez le compiler vous-même ? Voir [Compiler depuis les sources](#compiler-depuis-les-sources).
|
||||
|
||||
[Téléchargez Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) et décompressez-le (ou faites un `git clone`).
|
||||
**Windows :** double-cliquez sur **`START-HERE.bat`**. **Linux :** lancez **`./setup.sh`** dans le dossier Strata.
|
||||
## Modèles
|
||||
|
||||
**Mac (Apple Silicon, M1 à M5, macOS 15 ou plus récent) :** installez une fois les Command Line Tools d'Apple (`xcode-select --install` dans le Terminal ; Xcode lui-même n'est pas nécessaire). Ouvrez ensuite le Terminal dans le dossier Strata et lancez **`./setup.sh`**. Il compile le moteur Mac sur votre Mac (environ 7 minutes), pose les mêmes questions, télécharge le modèle et le démarre. La fois suivante, `./setup.sh` démarre tout de suite. Quelle taille convient à votre Mac et sa vitesse : [docs/MAC.md](docs/MAC.md).
|
||||
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.
|
||||
|
||||
Les étapes sont les mêmes pour NVIDIA et AMD. L'installateur détecte votre carte et installe le bon moteur pour elle.
|
||||
Il vous pose quelques questions :
|
||||
### Peu de mémoire ? Essayez Gyro
|
||||
|
||||
- quel modèle et quelle taille,
|
||||
- combien de contexte (la quantité de texte que le modèle garde en tête),
|
||||
- s'il doit lire les images.
|
||||
[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.
|
||||
|
||||
Appuyez sur Entrée à chaque fois pour garder la réponse recommandée. Ensuite il télécharge le modèle (environ 70 Go)
|
||||
et le démarre. Si le téléchargement s'arrête, relancez-le : il reprend là où il s'était arrêté. Votre navigateur
|
||||
ouvre l'application Strata à l'adresse `http://127.0.0.1:8080`.
|
||||
| Taille | Mémoire nécessaire (RAM + VRAM) | Téléchargement | Ce que c'est |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27,6 Go | 58,5 Go | Pour les PC où rien d'autre ne tient. La qualité est inférieure à celle d'IQ2_XS. |
|
||||
| **Gyro-M** | 35 Go | 92,0 Go | À peu près la qualité d'IQ3_XXS avec environ 20 % de mémoire GPU en moins. Il écrit les réponses 17 à 21 % plus lentement que Q2_0 sur une carte de 12 Go et sur une R9700. |
|
||||
|
||||
> **Pendant le démarrage du modèle, votre PC peut ralentir ou ne plus répondre pendant 1-3 minutes** (plus longtemps la première fois).
|
||||
> Strata charge 35-55 Go dans votre RAM et en réserve une partie pour la carte graphique. C'est normal. Attendez, et
|
||||
> ne fermez pas la fenêtre. La fenêtre montre ce que fait Strata.
|
||||
Gyro fonctionne sur les cartes NVIDIA et AMD (expérimental sur AMD), pas sur Intel Arc ni sur Mac. Détails et mesures :
|
||||
[docs/GYRO.md](docs/GYRO.md). Lancez-le avec `strata launch --model gyro-s`.
|
||||
|
||||
**La fois suivante**, relancez `START-HERE.bat` (ou `./setup.sh`). Il démarre tout de suite et ne télécharge rien
|
||||
deux fois. Fermez sa fenêtre pour arrêter le modèle. `UPDATE.bat` (`./update.sh`) met Strata à jour sans le démarrer.
|
||||
Mises à jour, Docker, plusieurs cartes, emplacement des fichiers et toutes les options : [docs/INSTALL.md](docs/INSTALL.md).
|
||||
### Les autres tailles
|
||||
|
||||
## Quel modèle choisir ?
|
||||
|
||||
L'installateur en recommande un selon votre RAM. Le même modèle existe en plusieurs tailles, plus ou moins
|
||||
compressées. Les petites tailles sont plus rapides. Les grandes sont un peu plus intelligentes.
|
||||
|
||||
| Votre RAM | Prenez | Pourquoi |
|
||||
| Votre mémoire (RAM + VRAM) | Prenez | Remarques |
|
||||
| --- | --- | --- |
|
||||
| **32 Go** | **Coder** | il tient dans 32 Go et il est fait pour le code (avec une carte de 24 Go, Q2_0 et IQ2_XS tournent aussi) |
|
||||
| **48 Go** | **IQ2_XS** (ou Q2_0, le plus rapide) | les plus grandes tailles ne tiennent pas |
|
||||
| **64 Go** | **IQ2_XS** (recommandé), ou IQ3_XXS / IQ3_S | toutes les tailles tiennent ; IQ3_S est le meilleur et le plus lent |
|
||||
| **96 Go ou plus** | **IQ3_S**, ou l'UD-IQ4_XS d'Unsloth (~4 bits) | de la place pour les plus grandes tailles, avec tout le reste ouvert |
|
||||
| **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 17 à 21 % plus lentement que Q2_0 |
|
||||
| **48 Go** | **IQ3_XXS** (47,0 Go) | Plus lent, meilleur |
|
||||
| **64 Go** | **IQ3_S** (54,8 Go), l'original ou Swift 1.5 | Le plus lent, le meilleur. Il égale le modèle complet dans les tests publiés |
|
||||
| **96 Go ou plus** | **UD-IQ4_XS** d'Unsloth (environ 72 Go de RAM) jusqu'à **Q8_0** (environ 136 Go), 4 à 8 bits | Plus proche du modèle complet. Avec moins de RAM, Strata lit une partie depuis le SSD, ce qui est beaucoup plus lent |
|
||||
|
||||
- **[Coder](docs/MODELS.md#coder) :** une version pour le code, avec la moitié des experts en moins. Elle atteint 91 %
|
||||
du score SWE-bench Verified du modèle complet (mesuré par ses auteurs) et tient dans 32 Go de RAM. Elle est moins
|
||||
bonne en dehors du code, y compris pour le chinois et les autres textes CJK (#438). Pour ces cas, prenez Q2_0,
|
||||
IQ2_XS ou IQ3_S, qui gardent tous les experts.
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15) :** un fine-tune qui réfléchit bien moins longtemps avant de répondre. Vous
|
||||
avez la réponse plus tôt, avec à peu près la même qualité.
|
||||
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs)** : la version ~4 bits d'Unsloth, entre IQ3_S et
|
||||
UD-Q4_K_XL en qualité. Un téléchargement de 94 Go. Avec moins de ~80 Go de RAM, Strata en lit une partie depuis
|
||||
le SSD pendant qu'il répond, il y est donc plus lent (un SSD NVMe aide).
|
||||
- **[Unsloth UD-Q4_K_XL, UD-Q5_K_XL, UD-Q6_K_XL et Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0) :** de 4 à 8 bits, plus proches du modèle
|
||||
complet (93-95 % le même token suivant que Q8_0). Rapides quand tous les experts tiennent dans la RAM (~88 Go pour
|
||||
UD-Q4_K_XL jusqu'à ~136 Go pour Q8_0) ; avec moins de RAM, Strata en lit une partie depuis le SSD pendant qu'il
|
||||
répond, ce qui est bien plus lent (7-8.5 tokens/s pour UD-Q4_K_XL sur un PC avec 64 Go).
|
||||
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs) :** vous l'installez à la main.
|
||||
Il n'est pas dans le menu de l'installateur.
|
||||
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.
|
||||
|
||||
Tailles, téléchargements et ce qui tient où : [docs/MODELS.md](docs/MODELS.md). Pour ajouter un autre modèle plus
|
||||
tard, lancez `SETUP.bat` (Linux : `./setup.sh --setup`).
|
||||
Autres choix :
|
||||
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** est une version affinée qui réfléchit moins longtemps avant de répondre, pour une qualité et un besoin en RAM à peu près identiques.
|
||||
- **[NVFP4](docs/NVFP4.md)** est la version 4 bits de NVIDIA pour les cartes NVIDIA. Elle demande environ 80 Go de RAM pour garder tous les experts en RAM.
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** est un modèle plus petit et différent, qui lit les images et peut répondre à plusieurs requêtes à la fois.
|
||||
- **[L'Uncensored IQ3_XXS d'OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** se configure à la main.
|
||||
|
||||
Toutes les tailles, les téléchargements et ce qui tient où : [docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
## Quelle est sa vitesse ?
|
||||
|
||||
Mesuré avec une carte graphique de 12 Go (RTX 5070, Ryzen 5 7600, 64 Go de RAM). Voir les résultats d'autres cartes :
|
||||
[vitesse de chaque taille](docs/MODELS.md#how-fast-is-each-size), [résultats de la communauté](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
| Modèle | Les réponses s'affichent à | Lit votre prompt à |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████████` 94 tokens/s | `████████████` 2 650 tokens/s |
|
||||
| **IQ2_XS** | `██████████░░` 79 tokens/s | `█████████░░░` 2 090 tokens/s |
|
||||
| **IQ3_XXS** | `████████░░░░` 62 tokens/s | `████████░░░░` 1 750 tokens/s |
|
||||
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1 620 tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2 180 tokens/s |
|
||||
|
||||
Un token vaut environ ¾ de mot, donc 60 tokens par seconde, c'est plus rapide que votre lecture. « Lit votre prompt »
|
||||
indique la vitesse à laquelle Strata absorbe un long document, du code ou un historique de discussion.
|
||||
|
||||
Mesuré avec une carte AMD de 16 Go (RX 9070 XT, Ryzen 9 3900X, 47 Go de RAM) :
|
||||
|
||||
| Modèle | Les réponses s'affichent à | Lit votre prompt à |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████░░░░` 60 tokens/s | `█████░░░░░░░` 1 160 tokens/s |
|
||||
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1 110 tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1 420 tokens/s |
|
||||
|
||||
Les barres utilisent la même échelle dans les deux tableaux. Une carte avec plus de VRAM est plus rapide. Une RTX 3090
|
||||
(24 Go) devrait écrire environ 100 à 140 tokens par seconde. La version du moteur derrière chaque chiffre est indiquée dans
|
||||
[DETAILS.md](docs/DETAILS.md#speed-measured).
|
||||
|
||||
## Utilisation
|
||||
|
||||
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="L'onglet Monitor de l'application Strata à côté d'un agent de code"><br>
|
||||
<sub>Le <b>Monitor</b> de l'application Strata (à gauche) pendant qu'un agent de code écrit le jardin de pagode de la vidéo (à droite)</sub></p>
|
||||
### Dans l'application
|
||||
|
||||
- **Dans le navigateur :** ouvrez `http://127.0.0.1:8080`. Vous y trouvez **Chat**, un **Monitor** en direct du
|
||||
modèle et de votre GPU/CPU/RAM, et **About** avec les réglages et les adresses.
|
||||
- **Vos applications et agents de code :** ajoutez un fournisseur « OpenAI-compatible » avec l'URL de base
|
||||
**`http://127.0.0.1:8080/v1`**. N'importe quelle clé API et n'importe quel nom de modèle fonctionnent.
|
||||
- Applications qui utilisent l'API d'Anthropic : `http://127.0.0.1:8080/v1/messages` (Claude Code :
|
||||
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
|
||||
- Codex CLI et les autres applications qui utilisent l'API OpenAI Responses : `/v1/responses`
|
||||
([configuration](docs/DETAILS.md#the-responses-api-and-codex-cli)).
|
||||
- **Réflexion :** choisissez **off, low, medium ou high** dans le menu du chat ou dans le « reasoning effort » de
|
||||
votre application. Off est le plus rapide. High est le meilleur pour les questions difficiles.
|
||||
- **Images :** répondez oui à « Images? » pendant l'installation. Ensuite cliquez sur **Picture** dans le chat, ou
|
||||
joignez des images dans votre application. Les cartes AMD lisent les images sous Linux via le processeur ; sous
|
||||
Windows, pas encore.
|
||||
- **Depuis votre téléphone ou un autre PC :** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Définissez
|
||||
toujours une clé.
|
||||
- **Une requête à la fois :** par défaut, Strata répond à une requête et les autres attendent. Pour répondre à
|
||||
plusieurs en même temps, mettez `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Sur une carte de 12 Go, chaque
|
||||
réponse devient alors plus lente.
|
||||
- **Longs prompts :** Strata lit en entier le premier message d'une discussion, environ 1 minute pour 30,000 tokens.
|
||||
Les messages suivants démarrent en quelques secondes.
|
||||
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.
|
||||
|
||||
Plus d'infos : [où sont stockées vos discussions](docs/INSTALL.md#where-things-are-stored), [l'API](docs/DETAILS.md#using-it).
|
||||
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).
|
||||
|
||||
### Optionnel : plus de vitesse sur une seule carte quand tout le modèle tient en RAM
|
||||
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.
|
||||
|
||||
Deux réglages optionnels (de RubenTorney, #1904 et #1905). Ils aident si vous utilisez une seule carte graphique et que votre RAM contient tous les experts (le mode arena de Strata, par exemple Q2_0 ou IQ3_XXS sur un PC de 64 Go). Ouvrez votre configuration `strata-<model>.json` dans le dossier de données de Strata ([où](docs/INSTALL.md#where-things-are-stored)) et ajoutez :
|
||||
<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>
|
||||
|
||||
- **Génération plus rapide :** `"--adapt-async", "1"` dans la liste `"args"`. Le cache d'experts est alors mis à jour dans un thread auxiliaire au lieu de mettre la réponse en pause. Mesuré sur une RTX 5070 (Windows, Q2_0) : +4-5 % de génération, +7-10 % sur le code et les longs documents. Les réponses peuvent légèrement différer de celles par défaut (elles restent identiques d'une exécution à l'autre).
|
||||
- **Lecture du prompt plus rapide sous Windows :** `"STRATA_PIN_BY_NEED": "1"` dans le bloc `"env"`. Windows ne peut verrouiller qu'environ la moitié de la RAM pour la carte ; ce réglage verrouille d'abord les experts dont la carte a vraiment besoin. Mesuré sur la RTX 5070 : +4,4 % en lecture du prompt.
|
||||
### Dans le Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<sub><code>strata monitor</code> pendant que le modèle répond : vitesse, GPU, mémoire et les dernières requêtes</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided screens: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
|
||||
strata launch --plan --model q2_0 # show what would happen; nothing changes
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
|
||||
strata update --check # is there a new engine? (strata update installs it)
|
||||
strata doctor # check the installed engine files
|
||||
```
|
||||
|
||||
`strata --help` liste tout. Démarrer et servir ne téléchargent rien. Seuls l'installation, `strata launch` (quand vous
|
||||
choisissez un modèle) et `strata update` le font. Pour joindre Strata depuis votre téléphone ou un autre PC, utilisez
|
||||
`strata launch --access lan`. La commande crée une clé d'API et refuse de démarrer sur un réseau sans elle. Aucun
|
||||
navigateur ne s'ouvre, sauf si vous ajoutez `--open`. Guide : [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Le client complet : [docs/TERMINAL.md](docs/TERMINAL.md).
|
||||
|
||||
### Avec des agents d'IA
|
||||
|
||||
Un agent installe Strata avec la commande en une ligne de l'option B et lance
|
||||
`strata launch --model NAME --yes --no-tui`. Les étapes, les options et les codes de sortie sont dans [docs/AI_SETUP.md](docs/AI_SETUP.md),
|
||||
et les règles du dépôt dans [AGENTS.md](AGENTS.md).
|
||||
|
||||
### Connecter vos autres applications
|
||||
|
||||
| Type d'application | URL de base |
|
||||
| --- | --- |
|
||||
| Compatible OpenAI (la plupart des applications et des agents) | `http://127.0.0.1:8080/v1` (n'importe quelle clé d'API, n'importe quel nom de modèle) |
|
||||
| API Anthropic (Claude Code et d'autres) | `http://127.0.0.1:8080`, sans `/v1`. Ces applications ajoutent `/v1/messages` elles-mêmes. Claude Code : `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| API OpenAI Responses (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([configuration](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` affiche les réglages exacts pour OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent et OpenClaw.
|
||||
`strata connect opencode --write` écrit le fichier après confirmation et garde une sauvegarde.
|
||||
|
||||
### Images
|
||||
|
||||
Les images sont activées par défaut pour les modèles qui savent les lire. Joignez ou collez une image dans le chat, ou
|
||||
envoyez-la depuis votre application. Le lecteur d'images tourne sur le processeur quand votre carte graphique a moins de
|
||||
24 Go (quelques secondes par image, sans prendre de mémoire à la carte) et sur la carte graphique à partir de 24 Go. La
|
||||
règle est la même pour NVIDIA et AMD. Vous pouvez désactiver les images par modèle, ou choisir le lecteur avec
|
||||
`--vision off`, `--vision gpu` ou `--vision cpu`.
|
||||
|
||||
Les chiffres derrière la règle : sur une Radeon AI PRO R9700, le lecteur de la carte a pris 57 ms pour une image de
|
||||
260 tokens et celui du processeur 3,1 s. Sur des cartes de 16 Go (Tesla P100 et RTX A4000, Linux), le lecteur de la carte
|
||||
a coûté 3 à 5 % de vitesse sur le texte, parce qu'il occupe environ 1,4 Go que le modèle utiliserait sinon. Sur un Mac, le lecteur tourne sur le GPU quelle que soit la mémoire (0,45 s pour une image de 260 tokens sur un M4 de 32 Go, environ 1,2 Go de la
|
||||
mémoire partagée) ; le processeur met environ 50 s par image, il ne lit donc qu'avec `--vision cpu`.
|
||||
|
||||
### Bon à savoir
|
||||
|
||||
- Choisissez le niveau de réflexion (**désactivé, faible, moyen ou élevé**) dans le menu du chat ou dans le réglage « reasoning effort » de votre application. Désactivé est le plus rapide.
|
||||
- Strata répond à une requête à la fois par défaut. Pour en traiter plusieurs en même temps, utilisez `--parallel N` dans `strata launch` ou
|
||||
`"parallel": 2` dans la configuration ([BATCHING.md](docs/BATCHING.md)). Sur une carte de 12 Go, chaque réponse devient alors plus lente.
|
||||
- Strata lit en entier le premier message d'une discussion, soit environ 1 minute pour 30 000 tokens. Les messages suivants démarrent en quelques secondes.
|
||||
- Où sont stockées vos discussions et l'API complète : [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
|
||||
|
||||
## Compiler depuis les sources
|
||||
|
||||
<details>
|
||||
<summary>Windows, Linux et Mac depuis le dépôt</summary>
|
||||
|
||||
[Téléchargez Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) et décompressez-le, ou faites un `git clone`.
|
||||
**Windows :** double-cliquez sur `START-HERE.bat`. **Linux :** lancez `./setup.sh` dans le dossier Strata.
|
||||
|
||||
**Mac (Apple Silicon, macOS 15 ou plus récent) :** installez une fois les Command Line Tools d'Apple avec `xcode-select --install`
|
||||
(Xcode lui-même n'est pas nécessaire). Ouvrez le Terminal dans le dossier Strata et lancez `./setup.sh`. Le script compile
|
||||
le moteur Mac sur votre Mac (environ 7 minutes), pose les mêmes questions, télécharge le modèle et le démarre. La fois
|
||||
suivante, `./setup.sh` le démarre tout de suite. Quelle taille convient à votre Mac : [docs/MAC.md](docs/MAC.md).
|
||||
|
||||
L'installateur détecte votre carte et prépare le bon moteur. Il demande quel modèle et quelle taille, combien de contexte
|
||||
(la quantité de texte que le modèle garde en tête) et s'il doit lire les images. Appuyez sur Entrée pour la réponse
|
||||
recommandée. Puis il télécharge le modèle (environ 70 Go) et le démarre. Si le téléchargement s'arrête, relancez le script
|
||||
et il reprend. Votre navigateur ouvre la page de Strata à l'adresse `http://127.0.0.1:8080`.
|
||||
|
||||
> Pendant le démarrage du modèle, votre PC peut être lent ou ne plus répondre pendant 1 à 3 minutes (le plus longtemps la
|
||||
> première fois). Strata charge 35 à 55 Go dans votre RAM et en verrouille une partie pour la carte graphique. Patientez et
|
||||
> ne fermez pas la fenêtre.
|
||||
|
||||
La fois suivante, relancez `START-HERE.bat` (ou `./setup.sh`). Il démarre tout de suite et ne télécharge rien deux fois.
|
||||
Fermez sa fenêtre pour arrêter le modèle. `UPDATE.bat` (`./update.sh`) met Strata à jour sans le démarrer. Mise à jour,
|
||||
Docker, plusieurs cartes, où vont les fichiers et toutes les options : [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
Pour mesurer votre PC, lancez `BENCH.bat` (`./bench.sh`). Cela prend 5 à 10 minutes et produit un rapport que vous pouvez
|
||||
partager ([comment](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
|
||||
|
||||
**Facultatif : plus de vitesse sur une carte quand tout le modèle tient en RAM.** Deux réglages à activer soi-même, par
|
||||
RubenTorney (#1904, #1905). Ils aident quand vous utilisez une seule carte graphique et que votre RAM contient tous les
|
||||
experts, par exemple Q2_0 ou IQ3_XXS sur un PC de 64 Go. Ouvrez `strata-<model>.json` dans le dossier de données de Strata ([où](docs/INSTALL.md#where-things-are-stored)) et ajoutez :
|
||||
|
||||
- `"--adapt-async", "1"` à la liste `"args"`. Le cache des experts se met alors à jour sur un thread auxiliaire au lieu de
|
||||
mettre la réponse en pause. Mesuré sur une RTX 5070 (Windows, Q2_0) : +4 à 5 % en décodage, +7 à 10 % sur le code et les
|
||||
longs documents. Les réponses peuvent différer légèrement de celles du réglage par défaut, et restent identiques d'une exécution à l'autre.
|
||||
- `"STRATA_PIN_BY_NEED": "1"` dans le bloc `"env"` (Windows). Windows ne peut verrouiller qu'environ la moitié de la RAM
|
||||
pour la carte graphique, et ce réglage verrouille d'abord les experts dont la carte a le plus besoin. Mesuré sur la RTX 5070 : +4,4 % en lecture du prompt.
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
Redémarrez Strata (`START-HERE.bat` ou `./setup.sh`) après la modification. Pour annuler, retirez les deux entrées.
|
||||
Redémarrez Strata après la modification. Pour annuler, supprimez les deux entrées.
|
||||
|
||||
## Un problème ?
|
||||
|
||||
- **Mon PC s'est figé au premier démarrage de Strata.** C'est normal pendant le chargement du modèle. Attendez, 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.
|
||||
- **Il s'est arrêté pendant le téléchargement ou l'installation.** Relancez `START-HERE.bat` (ou `./setup.sh`). Il
|
||||
reprend là où il s'était arrêté.
|
||||
- **C'est très lent et 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 les autres programmes (les navigateurs en utilisent beaucoup), ou
|
||||
choisissez une taille plus petite (Q2_0 ou IQ2_XS).
|
||||
- **Il dit que le port 8080 est déjà utilisé.** Strata tourne déjà. Cherchez sa fenêtre.
|
||||
|
||||
Autres problèmes et leurs solutions : [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Toujours bloqué ? Ouvrez une
|
||||
[issue](https://github.com/Niko1221/Strata/issues) et joignez `strata-<model>.log` depuis le dossier Strata. Vous
|
||||
avez trouvé un problème de sécurité ? Signalez-le en privé : [SECURITY.md](SECURITY.md).
|
||||
</details>
|
||||
|
||||
## Comment ça marche ?
|
||||
|
||||
Les modèles comme celui-ci tournent d'habitude sur des serveurs avec des centaines de gigaoctets de mémoire
|
||||
graphique. Votre carte graphique a 12-24 Go. Strata fait tenir le modèle en **répartissant le travail sur tout votre
|
||||
PC**. Pensez à une cuisine : ce que vous utilisez tout le temps reste sur le plan de travail, et le reste attend dans
|
||||
le placard.
|
||||
Les modèles de ce genre tournent d'habitude sur des serveurs qui disposent de centaines de gigaoctets de mémoire graphique.
|
||||
Votre carte graphique en a 12 à 24. Strata fait tenir le modèle en répartissant le travail sur tout votre PC. Imaginez une
|
||||
bibliothèque.
|
||||
|
||||
<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 en RAM, une table de correspondance sur le SSD"></p>
|
||||
- Le modèle est une équipe de 24 576 petits spécialistes (les « experts »), comme 24 576 ouvrages de référence. Chaque mot n'en demande que 10.
|
||||
- **Votre carte graphique est le bureau.** Il est petit et rapide. Les quelque 4 000 livres les plus utilisés y sont ouverts.
|
||||
- **Votre RAM, ce sont les étagères de la salle de lecture.** Les 24 576 livres s'y trouvent. Quand un mot demande un livre
|
||||
qui n'est pas sur le bureau, votre processeur va le chercher aussitôt sur l'étagère, pendant que la carte continue de travailler.
|
||||
- **Votre SSD, ce sont les archives.** Il contient une table de consultation de 29 Go. Le modèle n'en lit que quelques lignes par
|
||||
mot, elle n'a donc pas besoin de tenir en RAM.
|
||||
|
||||
- **Le modèle est une équipe de 24,576 petits spécialistes (les « experts »).** Chaque mot n'en demande que 10.
|
||||
- **Votre carte graphique** garde les quelques milliers d'experts les plus utilisés. **Votre RAM** les contient tous,
|
||||
et **votre processeur** travaille sur les autres en même temps. **Votre SSD** contient une grande table de
|
||||
correspondance.
|
||||
<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/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>
|
||||
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.
|
||||
|
||||
- **Deviner, puis vérifier :** un petit assistant devine les quelques mots suivants. Le grand modèle les vérifie
|
||||
tous d'un coup. Vous obtenez la même réponse, 1.6-1.8x plus tôt.
|
||||
- **Les longs textes 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>
|
||||
|
||||
L'explication plus longue : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Chaque partie et ses chiffres :
|
||||
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).
|
||||
|
||||
## Crédits et licence
|
||||
## Un problème ?
|
||||
|
||||
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)
|
||||
- **Mon PC s'est figé au premier démarrage de Strata.** Il charge le modèle en RAM. Patientez et ne fermez pas la fenêtre.
|
||||
Toujours figé après 10 minutes ? Redémarrez le PC, fermez les autres programmes et réessayez, ou choisissez une taille plus petite.
|
||||
- **Ça s'est arrêté pendant le téléchargement ou l'installation.** Relancez l'application ou `strata launch`, ou relancez
|
||||
`START-HERE.bat` (ou `./setup.sh`). Cela reprend là où ça s'était arrêté.
|
||||
- **C'est très lent, le voyant du disque clignote sans arrêt, ou il affiche « the engine stopped unexpectedly ».** Votre PC
|
||||
n'a pas assez de RAM libre. Fermez d'autres programmes (les navigateurs en consomment beaucoup) ou choisissez une taille plus petite, comme Q2_0 ou IQ2_XS.
|
||||
- **Il indique qu'il n'y a pas de moteur pour ce PC.** Dans l'application, ouvrez **Settings, Engine** et appuyez sur **Update engine** (mettre à jour le moteur). Dans le Terminal, lancez `strata update`. `strata doctor` montre quels fichiers du moteur sont installés.
|
||||
- **Il indique que le port 8080 est déjà utilisé.** Strata tourne déjà. Cherchez sa fenêtre.
|
||||
|
||||
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).
|
||||
|
||||
## 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
|
||||
crédits : [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits). Strata est open source sous [licence MIT](LICENSE).
|
||||
Quelques parties et chaque modèle ont leur propre licence ([lesquelles](docs/HOW_IT_WORKS.md#license)).
|
||||
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
|
||||
|
||||
Vous avez trouvé un bug, ou vous voulez proposer une modification ou un benchmark ? Lisez d'abord [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
Il indique ce qu'il faut inclure pour que d'autres puissent vérifier votre travail.
|
||||
|
||||
## Soutenir Strata
|
||||
|
||||
Strata est gratuit et open source. S'il vous est utile, vous pouvez soutenir son développement :
|
||||
Strata est gratuit et sous licence MIT. Je le construis et je le teste sur le matériel que je peux obtenir. S'il tourne bien sur votre PC, un café m'aide
|
||||
à continuer de tester de nouvelles cartes et à corriger ce que vous signalez. Aucune obligation. L'utiliser et signaler ce que vous constatez aide tout autant.
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a></p>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
|
||||
|
||||
Vous souhaitez sponsoriser Strata ? Contactez-moi via [GitHub](https://github.com/Niko1221).
|
||||
@@ -1,229 +1,385 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[English](README.md) · [简体中文](README.zh-CN.md) · **日本語** · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md)
|
||||
<p align="center"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <b>日本語</b> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<p align="center"><b>1,250 億パラメータの AI モデルを、手元のゲーミング PC で動かす</b><br>
|
||||
NVIDIA または AMD のグラフィックカード(12 GB 以上) · Windows または Linux · 無料・オープンソース</p>
|
||||
<h3 align="center">最先端のモデルを、手元のゲーミングPCで動かす</h3>
|
||||
|
||||
<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)に、1 回のプロンプトで作成を依頼 ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">動画を見る(全編 49 秒)</a></sub></p>
|
||||
<p align="center">NVIDIA、AMD、Intel に対応 · Windows、Linux、Mac · 無料のオープンソース</p>
|
||||
|
||||
Strata を使うと、**[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** を一般的な PC で動かせます。
|
||||
通常はサーバーを必要とする大規模で高性能な AI モデルで、チャットやコードの生成、画像の読み取りができます。
|
||||
アプリやコーディングエージェントとも連携でき、データは PC の外に送信されません。
|
||||
<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>
|
||||
</p>
|
||||
|
||||
## どのくらい速い?
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<sub>ボクセルの塔の庭園。1回のプロンプトで生成し、RTX 5070 上の Strata で動かしています(IQ3_S、128K コンテキスト) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">動画全体(49秒)</a></sub></p>
|
||||
|
||||
一般的なゲーミング PC 2 台で速度を測定しました。英語の場合、1 トークンはおよそ ¾ 語に相当します。
|
||||
Strata は **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** を動かします。1250億パラメータのモデルで、
|
||||
ふつうはサーバーが必要ですが、Strata なら一般的なPCで使えます。チャット、コード作成、画像の読み取りができ、
|
||||
お使いのアプリやコーディングエージェントとも連携します。チャットとファイルはPCの外に出ません。
|
||||
|
||||
- **生成速度:** 短いチャットで回答を生成する速度です。毎秒 60 トークンなら、人が読むよりも速く文章が表示されます。
|
||||
- **プロンプトの読み込み:** 入力した内容を読み込む速度です。ここでは、文書やコード、チャット履歴など 32K トークン分を読み込ませています。
|
||||
**はじめての方へ。** まず[必要な環境](#必要な環境)を確認して、[アプリをインストール](#方法a-strata-アプリ)してください。
|
||||
|
||||
<table>
|
||||
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM</th></tr>
|
||||
<tr><td>
|
||||
- **ゲーミングPCで動きます。** RAMとグラフィックスメモリの合計が32 GBあれば始められます。
|
||||
- **お使いのツールと連携します。** OpenAI と Anthropic の API に対応しているので、Claude Code、Codex CLI、OpenCode などのエージェントから接続できます。
|
||||
- **画像を読めます。** 初期設定でオンです。
|
||||
- **無料です。** MIT ライセンス。アカウントもクラウドも不要です。
|
||||
- **プライバシーを守ります。** テレメトリはありません。Strata が何をダウンロードするか、いつインターネットを使うか、ダウンロードをどう検証するかは [docs/SAFETY.md](docs/SAFETY.md) に書いてあります。
|
||||
|
||||
| モデルサイズ | 生成速度 | プロンプトの読み込み |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
|
||||
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
|
||||
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
|
||||
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
|
||||
| **Coder** | 55 tokens/s | 2,180 tokens/s |
|
||||
バージョン 0.2.1 · エンジン 0.1.46
|
||||
|
||||
</td><td>
|
||||
## 必要な環境
|
||||
|
||||
| モデルサイズ | 生成速度 | プロンプトの読み込み |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
|
||||
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
|
||||
| **Coder** | 44 tokens/s | 1,420 tokens/s |
|
||||
**RAMとグラフィックスメモリ(VRAM)の合計が、少なくとも32 GB必要です。** MacやRyzen AI Maxでは、ユニファイドメモリがその量にあたります。
|
||||
メモリが多いほど、大きなモデルを使えたり、速く動いたりします。
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
- RAM 32 GBのPCに12〜16 GBのグラフィックスカードがあれば、Coder が動きます。
|
||||
- RAM 64 GBなら、どのサイズも動きます。
|
||||
|
||||
NVIDIA の測定には、Q2_0 のみエンジン 0.1.36、それ以外は 0.1.26 を使用しています(回答 4K、プロンプト 32K)。測定結果の詳細は
|
||||
[DETAILS.md](docs/DETAILS.md#speed-measured) をご覧ください。VRAM の容量が大きいカードほど高速で、RTX 3090(24 GB)なら
|
||||
毎秒およそ 100-140 トークンの生成速度が見込めます。長いチャットや他のカードでの性能については、[モデルごとの速度](docs/MODELS.md#how-fast-is-each-size)や
|
||||
[コミュニティの測定結果](docs/COMMUNITY_BENCHMARKS.md)を参照してください。
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
|
||||
<sub>Strata は無料で使えます。お使いの PC で快適に動いたら、コーヒー 1 杯分のご支援が開発を続ける励みになります。</sub></p>
|
||||
|
||||
## 必要なもの
|
||||
|
||||
| | |
|
||||
| お使いのPC | 詳細 |
|
||||
| --- | --- |
|
||||
| **グラフィックカード** | **NVIDIA** GeForce RTX 20、30、40、50 シリーズ、または **AMD** Radeon RX 7900 XT / XTX、RX 7800 XT / 7700 XT、RX 9060 XT、RX 9070 / 9070 XT、Radeon AI PRO R9700、RX 6800 / 6900 シリーズ。**VRAM 12 GB 以上**が必要です。 |
|
||||
| **RAM** | 32 GB 以上。[利用できるモデル](#どのモデルを選べばいい)は RAM の容量によって異なります。64 GB あれば、どのサイズも利用できます。 |
|
||||
| **ディスク** | 約 80 GB の空き容量。SSD を使うと初回の起動が大幅に速くなるため、おすすめです。 |
|
||||
| **OS** | Windows 10 / 11 または Linux。NVIDIA または AMD の最新のグラフィックドライバーも必要です。 |
|
||||
| **NVIDIA** GeForce RTX 20、30、40、50 シリーズ(12 GB以上) | アプリまたはターミナル版をインストールしてください。 |
|
||||
| **AMD** Radeon RX 6800 以降、Radeon AI PRO R9700(12 GB以上) | [AMD のカード](docs/AMD_HIP.md) |
|
||||
| **Intel** Arc | [Intel Arc](docs/INTEL_ARC.md)、[Intel エンジン](docs/INTEL.md) |
|
||||
| **Apple Silicon の Mac**(M1 以降、macOS 15 以降) | [Mac](docs/MAC.md) |
|
||||
| **AMD Ryzen AI Max(Strix Halo)** などのユニファイドメモリ搭載機 | [Strix Halo](docs/STRIX_HALO.md) |
|
||||
| 古いカードやプロセッサ | [古いGPU](docs/OLDER_GPUS.md)、[古いCPU](docs/INSTALL.md#older-cpus-experimental) |
|
||||
|
||||
その他の必要なソフトウェアは、インストーラーが用意します。2 枚または 3 枚のカードにモデルを分散させることもできます([マルチ GPU](docs/MULTI_GPU.md))。
|
||||
そのほかに必要なもの:
|
||||
|
||||
以下のハードウェアにも試験的に対応しています。いずれもコミュニティのメンバーが手元のマシンで実装・テストしたものです。
|
||||
- 約80 GBの空きディスク容量(SSDなら初回の起動がずっと速くなります)
|
||||
- Windows 10 / 11、Linux、または macOS
|
||||
- 最新のグラフィックスドライバー
|
||||
|
||||
- **旧世代のグラフィックカード**(Tesla P40 / V100、GTX 10、Radeon VII / MI50、RX 6700 XT、RX 5500 XT):[旧世代 GPU の対応状況](docs/OLDER_GPUS.md)。
|
||||
- **Intel Arc**(Linux でソースからビルド):[Intel Arc の対応状況](docs/INTEL_ARC.md)。
|
||||
- **AMD Ryzen AI Max(Strix Halo)**(Linux でソースからビルド):[Strix Halo の対応状況](docs/STRIX_HALO.md)。
|
||||
- **AVX2 非対応の旧世代 CPU**:動作しますが、速度は遅くなります。[旧世代 CPU の対応状況](docs/INSTALL.md#older-cpus-experimental)を参照してください。
|
||||
|
||||
必要な環境の一覧は、[docs/INSTALL.md](docs/INSTALL.md#what-you-need)をご覧ください。
|
||||
同じメーカーのカードを2〜3枚使って、1つのモデルを動かせます([マルチGPU](docs/MULTI_GPU.md))。NVIDIA と AMD のカードにまたがってモデルを分けることはありません。
|
||||
一覧は [docs/INSTALL.md](docs/INSTALL.md#what-you-need) にあります。
|
||||
|
||||
## インストール
|
||||
|
||||
### AI にセットアップしてもらう
|
||||
3つの方法から1つ選んでください。どれも同じエンジンを起動し、同じモデルを使います。
|
||||
|
||||
Claude Code、Cursor、Codex、GitHub Copilot などの AI コーディングアシスタントを使っている場合は、次の文を貼り付けてください。
|
||||
### 方法A: Strata アプリ
|
||||
|
||||
いちばん簡単な方法です。エンジンはアプリに入っているので、インストールしてモデルを選べばチャットを始められます。お使いのOSとグラフィックスカードに合うファイルを
|
||||
[リリースページ](https://github.com/Niko1221/Strata/releases/latest)からダウンロードしてください。どれか分からない場合は、表の下の説明を見てください。
|
||||
|
||||
| OS | グラフィックスカード | ダウンロードするファイル |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX、ドライバー 580 以降 | [`Strata-0.2.1-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda13.exe) |
|
||||
| | NVIDIA、古いカード、またはドライバーが 580 より古い場合 | [`Strata-0.2.1-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda12.exe) |
|
||||
| | AMD Radeon RX 9000 シリーズまたは Radeon AI PRO R9700 | [`Strata-0.2.1-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip.exe) |
|
||||
| | AMD Radeon RX 7000 シリーズまたは Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX、ドライバー 580 以降 | [`Strata-0.2.1-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA、古いカード、またはドライバーが 580 より古い場合 | [`Strata-0.2.1-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon RX 9000 シリーズまたは Radeon AI PRO R9700 | [`Strata-0.2.1-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon RX 7000 シリーズ | [`Strata-0.2.1-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac**(Apple Silicon、macOS 15 以降) | 内蔵 | [`Strata-0.2.1-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-mac.dmg) |
|
||||
|
||||
**迷ったときは:** NVIDIA の場合、ドライバーがバージョン 580 以降なら `cuda13`、それより古ければ `cuda12` を選びます。`cuda12` は古いカードと古いドライバー向けです。
|
||||
`hip` と `amd` のファイルは AMD のカード用です。それ以外の AMD カード(RX 6000)には、まだ用意されたアプリがありません。
|
||||
[AMD のカード](docs/AMD_HIP.md)を見てください。
|
||||
|
||||
そのあとの手順:
|
||||
|
||||
- **Windows:** `.exe` を実行します。ユーザー単位でインストールされ、管理者権限は不要です。「Windows によってPCが保護されました」と表示されたら、
|
||||
**詳細情報**を選び、**実行**を選んでください(インストーラーにはまだコード署名がありません)。
|
||||
- **Linux:** ファイルを実行可能にして(`chmod +x`)起動します。FUSE 2 が必要です。`sudo apt install libfuse2`(Ubuntu 22.04)
|
||||
または `libfuse2t64`(Ubuntu 24.04 以降)を実行してください。
|
||||
- **Mac:** `.dmg` を開き、Strata を「アプリケーション」にドラッグします。
|
||||
|
||||
最初の画面で、アプリがPCを調べて合うモデルをすすめます。インストールを押してください。モデルは大きく(約40〜70 GB)、中断しても続きからダウンロードできます。
|
||||
ウィンドウを閉じると、Strata とエンジンが止まります。
|
||||
|
||||
### 方法B: ターミナル(1行)
|
||||
|
||||
ターミナル版は `strata` コマンドです。スクリプトやサーバー、AI エージェント向けです。
|
||||
|
||||
Linux と Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
```
|
||||
|
||||
Windows(PowerShell):
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
スクリプトはPCを調べ、ターミナル版をダウンロードし、SHA-256 を検証して、ホームフォルダに展開します。管理者権限は不要です。Windows では、終わったら
|
||||
新しいターミナルを開いてください。そのあと Strata を起動します。
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
質問は1つずつ表示されます。モデル、コンテキスト、画像、接続を許可する相手の順です。そのあと、お使いのグラフィックスカード用のエンジン
|
||||
(署名付きで、使う前にテスト起動します)とモデルをダウンロードして、サーバーを開始します。スクリプトや AI エージェントからは、フラグを使います。
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
実行前にインストールスクリプトを読みたい場合は、先に `curl -fsSL <address> -o install.sh` でダウンロードし、内容を確認してから
|
||||
`sh install.sh` を実行してください。すべての画面とフラグは [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md) にあります。
|
||||
|
||||
### 方法C: AI にセットアップを任せる
|
||||
|
||||
Claude Code、Cursor、Codex、GitHub Copilot などのコーディングアシスタントに、次の文を貼り付けます。
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
AI がグラフィックカード、RAM、ディスクを確認して、PC に合ったモデルを選びます。インストールと起動を行い、
|
||||
アプリとの接続方法も案内します。また、Strata の [MCP サーバー](docs/MCP_SERVER.md) を使えば、
|
||||
AI ツールから Strata のインストール・起動・停止を行えます。
|
||||
AI はグラフィックスカード、RAM、ディスクを調べ、合うモデルを選んで、インストールと起動をし、アプリの接続方法を教えてくれます。
|
||||
AI ツールは [MCP サーバー](docs/MCP_SERVER.md)を通じて、Strata のインストール、起動、停止もできます。
|
||||
|
||||
### 自分でやる
|
||||
自分でビルドしたい方は[ソースからビルド](#ソースからビルド)を見てください。
|
||||
|
||||
[Strata をダウンロード](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip)して展開します(`git clone` でも構いません)。
|
||||
**Windows:** **`START-HERE.bat`** をダブルクリックします。**Linux:** Strata フォルダーで **`./setup.sh`** を実行します。
|
||||
## モデル
|
||||
|
||||
**Mac(Apple シリコン、M1〜M5、macOS 15 以降):** まず一度だけ Apple の Command Line Tools をインストールします(ターミナルで `xcode-select --install`。Xcode 本体は不要です)。次に Strata フォルダーでターミナルを開き、**`./setup.sh`** を実行します。Mac 用エンジンをお使いの Mac 上でビルドし(約 7 分)、同じ質問をしてからモデルをダウンロードして起動します。次回からは `./setup.sh` ですぐに起動します。どのサイズが Mac に合うかと速度:[docs/MAC.md](docs/MAC.md)。
|
||||
アプリと `strata launch` は、メモリ量に合うモデルをすすめます。モデルは1つで、圧縮の度合いが違う複数のサイズがあります。
|
||||
小さいサイズは速く、大きいサイズは少し賢くなります。
|
||||
|
||||
NVIDIA でも AMD でも手順は同じです。インストーラーがカードを検出し、対応するエンジンを用意します。
|
||||
続いて、次の項目を設定します。
|
||||
### メモリが少ないなら Gyro
|
||||
|
||||
- 使用するモデルとサイズ
|
||||
- コンテキストの長さ(モデルが覚えておけるテキストの量)
|
||||
- 画像を読み取る機能を使うかどうか
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)(agentionai 製)は、このモデルをいちばん小さく動かせる方法です。
|
||||
|
||||
各項目で Enter を押すと、推奨設定が選ばれます。設定が終わるとモデル(約 70 GB)をダウンロードし、起動します。
|
||||
ダウンロードが途中で止まった場合は、もう一度実行すると続きから再開できます。起動が完了すると、ブラウザーで
|
||||
`http://127.0.0.1:8080` が開き、Strata の画面が表示されます。
|
||||
| サイズ | 必要なメモリ(RAM + VRAM) | ダウンロード | 内容 |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27.6 GB | 58.5 GB | ほかのサイズが入らないPC向け。品質は IQ2_XS より下がります。 |
|
||||
| **Gyro-M** | 35 GB | 92.0 GB | 品質は IQ3_XXS 程度で、GPUメモリの使用量は約20%少なくなります。12 GBのカードと R9700 では、回答の書き出しが Q2_0 より17〜21%遅くなります。 |
|
||||
|
||||
> **モデルの起動中は、1-3 分ほど PC の動作が遅くなったり、応答しなくなったりすることがあります**(初回は特に時間がかかります)。
|
||||
> Strata が 35-55 GB のデータを RAM に読み込み、その一部をグラフィックカード用に固定(ピン留め)しているためです。
|
||||
> 正常な動作なので、ウィンドウに表示される処理状況を確認しながら、閉じずにお待ちください。
|
||||
Gyro は NVIDIA と AMD のカードで動きます(AMD は実験的)。Intel Arc と Mac では動きません。詳細と測定結果は
|
||||
[docs/GYRO.md](docs/GYRO.md) にあります。`strata launch --model gyro-s` で起動します。
|
||||
|
||||
**次回からは** `START-HERE.bat`(または `./setup.sh`)を実行するだけですぐに起動できます。2 回目以降は、ダウンロード済みのファイルを再度取得する必要はありません。
|
||||
モデルを停止するにはウィンドウを閉じてください。`UPDATE.bat`(`./update.sh`)を使うと、Strata を起動せずに更新できます。
|
||||
更新方法や Docker、複数のカードの使い方、ファイルの保存先、各種オプションについては、[docs/INSTALL.md](docs/INSTALL.md)をご覧ください。
|
||||
### ほかのサイズ
|
||||
|
||||
## どのモデルを選べばいい
|
||||
|
||||
インストーラーが RAM の容量に合ったモデルをおすすめします。同じモデルでも、圧縮率によって複数のサイズが用意されています。
|
||||
サイズが小さいほど高速に動作し、大きいほど回答の品質が少し高くなります。
|
||||
|
||||
| RAM | おすすめのモデル | 選ぶ理由 |
|
||||
| メモリ(RAM + VRAM) | 選ぶモデル | 備考 |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Coder** | 32 GB で動作するコーディング向けモデル(24 GB のカードがあれば Q2_0 と IQ2_XS も動作) |
|
||||
| **48 GB** | **IQ2_XS**(速度優先なら Q2_0) | これより大きいサイズはメモリに収まらない |
|
||||
| **64 GB** | **IQ2_XS**(おすすめ)、または IQ3_XXS / IQ3_S | どのサイズも利用可能。IQ3_S は回答の品質が最も高い一方、速度は最も遅い |
|
||||
| **96 GB 以上** | **IQ3_S**、または Unsloth の UD-IQ4_XS(約 4-bit) | 他のアプリを開いたままでも、最大サイズのモデルを動かせるだけの余裕がある |
|
||||
| **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 より17〜21%遅くなります |
|
||||
| **48 GB** | **IQ3_XXS**(47.0 GB) | 遅くなりますが、品質は上がります |
|
||||
| **64 GB** | **IQ3_S**(54.8 GB)、オリジナルまたは Swift 1.5 | いちばん遅く、いちばん高品質です。公開されているテストでは完全版と同等です |
|
||||
| **96 GB 以上** | Unsloth の **UD-IQ4_XS**(RAM 約72 GB)から **Q8_0**(約136 GB)まで、4〜8ビット | 完全版に近づきます。RAM が足りないと、Strata は一部を SSD から読むため、ずっと遅くなります |
|
||||
|
||||
- **[Coder](docs/MODELS.md#coder):** エキスパートを半分に減らしたコーディング向けモデルです。SWE-bench Verified ではフルモデルのスコアの
|
||||
91% を達成しており(開発者による測定)、32 GB の RAM で動作します。ただし、コード以外の用途では性能が劣り、中国語などの CJK 言語の処理も苦手です(#438)。
|
||||
こうした用途には、すべてのエキスパートを残した Q2_0、IQ2_XS、IQ3_S を選んでください。
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15):** 回答前の思考時間が大幅に短くなるようファインチューニングしたモデルです。
|
||||
回答の品質をほぼ保ったまま、より短い時間で答えが返ってきます。
|
||||
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** Unsloth による約 4-bit のモデルです。回答の品質は IQ3_S と UD-Q4_K_XL の中間です。
|
||||
ダウンロード容量は 94 GB です。RAM が約 80 GB 未満の場合、回答の生成中にモデルの一部を SSD から読み込むため、速度が落ちます(NVMe SSD を使うと改善します)。
|
||||
- **[Unsloth UD-Q4_K_XL / UD-Q5_K_XL / UD-Q6_K_XL / Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):** 4〜8 ビットで、フルモデルにより近いモデルです
|
||||
(次のトークンが Q8_0 と一致する割合は 93〜95%)。すべてのエキスパートが RAM に収まる場合(UD-Q4_K_XL で約 88 GB、
|
||||
Q8_0 で約 136 GB)は高速です。RAM が足りない場合は回答の生成中に一部を SSD から読み込むため、大幅に遅くなります
|
||||
(RAM が 64 GB の PC で UD-Q4_K_XL は毎秒 7-8.5 トークン程度)。
|
||||
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** 手動でのセットアップが必要です。
|
||||
インストーラーのメニューからは選べません。
|
||||
表の行より RAM が少なくても動くことがあります。Strata は自動で[低RAMモード](docs/MODELS.md#a-big-graphics-card-and-little-ram)に切り替わります。
|
||||
速度は落ち、グラフィックスカードが小さいとさらに大きく落ちます。
|
||||
|
||||
各サイズのダウンロード情報や必要なメモリ容量は、[docs/MODELS.md](docs/MODELS.md)をご覧ください。後から別のモデルを追加する場合は、
|
||||
`SETUP.bat`(Linux:`./setup.sh --setup`)を実行してください。
|
||||
そのほかの選択肢:
|
||||
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** はファインチューン版で、答える前に考える時間が短くなります。品質と RAM の必要量はほぼ同じです。
|
||||
- **[NVFP4](docs/NVFP4.md)** は NVIDIA カード向けの NVIDIA 製4ビット版です。すべてのエキスパートを RAM に置くには、約80 GBの RAM が必要です。
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** は、より小さい別のモデルです。画像を読め、複数のリクエストに同時に答えられます。
|
||||
- **[OrcaRouter の Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** は手動で設定します。
|
||||
|
||||
すべてのサイズ、ダウンロード、どの構成で何が動くかは [docs/MODELS.md](docs/MODELS.md) にあります。
|
||||
|
||||
## どのくらい速い?
|
||||
|
||||
12 GBのグラフィックスカード(RTX 5070、Ryzen 5 7600、RAM 64 GB)での数値です。ほかのカードの結果は
|
||||
[各モデルの速度](docs/MODELS.md#how-fast-is-each-size)、[コミュニティの結果](docs/COMMUNITY_BENCHMARKS.md)を見てください。
|
||||
|
||||
| モデル | 回答が出る速さ | プロンプトを読む速さ |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████████` 94 tokens/s | `████████████` 2,650 tokens/s |
|
||||
| **IQ2_XS** | `██████████░░` 79 tokens/s | `█████████░░░` 2,090 tokens/s |
|
||||
| **IQ3_XXS** | `████████░░░░` 62 tokens/s | `████████░░░░` 1,750 tokens/s |
|
||||
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1,620 tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2,180 tokens/s |
|
||||
|
||||
1トークンは英単語の約¾にあたるので、毎秒60トークンは読む速さより速くなります。「プロンプトを読む速さ」は、長い文書やコード、チャット履歴を Strata が取り込む速さです。
|
||||
|
||||
16 GBの AMD カード(RX 9070 XT、Ryzen 9 3900X、RAM 47 GB)での測定値:
|
||||
|
||||
| モデル | 回答が出る速さ | プロンプトを読む速さ |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████░░░░` 60 tokens/s | `█████░░░░░░░` 1,160 tokens/s |
|
||||
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1,110 tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1,420 tokens/s |
|
||||
|
||||
バーは2つの表で同じ目盛りです。VRAM が多いカードほど速くなり、RTX 3090(24 GB)なら毎秒100〜140トークンほどになるはずです。各数値のエンジンのバージョンは
|
||||
[DETAILS.md](docs/DETAILS.md#speed-measured) にあります。
|
||||
|
||||
## 使い方
|
||||
|
||||
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="Strata の Monitor タブ(左)と、コードを生成するコーディングエージェント(右)"><br>
|
||||
<sub>動画に登場する五重塔の庭を作成中の画面。左が Strata の <b>Monitor</b>、右がコードを生成しているコーディングエージェント</sub></p>
|
||||
### アプリで
|
||||
|
||||
- **ブラウザーで使う:** `http://127.0.0.1:8080` を開きます。チャット用の **Chat**、モデルや GPU/CPU/RAM の状態をリアルタイムで確認できる **Monitor**、
|
||||
設定や接続先のアドレスを確認できる **About** の各タブがあります。
|
||||
- **アプリやコーディングエージェントと連携する:** 「OpenAI-compatible」プロバイダーを追加し、ベース URL に
|
||||
**`http://127.0.0.1:8080/v1`** を指定します。API キーとモデル名には任意の値を指定できます。
|
||||
- Anthropic API を使うアプリ:`http://127.0.0.1:8080/v1/messages`(Claude Code では
|
||||
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080` を設定)。
|
||||
- Codex CLI など、OpenAI Responses API を使うアプリ:`/v1/responses`
|
||||
([設定方法](docs/DETAILS.md#the-responses-api-and-codex-cli))。
|
||||
- **思考(Thinking)の設定:** チャットのメニュー、または連携先アプリの「reasoning effort」で **off、low、medium、high** から選びます。
|
||||
off が最も速く、難しい質問には high が向いています。
|
||||
- **画像を読み取る:** セットアップ時に「Images?」で yes を選びます。その後、チャットの **Picture** ボタンをクリックするか、連携先のアプリで画像を添付してください。
|
||||
AMD のカードを使う場合、Linux では CPU で画像を読み取ります。Windows ではまだ画像の読み取りに対応していません。
|
||||
- **スマートフォンや別の PC から使う:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>` で起動します。API キーは必ず設定してください。
|
||||
- **リクエストは 1 つずつ:** 標準設定では 1 つずつ処理し、それ以外のリクエストは順番を待ちます。同時に処理するには、
|
||||
`"parallel": 2` を設定してください([BATCHING.md](docs/BATCHING.md))。12 GB のカードでは、リクエストごとの回答速度が落ちます。
|
||||
- **長いプロンプトを送る:** チャットの最初のメッセージは全文を読み込むため、30,000 トークンあたり約 1 分かかります。
|
||||
2 通目以降は数秒で応答が始まります。
|
||||
アプリには、**チャット**、モデルとGPU・CPU・RAMの状態を見る**モニター**、**モデル**ページ、**設定**があります。チャットはPC上のフォルダで作業できます。
|
||||
**手動**モードでは、ファイルを変更したりコマンドを実行したりする前に確認します。`/loop 10m <やること>` と入力すると、チャットを開いている間、メッセージを10分ごとに繰り返し送ります。
|
||||
|
||||
[チャットの保存場所](docs/INSTALL.md#where-things-are-stored)や[API の詳細](docs/DETAILS.md#using-it)も参照してください。
|
||||
チャットは DuckDuckGo(または Bing)でウェブ検索ができ、アカウントは不要です。**設定、ツール**で検索キーを追加することもできます。
|
||||
検索は、PCの外に何かを送る唯一の機能で、送られるのは検索語です。
|
||||
|
||||
### オプション:モデル全体が RAM に収まる場合、GPU 1 枚でさらに高速化
|
||||
2つのモデルを動かすには、**複数のモデルを実行**をオンにして、それぞれのモデルに同じメーカーのカードを1枚ずつ割り当てます。
|
||||
|
||||
2 つのオプトイン設定です(RubenTorney 氏による #1904 と #1905)。GPU を 1 枚使い、RAM にすべてのエキスパートが収まる場合(Strata の arena モード。例:64 GB の PC で Q2_0 や IQ3_XXS)に効果があります。Strata のデータフォルダーにある設定ファイル `strata-<model>.json`([場所](docs/INSTALL.md#where-things-are-stored))を開き、次を追加します:
|
||||
<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>
|
||||
|
||||
- **生成の高速化:** `"args"` リストに `"--adapt-async", "1"`。エキスパートキャッシュの更新が回答を止めずに補助スレッドで行われます。RTX 5070(Windows、Q2_0)での実測:生成 +4〜5%、コードや長文では +7〜10%。回答はデフォルトとわずかに異なる場合があります(実行ごとには同じです)。
|
||||
- **Windows でのプロンプト読み込みの高速化:** `"env"` ブロックに `"STRATA_PIN_BY_NEED": "1"`。Windows はGPU 用に RAM の約半分しか固定できないため、この設定で GPU が本当に必要とするエキスパートから先に固定します。RTX 5070 での実測:プロンプト読み込み +4.4%。
|
||||
### ターミナルで
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<sub>モデルが返信を書いている間の <code>strata monitor</code>。速度、GPU、メモリ、最近のリクエストが表示されます</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # ガイド付きの画面: モデル、コンテキスト、画像、アクセス、そして開始
|
||||
strata launch --model q2_0 --yes --no-tui # 質問なし: 開始し、アドレスを表示して戻る
|
||||
strata launch --plan --model q2_0 # 実行内容を表示するだけで、何も変更しない
|
||||
strata launch --status # 今、何が動いているか
|
||||
strata launch --stop # 停止する
|
||||
strata monitor --url http://127.0.0.1:8080 # 速度、GPU、メモリ、リクエストを見る
|
||||
strata update --check # 新しいエンジンがあるか確認(strata update でインストール)
|
||||
strata doctor # インストール済みのエンジンファイルを調べる
|
||||
```
|
||||
|
||||
`strata --help` ですべてのコマンドを確認できます。起動とサーバーの稼働では、何もダウンロードしません。ダウンロードするのは、インストール、
|
||||
`strata launch`(モデルを選んだとき)、`strata update` だけです。スマートフォンや別のPCから Strata に接続するには、
|
||||
`strata launch --access lan` を使います。API キーが作られ、キーがない状態ではネットワーク上で起動しません。`--open` を付けない限り、ブラウザは開きません。
|
||||
ガイド: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md)。クライアント全体: [docs/TERMINAL.md](docs/TERMINAL.md)。
|
||||
|
||||
### AI エージェントと使う
|
||||
|
||||
エージェントは、方法Bの1行コマンドで Strata をインストールし、
|
||||
`strata launch --model NAME --yes --no-tui` を実行します。手順、フラグ、終了コードは [docs/AI_SETUP.md](docs/AI_SETUP.md) に、
|
||||
リポジトリのルールは [AGENTS.md](AGENTS.md) にあります。
|
||||
|
||||
### ほかのアプリをつなぐ
|
||||
|
||||
| アプリの種類 | ベース URL |
|
||||
| --- | --- |
|
||||
| OpenAI 互換(ほとんどのアプリとエージェント) | `http://127.0.0.1:8080/v1`(API キーもモデル名も何でも可) |
|
||||
| Anthropic API(Claude Code など) | `http://127.0.0.1:8080`、`/v1` は付けません。これらのアプリは `/v1/messages` を自分で付けます。Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API(Codex CLI) | `http://127.0.0.1:8080/v1/responses`([設定方法](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` は、OpenCode、Codex CLI、Claude Code、oh-my-pi、pi、Hermes Agent、OpenClaw の正確な設定を表示します。
|
||||
`strata connect opencode --write` は、確認したうえでファイルを書き込み、バックアップを残します。
|
||||
|
||||
### 画像
|
||||
|
||||
画像を読めるモデルでは、画像は初期設定でオンです。チャットに画像を添付または貼り付けるか、アプリから送ってください。
|
||||
グラフィックスカードが24 GB未満のときは、画像リーダーがプロセッサで動きます(画像1枚につき数秒、カードのメモリは使いません)。24 GB以上のときはグラフィックスカードで動きます。
|
||||
このルールは NVIDIA と AMD で同じです。画像はモデルごとにオフにでき、`--vision off`、`--vision gpu`、`--vision cpu` でリーダーを選べます。
|
||||
|
||||
このルールの根拠となる数値です。Radeon AI PRO R9700 では、カードのリーダーは260トークンの画像に57 ms、プロセッサのリーダーは3.1秒かかりました。
|
||||
16 GBのカード(Tesla P100 と RTX A4000、Linux)では、カードのリーダーを使うとテキストの速度が3〜5%下がりました。
|
||||
本来モデルが使う約1.4 GBをリーダーが占めるためです。Mac ではメモリ量にかかわらず GPU が読み取ります(32 GB の M4 で260トークンの画像に0.45秒、共有メモリを約1.2 GB使います)。プロセッサでは1枚あたり約50秒かかるため、`--vision cpu` を指定したときだけ使います。
|
||||
|
||||
### 知っておくと便利なこと
|
||||
|
||||
- 思考のレベル(**off、low、medium、high**)は、チャットのメニューか、お使いのアプリの「reasoning effort」で選びます。off がいちばん速くなります。
|
||||
- Strata は初期設定では、一度に1つのリクエストに答えます。同時に複数処理するには、`strata launch` で `--parallel N` を使うか、設定に
|
||||
`"parallel": 2` を書きます([BATCHING.md](docs/BATCHING.md))。12 GBのカードでは、1つずつの回答が遅くなります。
|
||||
- Strata はチャットの最初のメッセージを全文読みます。30,000トークンあたり約1分かかります。続きのメッセージは数秒で始まります。
|
||||
- チャットの保存場所と API 全体: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored)、[docs/DETAILS.md](docs/DETAILS.md#using-it)。
|
||||
|
||||
## ソースからビルド
|
||||
|
||||
<details>
|
||||
<summary>リポジトリから Windows、Linux、Mac 用にビルドする</summary>
|
||||
|
||||
[Strata をダウンロード](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip)して展開するか、`git clone` します。
|
||||
**Windows:** `START-HERE.bat` をダブルクリックします。**Linux:** Strata フォルダで `./setup.sh` を実行します。
|
||||
|
||||
**Mac(Apple Silicon、macOS 15 以降):** 最初に一度、`xcode-select --install` で Apple のコマンドラインツールをインストールします
|
||||
(Xcode 本体は不要です)。Strata フォルダでターミナルを開き、`./setup.sh` を実行します。Mac 用エンジンをお使いの Mac 上でビルドし
|
||||
(約7分)、同じ質問をして、モデルをダウンロードし、起動します。2回目以降は `./setup.sh` ですぐに起動します。
|
||||
お使いの Mac に合うサイズは [docs/MAC.md](docs/MAC.md) を見てください。
|
||||
|
||||
インストーラーはカードを見つけて、合うエンジンを設定します。どのモデルとサイズにするか、コンテキストをどれだけにするか(モデルが一度に覚えておけるテキストの量)、
|
||||
画像を読ませるかを聞かれます。おすすめの答えでよければ Enter を押します。そのあとモデル(約70 GB)をダウンロードして起動します。
|
||||
ダウンロードが止まったら、もう一度実行すると続きから再開します。ブラウザで `http://127.0.0.1:8080` の Strata ページが開きます。
|
||||
|
||||
> モデルの起動中は、PCが1〜3分ほど(初回がいちばん長い)遅くなったり、応答しなくなったりすることがあります。Strata は35〜55 GBをRAMに読み込み、
|
||||
> その一部をグラフィックスカード用に固定するためです。ウィンドウを閉じずに待ってください。
|
||||
|
||||
次回からは `START-HERE.bat`(または `./setup.sh`)をもう一度実行するだけです。すぐに起動し、同じものを2回ダウンロードすることはありません。ウィンドウを閉じるとモデルが止まります。
|
||||
`UPDATE.bat`(`./update.sh`)は、起動せずに Strata を更新します。更新、Docker、複数のカード、ファイルの保存先、すべてのオプションは
|
||||
[docs/INSTALL.md](docs/INSTALL.md) にあります。
|
||||
|
||||
PCの性能を測るには、`BENCH.bat`(`./bench.sh`)を実行します。5〜10分かかり、共有できるレポートが作られます
|
||||
([方法](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark))。
|
||||
|
||||
**オプション: モデル全体がRAMに入るとき、1枚のカードをさらに速くする。** RubenTorney による、自分でオンにする2つの設定です(#1904、#1905)。
|
||||
グラフィックスカードが1枚で、RAM がすべてのエキスパートを保持できるときに効果があります。たとえば64 GBのPCでの Q2_0 や IQ3_XXS です。
|
||||
Strata のデータフォルダにある `strata-<model>.json`([場所](docs/INSTALL.md#where-things-are-stored))を開いて、次を追加します。
|
||||
|
||||
- `"args"` のリストに `"--adapt-async", "1"` を追加します。エキスパートキャッシュが、回答を止めずに補助スレッドで更新されます。
|
||||
RTX 5070(Windows、Q2_0)での測定: デコードが4〜5%、コードや長い文書では7〜10%速くなりました。回答は初期設定と少し違うことがありますが、実行ごとの結果は変わりません。
|
||||
- `"env"` ブロックに `"STRATA_PIN_BY_NEED": "1"` を追加します(Windows)。Windows ではグラフィックスカード用に固定できる RAM が半分ほどしかありません。
|
||||
この設定は、カードが最初に必要とするエキスパートを優先して固定します。RTX 5070 での測定: プロンプトの読み取りが4.4%速くなりました。
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
変更後は Strata を再起動してください(`START-HERE.bat` または `./setup.sh`)。元に戻すには、2 つの項目を削除します。
|
||||
変更したら Strata を再起動してください。元に戻すには、2つの項目を削除します。
|
||||
|
||||
</details>
|
||||
|
||||
## 仕組み
|
||||
|
||||
このようなモデルは、ふつう数百GBのグラフィックスメモリを持つサーバーで動きます。お使いのグラフィックスカードは12〜24 GBです。Strata は、PC全体で作業を分担して、モデルを収めます。
|
||||
図書館にたとえてみます。
|
||||
|
||||
- モデルは、24,576人の小さな専門家(「エキスパート」)からなるチームです。24,576冊の参考書のようなものです。1つの単語に必要なのは、そのうち10冊だけです。
|
||||
- **グラフィックスカードは机です。** 小さくて速く、最もよく使う約4,000冊が開いて置いてあります。
|
||||
- **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>
|
||||
|
||||
小さな助手が、次の数単語も予想します。大きなモデルがそれをまとめて確認し、正しいものだけを残します。答えは同じで、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>
|
||||
|
||||
詳しい説明は [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md) にあります。各部分と数値は
|
||||
[詳細](docs/DETAILS.md#how-it-works)と[論文](docs/paper/Strata-Paper.pdf)を見てください。
|
||||
|
||||
## うまくいかないとき
|
||||
|
||||
- **初回起動時に PC が応答しなくなる。** モデルの読み込み中は、一時的に応答しなくなることがあります。ウィンドウを閉じずにお待ちください。
|
||||
10 分たっても応答しない場合は、PC を再起動して他のアプリを閉じてからやり直すか、モデルのサイズを小さくしてください。
|
||||
- **ダウンロードやインストールが途中で止まる。** `START-HERE.bat`(または `./setup.sh`)をもう一度実行してください。
|
||||
中断したところから再開できます。
|
||||
- **動作が極端に遅く、ディスクのアクセスランプが点滅し続ける。または「the engine stopped unexpectedly」と表示される。** RAM の空き容量が不足しています。
|
||||
ブラウザーなど、RAM を多く使う他のアプリを閉じるか、小さいモデル(Q2_0 または IQ2_XS)を選んでください。
|
||||
- **ポート 8080 が使用中と表示される。** Strata がすでに起動しています。起動済みのウィンドウを探してください。
|
||||
- **Strata を初めて起動したときに、PCが固まりました。** モデルを RAM に読み込んでいます。ウィンドウを閉じずに待ってください。10分たっても固まったままなら、
|
||||
PCを再起動し、ほかのプログラムを閉じてからもう一度試すか、小さいサイズを選んでください。
|
||||
- **ダウンロードやインストールの途中で止まりました。** アプリか `strata launch` をもう一度起動するか、`START-HERE.bat`(または
|
||||
`./setup.sh`)をもう一度実行してください。止まったところから再開します。
|
||||
- **とても遅い、ディスクのランプが点滅し続ける、または「エンジンが予期せず停止しました」と表示される。** PCの空き RAM が足りません。
|
||||
ほかのプログラムを閉じるか(ブラウザはメモリを多く使います)、Q2_0 や IQ2_XS のような小さいサイズを選んでください。
|
||||
- **このPCに合うエンジンがないと表示される。** アプリでは、**設定、エンジン**を開いて**エンジンを更新**を押します。ターミナルでは
|
||||
`strata update` を実行します。`strata doctor` で、どのエンジンファイルがインストールされているか分かります。
|
||||
- **ポート 8080 はすでに使われていると表示される。** Strata がすでに動いています。ウィンドウを探してください。
|
||||
|
||||
その他の問題と対処方法は、[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)をご覧ください。解決しない場合は
|
||||
[Issue](https://github.com/Niko1221/Strata/issues) を作成し、Strata フォルダー内の `strata-<model>.log` を添付してください。
|
||||
セキュリティ上の問題は、[SECURITY.md](SECURITY.md)の手順に従って非公開で報告してください。
|
||||
|
||||
## しくみ
|
||||
|
||||
この規模のモデルは通常、数百ギガバイトの GPU メモリを搭載したサーバーで動かします。一方、手元のグラフィックカードのメモリは
|
||||
12-24 GB 程度です。Strata は **PC 全体で処理を分担する** ことで、こうした環境でもモデルを動かせるようにしています。
|
||||
台所で、よく使うものは調理台に置き、それ以外は食品庫にしまっておくような仕組みです。
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="24,576 個のエキスパートのうち、よく使うものをグラフィックカードに配置。全エキスパートは RAM に、ルックアップテーブルは SSD に保持"></p>
|
||||
|
||||
- **モデルは「エキスパート」と呼ばれる 24,576 個の小さな処理単位から構成されています。** 1 語の生成に必要なのは、このうち 10 個だけです。
|
||||
- **グラフィックカード** に、使用頻度の高い数千個のエキスパートを置きます。**RAM** にはすべてのエキスパートを保持し、
|
||||
**CPU** が残りの処理を並行して担当します。**SSD** には大きなルックアップテーブルを保存します。
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="小さな補助モデルが次の数語を予測し、大きなモデルがまとめて検証。正しい予測だけを採用する"></p>
|
||||
|
||||
- **予測してから検証:** 小さな補助モデルが次の数語を予測し、大きなモデルがまとめて検証します。
|
||||
回答を変えずに、生成速度を 1.6-1.8 倍に高められます。
|
||||
- **長いテキストはまとめて読み込みます**(一度に最大 8,192 トークン)。読み込み速度は毎秒 1,000 トークンを超えます。
|
||||
|
||||
仕組みについては、[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)で詳しく説明しています。各処理の詳細や測定値は、
|
||||
[技術資料](docs/DETAILS.md#how-it-works)と[論文](docs/paper/Strata-Paper.pdf)をご覧ください。
|
||||
そのほかの問題と対処法は [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) にあります。それでも解決しない場合は、
|
||||
[issue](https://github.com/Niko1221/Strata/issues) を作成してください。アプリでは、**設定、エンジン、診断、ログを見る、ログを保存**で作られるファイルを添付します。
|
||||
ターミナルでは、`strata doctor` の出力を貼り付けます。ソースから動かしている場合は、Strata フォルダの `strata-<model>.log` を添付してください。
|
||||
セキュリティ上の問題は、非公開で報告してください: [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)、Unsloth が提供する圧縮版を利用しています。
|
||||
Strata は [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) の一部も使用しています。クレジットの一覧は
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits)をご覧ください。Strata は [MIT License](LICENSE) で公開されていますが、一部の構成要素と
|
||||
各モデルには、それぞれ別のライセンスが適用されます([ライセンスの詳細](docs/HOW_IT_WORKS.md#license))。
|
||||
モデルは Qwen チームの [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next) です。圧縮は
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF)、UkisAI(Swift 1.5)、agentionai(Gyro)、
|
||||
Unsloth が行いました。Strata は [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) の一部を使っています。すべてのクレジットは
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits) にあります。Strata は [MIT ライセンス](LICENSE)のオープンソースです。一部のコンポーネントと
|
||||
すべてのモデルには、それぞれのライセンスがあります([該当するもの](docs/HOW_IT_WORKS.md#license))。
|
||||
|
||||
## Strata を支援する
|
||||
## コントリビューション
|
||||
|
||||
Strata は無料で使えるオープンソースソフトウェアです。役に立ったら、開発へのご支援をお願いします。
|
||||
バグを見つけた、変更やベンチマークを送りたい、という方は、先に [CONTRIBUTING.md](CONTRIBUTING.md) を読んでください。
|
||||
ほかの人があなたの成果を確認できるように、何を書くべきかが説明してあります。
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a></p>
|
||||
## Strata を応援する
|
||||
|
||||
Strata は無料で、MIT ライセンスです。私が入手できるハードウェアで開発とテストをしています。お使いのPCで快適に動いたなら、コーヒー1杯分の支援は、
|
||||
新しいカードのテストと、報告いただいた不具合の修正を続ける助けになります。無理のない範囲で大丈夫です。使って、気づいたことを報告してもらえるだけでも同じように助かります。
|
||||
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
|
||||
|
||||
Strata のスポンサーに関心がある方は、[GitHub](https://github.com/Niko1221) から連絡してください。
|
||||
@@ -1,243 +1,396 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
**English** · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md)
|
||||
<p align="center"><b>English</b> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<p align="center"><b>Run a 125-billion-parameter AI model on your own gaming PC</b><br>
|
||||
NVIDIA or AMD graphics card (12 GB or more) · Windows or Linux · free and open source</p>
|
||||
<h3 align="center">Run a state-of-the-art model on your own gaming PC</h3>
|
||||
|
||||
<p align="center">Support for NVIDIA, AMD, Intel · Windows, Linux, Mac · free and open source</p>
|
||||
|
||||
<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>
|
||||
</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)** on a normal PC. This is a
|
||||
large, smart AI model that usually needs a server. It chats, writes code, reads pictures and works with your apps
|
||||
and coding agents. Nothing leaves your PC.
|
||||
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.
|
||||
|
||||
## How fast is it?
|
||||
**New here?** Check [what you need](#what-you-need), then [install the app](#option-a-the-strata-app).
|
||||
|
||||
We measured it on two ordinary gaming PCs. A token is about ¾ of a word.
|
||||
- **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).
|
||||
|
||||
- **Writes answers:** how fast the reply appears in a short chat. 60 tokens per second is faster than you can read.
|
||||
- **Reads your prompt:** how fast it takes in what you send (here a 32K-token document, code or chat history).
|
||||
|
||||
<table>
|
||||
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB RAM</th></tr>
|
||||
<tr><td>
|
||||
|
||||
| Size | Writes answers | Reads your prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
|
||||
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
|
||||
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
|
||||
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
|
||||
| **Coder** | 55 tokens/s | 2,180 tokens/s |
|
||||
|
||||
</td><td>
|
||||
|
||||
| Size | Writes answers | Reads your prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
|
||||
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
|
||||
| **Coder** | 44 tokens/s | 1,420 tokens/s |
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
NVIDIA: Q2_0 with engine 0.1.36, the other rows with 0.1.26 (4K answers, 32K prompts). The full tables are in
|
||||
[DETAILS.md](docs/DETAILS.md#speed-measured). A card with more VRAM is faster: an RTX 3090 (24 GB) should write
|
||||
about 100-140 tokens per second. Long chats and other cards: [speed of each model](docs/MODELS.md#how-fast-is-each-size),
|
||||
[community results](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
|
||||
<sub>Strata is free. If it runs well on your PC, a coffee keeps the work on it going.</sub></p>
|
||||
Version 0.2.1 · engine 0.1.46
|
||||
|
||||
## 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.
|
||||
|
||||
- A 32 GB PC with a 12-16 GB card runs the Coder.
|
||||
- 64 GB of RAM runs every size.
|
||||
|
||||
| Your computer | Details |
|
||||
| --- | --- |
|
||||
| **Graphics card** | **NVIDIA** GeForce RTX 20, 30, 40 or 50 series, or **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 or RX 6800 / 6900 series. It needs **12 GB of VRAM or more**. |
|
||||
| **RAM** | 32 GB or more. Your RAM decides [which model](#which-model-should-i-pick) fits. 64 GB runs every size. |
|
||||
| **Disk** | About 80 GB free. Use an SSD if you can: the first start is much faster. |
|
||||
| **System** | Windows 10 / 11 or Linux, and a current graphics driver from NVIDIA or AMD. |
|
||||
| **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) |
|
||||
|
||||
The installer sets up everything else. Two or three cards can share the model ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
Also needed:
|
||||
|
||||
Experimental, written and tested by community members on their own machines:
|
||||
- about 80 GB of free disk (an SSD makes the first start much faster)
|
||||
- Windows 10 / 11, Linux or macOS
|
||||
- a current graphics driver
|
||||
|
||||
- **Older graphics cards** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Older GPUs](docs/OLDER_GPUS.md).
|
||||
- **Intel Arc**, built from source on Linux: [Intel Arc](docs/INTEL_ARC.md).
|
||||
- **Apple Silicon Macs (M1 to M5)**, built from source by `./setup.sh` (the Metal engine): [Mac](docs/MAC.md).
|
||||
- **AMD Ryzen AI Max (Strix Halo)**, built from source on Linux: [Strix Halo](docs/STRIX_HALO.md).
|
||||
- **Older processors without AVX2**: they work, but slowly. [Older CPUs](docs/INSTALL.md#older-cpus-experimental).
|
||||
|
||||
The full list: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
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).
|
||||
|
||||
## Install
|
||||
|
||||
### Let your AI set it up
|
||||
Pick one option. They all start the same engine and use the same models.
|
||||
|
||||
Do you use an AI coding assistant (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Paste this into it:
|
||||
### Option A: the Strata app
|
||||
|
||||
The easiest way. The app has the engine inside: install it, pick a model, chat. Download the file for your system and
|
||||
graphics card from the [releases page](https://github.com/Niko1221/Strata/releases/latest). Not sure which one? See the
|
||||
line under the table.
|
||||
|
||||
| System | Your graphics card | File to download |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, driver 580 or newer | [`Strata-0.2.1-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda13.exe) |
|
||||
| | NVIDIA, older card or driver older than 580 | [`Strata-0.2.1-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda12.exe) |
|
||||
| | AMD Radeon RX 9000 series or Radeon AI PRO R9700 | [`Strata-0.2.1-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip.exe) |
|
||||
| | AMD Radeon RX 7000 series or Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, driver 580 or newer | [`Strata-0.2.1-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, older card or driver older than 580 | [`Strata-0.2.1-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon RX 9000 series or Radeon AI PRO R9700 | [`Strata-0.2.1-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon RX 7000 series | [`Strata-0.2.1-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 or newer) | built in | [`Strata-0.2.1-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-mac.dmg) |
|
||||
|
||||
**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. Other AMD cards (RX 6000) have no
|
||||
ready-made app yet: see [AMD cards](docs/AMD_HIP.md).
|
||||
|
||||
Then:
|
||||
|
||||
- **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.
|
||||
|
||||
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.
|
||||
|
||||
### Option B: the Terminal (one line)
|
||||
|
||||
The Terminal is the `strata` command, for scripts, servers and AI agents.
|
||||
|
||||
Linux and Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
The script checks your PC, downloads the Terminal, verifies its SHA-256 and unpacks it into your home folder. It needs no
|
||||
administrator rights. On Windows, open a new terminal window afterwards. Then start Strata:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
It asks one question at a time: model, context, pictures, who may connect. Then it downloads the engine for your graphics
|
||||
card (signed, and test-started before use) and the model, and starts serving. For scripts and AI agents, use flags:
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
To read the install script before it runs, download it first with `curl -fsSL <address> -o install.sh`, read it, then run
|
||||
`sh install.sh`. All screens and flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### Option C: let your AI set it up
|
||||
|
||||
Paste this into Claude Code, Cursor, Codex, GitHub Copilot or another coding assistant:
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
It checks your graphics card, RAM and disk and picks the model that fits. Then it 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 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).
|
||||
|
||||
### Or do it yourself
|
||||
Prefer to build it yourself? See [Build from source](#build-from-source).
|
||||
|
||||
[Download Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) and unzip it (or `git clone` it).
|
||||
**Windows:** double-click **`START-HERE.bat`**. **Linux:** run **`./setup.sh`** in the Strata folder.
|
||||
## Models
|
||||
|
||||
**Mac (Apple Silicon, M1 to M5, macOS 15 or newer):** install Apple's Command Line Tools once
|
||||
(`xcode-select --install` in Terminal; Xcode itself is not needed). Then open Terminal in the Strata folder and run
|
||||
**`./setup.sh`**. It builds the Mac engine on your Mac (about 7 minutes), asks the same questions, downloads the model
|
||||
and starts it. Next time, `./setup.sh` starts it right away. Which size fits your Mac and how fast it is:
|
||||
[docs/MAC.md](docs/MAC.md).
|
||||
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 steps are the same for NVIDIA and AMD. The installer finds your card and sets up the right engine for it. It
|
||||
asks you a few questions:
|
||||
### Low on memory? Try Gyro
|
||||
|
||||
- which model and which size,
|
||||
- how much context (how much text the model keeps in mind),
|
||||
- whether it should read pictures.
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (by agentionai) is the smallest way to run this model.
|
||||
|
||||
Press Enter each time for the recommended answer. Then it downloads the model (about 70 GB) and starts it. If the
|
||||
download stops, run it again: it continues where it left off. Your browser opens the Strata app at
|
||||
`http://127.0.0.1:8080`.
|
||||
| Size | Memory it needs (RAM + VRAM) | Download | What it is |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27.6 GB | 58.5 GB | For PCs that fit nothing else. Quality is lower than IQ2_XS. |
|
||||
| **Gyro-M** | 35 GB | 92.0 GB | About IQ3_XXS quality with about 20% less GPU memory. It writes answers 17-21% slower than Q2_0 on a 12 GB card and on an R9700. |
|
||||
|
||||
> **While the model starts, your PC can be slow or stop responding for 1-3 minutes** (longest the first time).
|
||||
> Strata loads 35-55 GB into your RAM and locks part of it for the graphics card. This is normal. Wait, and don't
|
||||
> close the window. The window shows what Strata is doing.
|
||||
Gyro runs on NVIDIA and AMD cards (AMD is experimental), not on Intel Arc or the Mac. Details and measurements:
|
||||
[docs/GYRO.md](docs/GYRO.md). Start it with `strata launch --model gyro-s`.
|
||||
|
||||
**Next time**, run `START-HERE.bat` (or `./setup.sh`) again. It starts right away and downloads nothing twice. Close
|
||||
its window to stop the model. `UPDATE.bat` (`./update.sh`) updates Strata without starting it. Updating, Docker,
|
||||
several cards, where the files go and every option: [docs/INSTALL.md](docs/INSTALL.md).
|
||||
### The other sizes
|
||||
|
||||
**Is it fast on your PC?** Run `BENCH.bat` (`./bench.sh`): about 5-10 minutes, it measures chat, 4K/16K prompts and an agent-style session with your own settings and writes a report to share ([how](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
|
||||
|
||||
## Which model should I pick?
|
||||
|
||||
The installer recommends one for your RAM. The same model comes in several sizes, compressed more or less. Smaller
|
||||
sizes are faster. Larger sizes are a bit smarter.
|
||||
|
||||
| Your RAM | Take | Why |
|
||||
| Your memory (RAM + VRAM) | Take | Notes |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Coder** | it fits 32 GB, and it is made for code (with a 24 GB card, Q2_0 and IQ2_XS run too) |
|
||||
| **48 GB** | **IQ2_XS** (or Q2_0, the fastest) | the larger sizes do not fit |
|
||||
| **64 GB** | **IQ2_XS** (recommended), or IQ3_XXS / IQ3_S | every size fits; IQ3_S is the best and the slowest |
|
||||
| **96 GB or more** | **IQ3_S**, or Unsloth's UD-IQ4_XS (~4-bit) | room for the largest sizes with everything else open |
|
||||
| **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 17-21% slower than Q2_0 |
|
||||
| **48 GB** | **IQ3_XXS** (47.0 GB) | Slower, better |
|
||||
| **64 GB** | **IQ3_S** (54.8 GB), the original or Swift 1.5 | The slowest, the best: it matches the full model on the published tests |
|
||||
| **96 GB or more** | Unsloth's **UD-IQ4_XS** (about 72 GB of RAM) to **Q8_0** (about 136 GB), 4 to 8 bit | Closer to the full model. With less RAM, Strata reads part of them from the SSD, which is much slower |
|
||||
|
||||
- **[Coder](docs/MODELS.md#coder):** a coding version with half of the experts removed. It reaches 91% of the full
|
||||
model's SWE-bench Verified score (measured by its authors) and fits 32 GB of RAM. It is weaker outside code,
|
||||
including Chinese and other CJK text (#438). For those, take Q2_0, IQ2_XS or IQ3_S, which keep every expert.
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15):** a fine-tune that thinks for a much shorter time before it answers. You
|
||||
get the answer sooner, at about the same quality.
|
||||
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** Unsloth's ~4-bit version, between IQ3_S and
|
||||
UD-Q4_K_XL in quality. A 94 GB download. With less than ~80 GB of RAM, Strata reads part of it from the SSD
|
||||
while it answers, so it is slower there (an NVMe SSD helps).
|
||||
- **[Unsloth UD-Q4_K_XL, UD-Q5_K_XL, UD-Q6_K_XL and Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):**
|
||||
4- to 8-bit, closer to the full model (93-95% the same next token as Q8_0). Fast where every expert fits in RAM
|
||||
(~88 GB for UD-Q4_K_XL up to ~136 GB for Q8_0); with less RAM Strata reads part of them from the SSD while it
|
||||
answers, which is much slower (7-8.5 tokens/s for UD-Q4_K_XL on a 64 GB PC).
|
||||
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** you set it up by hand. It is
|
||||
not in the installer's menu.
|
||||
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.
|
||||
|
||||
Sizes, downloads and what fits where: [docs/MODELS.md](docs/MODELS.md). To add another model later, run
|
||||
`SETUP.bat` (Linux: `./setup.sh --setup`).
|
||||
More choices:
|
||||
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** is a fine-tune that thinks for a shorter time before it answers, at about the same quality and RAM.
|
||||
- **[NVFP4](docs/NVFP4.md)** is NVIDIA's 4-bit version for NVIDIA cards. It needs about 80 GB of RAM to keep every expert in RAM.
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** is a smaller, different model that reads pictures and can answer several requests at once.
|
||||
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** is set up by hand.
|
||||
|
||||
Every size, download and what fits where: [docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
## How fast is it?
|
||||
|
||||
Based on a 12 GB graphics card (RTX 5070, Ryzen 5 7600, 64 GB RAM). See results from other cards:
|
||||
[speed of each model](docs/MODELS.md#how-fast-is-each-size), [community results](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
| Model | Answers appear at | Reads your prompt at |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████████` 94 tokens/s | `████████████` 2,650 tokens/s |
|
||||
| **IQ2_XS** | `██████████░░` 79 tokens/s | `█████████░░░` 2,090 tokens/s |
|
||||
| **IQ3_XXS** | `████████░░░░` 62 tokens/s | `████████░░░░` 1,750 tokens/s |
|
||||
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1,620 tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2,180 tokens/s |
|
||||
|
||||
A token is about ¾ of a word, so 60 tokens per second is faster than you can read. "Reads your prompt" is how fast Strata
|
||||
takes in a long document, code or chat history.
|
||||
|
||||
Measured on a 16 GB AMD card (RX 9070 XT, Ryzen 9 3900X, 47 GB RAM):
|
||||
|
||||
| Model | Answers appear at | Reads your prompt at |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████░░░░` 60 tokens/s | `█████░░░░░░░` 1,160 tokens/s |
|
||||
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1,110 tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1,420 tokens/s |
|
||||
|
||||
The bars use one scale for both tables. A card with more VRAM is faster: an RTX 3090 (24 GB) should write about 100-140
|
||||
tokens per second. The engine version behind each number is in [DETAILS.md](docs/DETAILS.md#speed-measured).
|
||||
|
||||
## Using it
|
||||
|
||||
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="The Strata app's Monitor tab next to a coding agent"><br>
|
||||
<sub>The Strata app's <b>Monitor</b> (left) while a coding agent writes the pagoda garden from the video (right)</sub></p>
|
||||
### In the app
|
||||
|
||||
- **In the browser:** open `http://127.0.0.1:8080`. It has **Chat**, a live **Monitor** of the model and your
|
||||
GPU/CPU/RAM, and **About** with the settings and addresses.
|
||||
- **Your apps and coding agents:** add an "OpenAI-compatible" provider with the base URL
|
||||
**`http://127.0.0.1:8080/v1`**. Any API key and any model name work.
|
||||
- Apps that use Anthropic's API: `http://127.0.0.1:8080/v1/messages` (Claude Code:
|
||||
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
|
||||
- Codex CLI and other apps that use the OpenAI Responses API: `/v1/responses`
|
||||
([setup](docs/DETAILS.md#the-responses-api-and-codex-cli)).
|
||||
- **Thinking:** choose **off, low, medium or high** in the chat menu or in your app's "reasoning effort". Off is the
|
||||
fastest. High is best for hard questions.
|
||||
- **Pictures:** say yes to "Images?" in setup. Then click **Picture** in the chat, or attach pictures in your app.
|
||||
AMD cards read pictures on Linux through the processor; on Windows they can't yet.
|
||||
- **From your phone or another PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Always set a key.
|
||||
- **One request at a time:** by default Strata answers one request, and the others wait. To answer several at once,
|
||||
set `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). On a 12 GB card this makes each answer slower.
|
||||
- **Long prompts:** Strata reads the first message of a chat in full, about 1 minute per 30,000 tokens. Follow-up
|
||||
messages start in seconds.
|
||||
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.
|
||||
|
||||
More: [where your chats are stored](docs/INSTALL.md#where-things-are-stored), [the API](docs/DETAILS.md#using-it).
|
||||
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.
|
||||
|
||||
### Optional: more speed on one card when the whole model fits in RAM
|
||||
To run two models, turn on **Run multiple models** and give each model its own card of the same make.
|
||||
|
||||
Two opt-in settings (by RubenTorney, #1904 and #1905). They help when you run one graphics card and your RAM holds
|
||||
every expert (Strata's arena mode, for example Q2_0 or IQ3_XXS on a 64 GB PC). Open your config,
|
||||
`strata-<model>.json` in the Strata data folder ([where](docs/INSTALL.md#where-things-are-stored)), and add:
|
||||
<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>
|
||||
|
||||
- **Faster decode:** `"--adapt-async", "1"` to the `"args"` list. The expert cache then updates on a helper thread
|
||||
instead of pausing the answer. Measured on an RTX 5070 (Windows, Q2_0): +4-5% decode, +7-10% on code and long
|
||||
documents. Answers can differ slightly from the default (they stay the same from run to run).
|
||||
- **Faster prompt reading on Windows:** `"STRATA_PIN_BY_NEED": "1"` in the `"env"` block. Windows can lock only about
|
||||
half the RAM for the graphics card; this locks the experts the card actually needs first. Measured on the RTX 5070:
|
||||
+4.4% prompt reading.
|
||||
### In the Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<sub><code>strata monitor</code> while the model writes a reply: speed, GPU, memory and the recent requests</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # guided screens: model, context, pictures, access, then serve
|
||||
strata launch --model q2_0 --yes --no-tui # no questions: serve, print the address, return
|
||||
strata launch --plan --model q2_0 # show what would happen; nothing changes
|
||||
strata launch --status # what is being served now
|
||||
strata launch --stop # stop it
|
||||
strata monitor --url http://127.0.0.1:8080 # watch speed, GPU, memory and requests
|
||||
strata update --check # is there a new engine? (strata update installs it)
|
||||
strata doctor # check the installed engine files
|
||||
```
|
||||
|
||||
`strata --help` lists everything. Starting and serving never download anything. Only the install, `strata launch` (when
|
||||
you choose a model) and `strata update` do. To reach Strata from your phone or another PC, use
|
||||
`strata launch --access lan`. It makes an API key (recommended; add `--no-api-key` to go without, with a warning). No browser opens unless
|
||||
you add `--open`. Guide: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Whole client: [docs/TERMINAL.md](docs/TERMINAL.md).
|
||||
|
||||
### With AI agents
|
||||
|
||||
An agent installs Strata with the one-line command in Option B and runs
|
||||
`strata launch --model NAME --yes --no-tui`. The steps, flags and exit codes are in [docs/AI_SETUP.md](docs/AI_SETUP.md),
|
||||
and the repository rules are in [AGENTS.md](AGENTS.md).
|
||||
|
||||
### Connect your other apps
|
||||
|
||||
| App type | Base URL |
|
||||
| --- | --- |
|
||||
| OpenAI-compatible (most apps and agents) | `http://127.0.0.1:8080/v1` (any API key, any model name) |
|
||||
| Anthropic API (Claude Code and others) | `http://127.0.0.1:8080`, without `/v1`. These apps add `/v1/messages` themselves. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([setup](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` prints the exact settings for OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent and OpenClaw.
|
||||
`strata connect opencode --write` writes the file after asking and keeps a backup.
|
||||
|
||||
### Pictures
|
||||
|
||||
Pictures are on by default for models that can read them. Attach or paste a picture in the chat, or send it from your app.
|
||||
The picture reader runs on the processor when your graphics card has less than 24 GB (a few seconds per picture, nothing
|
||||
taken from the card's memory) and on the graphics card from 24 GB. The rule is the same for NVIDIA and AMD. Turn pictures
|
||||
off per model, or choose the reader with `--vision off`, `--vision gpu` or `--vision cpu`.
|
||||
|
||||
The numbers behind the rule: on a Radeon AI PRO R9700 the card's reader took 57 ms for a 260-token picture and the
|
||||
processor's took 3.1 s. On 16 GB cards (Tesla P100 and RTX A4000, Linux) the card's reader cost 3-5% of text speed,
|
||||
because it holds about 1.4 GB that the model would use otherwise. On a Mac the reader runs on the Mac's GPU whatever its memory (0.45 s for a 260-token picture on an M4 with 32 GB, about 1.2 GB of the shared
|
||||
memory); the processor reader needs about 50 s per picture there, so it runs only when you ask for it with `--vision cpu`.
|
||||
|
||||
### Good to know
|
||||
|
||||
- Choose the thinking level (**off, low, medium or high**) in the chat menu or in your app's "reasoning effort". Off is fastest.
|
||||
- Strata answers one request at a time by default. For several at once, use `--parallel N` in `strata launch` or
|
||||
`"parallel": 2` in the config ([BATCHING.md](docs/BATCHING.md)). On a 12 GB card this makes each answer slower.
|
||||
- Strata reads the first message of a chat in full, about 1 minute per 30,000 tokens. Follow-up messages start in seconds.
|
||||
- Where your chats are stored and the whole API: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
|
||||
|
||||
## Build from source
|
||||
|
||||
<details>
|
||||
<summary>Windows, Linux and Mac from the repository</summary>
|
||||
|
||||
[Download Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) and unzip it, or `git clone` it.
|
||||
**Windows:** double-click `START-HERE.bat`. **Linux:** run `./setup.sh` in the Strata folder.
|
||||
|
||||
**Mac (Apple Silicon, macOS 15 or newer):** install Apple's Command Line Tools once with `xcode-select --install`
|
||||
(Xcode itself is not needed). Open Terminal in the Strata folder and run `./setup.sh`. It builds the Mac engine on your
|
||||
Mac (about 7 minutes), asks the same questions, downloads the model and starts it. Next time, `./setup.sh` starts it
|
||||
right away. Which size fits your Mac: [docs/MAC.md](docs/MAC.md).
|
||||
|
||||
The installer finds your card and sets up the right engine. It asks which model and size, how much context (how much text
|
||||
the model keeps in mind) and whether it should read pictures. Press Enter for the recommended answer. Then it downloads the
|
||||
model (about 70 GB) and starts it. If the download stops, run it again and it continues. Your browser opens the Strata
|
||||
page at `http://127.0.0.1:8080`.
|
||||
|
||||
> While the model starts, your PC can be slow or stop responding for 1-3 minutes (longest the first time). Strata loads
|
||||
> 35-55 GB into your RAM and locks part of it for the graphics card. Wait, and don't close the window.
|
||||
|
||||
Next time, run `START-HERE.bat` (or `./setup.sh`) again. It starts right away and downloads nothing twice. Close its
|
||||
window to stop the model. `UPDATE.bat` (`./update.sh`) updates Strata without starting it. Updating, Docker, several cards,
|
||||
where the files go and every option: [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
To measure your PC, run `BENCH.bat` (`./bench.sh`). It takes 5-10 minutes and writes a report you can share
|
||||
([how](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
|
||||
|
||||
**Optional: more speed on one card when the whole model fits in RAM.** Two opt-in settings by RubenTorney (#1904, #1905).
|
||||
They help when you run one graphics card and your RAM holds every expert, for example Q2_0 or IQ3_XXS on a 64 GB PC. Open
|
||||
`strata-<model>.json` in the Strata data folder ([where](docs/INSTALL.md#where-things-are-stored)) and add:
|
||||
|
||||
- `"--adapt-async", "1"` to the `"args"` list. The expert cache then updates on a helper thread instead of pausing the
|
||||
answer. Measured on an RTX 5070 (Windows, Q2_0): +4-5% decode, +7-10% on code and long documents. Answers can differ
|
||||
slightly from the default and stay the same from run to run.
|
||||
- `"STRATA_PIN_BY_NEED": "1"` in the `"env"` block (Windows). Windows can lock only about half the RAM for the graphics
|
||||
card, and this locks the experts the card needs first. Measured on the RTX 5070: +4.4% prompt reading.
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
Restart Strata (`START-HERE.bat` or `./setup.sh`) after the change. To undo, remove the two entries.
|
||||
Restart Strata after the change. To undo, remove the two entries.
|
||||
|
||||
## Something went wrong?
|
||||
|
||||
- **My PC froze the first time Strata started.** This is normal while it loads the model. 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.** Run `START-HERE.bat` (or `./setup.sh`) again. It continues where
|
||||
it stopped.
|
||||
- **It's very slow and 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 (Q2_0 or IQ2_XS).
|
||||
- **It says port 8080 is already in use.** Strata is already running. Look for its window.
|
||||
|
||||
More problems and their fixes: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Still stuck? Open an
|
||||
[issue](https://github.com/Niko1221/Strata/issues) and attach `strata-<model>.log` from the Strata folder. Found a
|
||||
security problem? Report it privately: [SECURITY.md](SECURITY.md).
|
||||
</details>
|
||||
|
||||
## How does it work?
|
||||
|
||||
Models like this one usually run on servers with hundreds of gigabytes of graphics memory. Your graphics card has
|
||||
12-24 GB. Strata makes the model fit by **sharing the work across your whole PC**. Think of a kitchen: the things
|
||||
you use all the time stay on the counter, and the rest waits in the pantry.
|
||||
12-24 GB. Strata makes the model fit by sharing the work across your whole PC. Picture a library.
|
||||
|
||||
- The model is a team of 24,576 small specialists ("experts"), like 24,576 reference books. Each word needs only 10 of them.
|
||||
- **Your graphics card is the desk.** It is small and fast. The roughly 4,000 books used most often lie open on it.
|
||||
- **Your RAM is the shelves in the reading room.** All 24,576 books are there. When a word needs a book that is not on the
|
||||
desk, your processor reads it from the shelf at once, while the card keeps working.
|
||||
- **Your SSD is the archive.** It holds a 29 GB lookup table. The model reads a few rows of it per word, so it does not
|
||||
need to sit in RAM.
|
||||
|
||||
<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>
|
||||
|
||||
- **The model is a team of 24,576 small specialists ("experts").** Each word needs only 10 of them.
|
||||
- **Your graphics card** keeps the few thousand experts that are used most often. **Your RAM** holds all of them,
|
||||
and **your processor** works on the rest at the same time. **Your SSD** holds a big lookup table.
|
||||
A small helper also guesses the next few words. The big model checks all of them at once and keeps the right ones. You get
|
||||
the same answer, 1.6-1.8x sooner. Long texts are read in big pieces, up to 8,192 tokens at a time, at over 1,000 tokens
|
||||
per second.
|
||||
|
||||
<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>
|
||||
|
||||
- **Guess, then check:** a small helper guesses the next few words. The big model checks them all at once. You get
|
||||
the same answer, 1.6-1.8x sooner.
|
||||
- **Long texts are read in big pieces** (up to 8,192 tokens at a time), at over 1,000 tokens per second.
|
||||
|
||||
The longer explanation: [docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md). Every part and its numbers:
|
||||
[the details](docs/DETAILS.md#how-it-works) and the [paper](docs/paper/Strata-Paper.pdf).
|
||||
|
||||
## Something went wrong?
|
||||
|
||||
- **My PC froze the first time Strata started.** It loads the model into RAM. Wait, and don't close the window. Still
|
||||
frozen after 10 minutes? Restart the PC, close other programs and try again, or pick a smaller size.
|
||||
- **It stopped while downloading or installing.** Start the app or `strata launch` again, or run `START-HERE.bat` (or
|
||||
`./setup.sh`) again. It continues where it stopped.
|
||||
- **It is very slow, the disk light keeps blinking, or it says "the engine stopped unexpectedly".** Your PC does not have
|
||||
enough free RAM. Close other programs (browsers use a lot), or pick a smaller size such as Q2_0 or IQ2_XS.
|
||||
- **It says there is no engine for this PC.** In the app, open **Settings, Engine** and press **Update engine**. In the
|
||||
Terminal, run `strata update`. `strata doctor` shows which engine files are installed.
|
||||
- **It says port 8080 is already in use.** Strata is already running. Look for its window.
|
||||
|
||||
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).
|
||||
|
||||
## 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)
|
||||
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)).
|
||||
[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
|
||||
|
||||
Found a bug, or want to send a change or a benchmark? Read [CONTRIBUTING.md](CONTRIBUTING.md) first. It says what to
|
||||
include so that others can check your work.
|
||||
|
||||
## Support Strata
|
||||
|
||||
Strata is free and open source. If it is useful to you, you can support its development:
|
||||
Strata is free and MIT-licensed. I build it and test it on whatever hardware I can get. If it runs well on your PC, a coffee helps
|
||||
me keep testing new cards and fixing what you report. No pressure. Using it and reporting what you find helps just as much.
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a></p>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
|
||||
|
||||
Interested in sponsoring Strata? Contact me via [GitHub](https://github.com/Niko1221).
|
||||
@@ -1,234 +1,394 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · **Português**
|
||||
<p align="center"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <b>Português</b></p>
|
||||
|
||||
<p align="center"><b>Rode um modelo de IA de 125 bilhões de parâmetros no seu próprio PC gamer</b><br>
|
||||
Placa de vídeo NVIDIA ou AMD (12 GB ou mais) · Windows ou Linux · gratuito e de código aberto</p>
|
||||
<h3 align="center">Rode um modelo de ponta no seu próprio PC gamer</h3>
|
||||
|
||||
<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 com pagode em voxels que o modelo do Strata escreveu, rodando no navegador"></a><br>
|
||||
<sub>Um jardim com pagode em voxels, feito com um único prompt numa RTX 5070 com o Strata (IQ3_S, contexto de 128K) ·
|
||||
<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>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<sub>Um jardim de pagodes em voxels, gerado com um único prompt, rodando numa RTX 5070 com o Strata (IQ3_S, contexto de 128K) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">vídeo completo (49 s)</a></sub></p>
|
||||
|
||||
O Strata roda o **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)** num PC comum. É um modelo
|
||||
de IA grande e inteligente que normalmente precisa de um servidor. Ele conversa, escreve código, entende imagens e
|
||||
funciona com seus apps e agentes de programação. Nada sai do seu PC.
|
||||
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.
|
||||
|
||||
## Qual é a velocidade?
|
||||
**Primeira vez aqui?** Veja [o que você precisa](#o-que-você-precisa) e depois [instale o app](#opção-a-o-app-strata).
|
||||
|
||||
Medimos em dois PCs gamer comuns. Um token equivale a cerca de ¾ de uma palavra.
|
||||
- **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).
|
||||
|
||||
- **Escreve respostas:** a velocidade com que a resposta aparece numa conversa curta. 60 tokens por segundo é mais rápido do que você consegue ler.
|
||||
- **Lê seu prompt:** a velocidade com que ele absorve o que você envia (aqui, um documento, código ou histórico de conversa de 32K tokens).
|
||||
Versão 0.2.1 · mecanismo 0.1.46
|
||||
|
||||
<table>
|
||||
<tr><th>NVIDIA: RTX 5070 (12 GB), Ryzen 5 7600, 64 GB de RAM</th><th>AMD: RX 9070 XT (16 GB), Ryzen 9 3900X, 47 GB de RAM</th></tr>
|
||||
<tr><td>
|
||||
## O que você precisa
|
||||
|
||||
| Tamanho | Escreve respostas | Lê seu prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
|
||||
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
|
||||
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
|
||||
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
|
||||
| **Coder** | 55 tokens/s | 2,180 tokens/s |
|
||||
**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.
|
||||
|
||||
</td><td>
|
||||
- Um PC de 32 GB com uma placa de 12 a 16 GB roda o Coder.
|
||||
- 64 GB de RAM rodam todos os tamanhos.
|
||||
|
||||
| Tamanho | Escreve respostas | Lê seu prompt |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
|
||||
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
|
||||
| **Coder** | 44 tokens/s | 1,420 tokens/s |
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
NVIDIA: Q2_0 com o engine 0.1.36, as outras linhas com 0.1.26 (respostas de 4K, prompts de 32K). As tabelas
|
||||
completas estão em [DETAILS.md](docs/DETAILS.md#speed-measured). Uma placa com mais VRAM é mais rápida: uma
|
||||
RTX 3090 (24 GB) deve escrever cerca de 100-140 tokens por segundo. Conversas longas e outras placas:
|
||||
[velocidade de cada modelo](docs/MODELS.md#how-fast-is-each-size), [resultados da comunidade](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a><br>
|
||||
<sub>O Strata é gratuito. Se ele roda bem no seu PC, um café ajuda a manter o trabalho nele.</sub></p>
|
||||
|
||||
## Do que você precisa
|
||||
|
||||
| | |
|
||||
| Seu computador | Detalhes |
|
||||
| --- | --- |
|
||||
| **Placa de vídeo** | **NVIDIA** GeForce RTX séries 20, 30, 40 ou 50, ou **AMD** Radeon RX 7900 XT / XTX, RX 7800 XT / 7700 XT, RX 9060 XT, RX 9070 / 9070 XT, Radeon AI PRO R9700 ou séries RX 6800 / 6900. Ela precisa de **12 GB de VRAM ou mais**. |
|
||||
| **RAM** | 32 GB ou mais. Sua RAM decide [qual modelo](#qual-modelo-devo-escolher) cabe. Com 64 GB, roda qualquer tamanho. |
|
||||
| **Disco** | Cerca de 80 GB livres. Use um SSD se puder: a primeira inicialização fica bem mais rápida. |
|
||||
| **Sistema** | Windows 10 / 11 ou Linux, e um driver de vídeo atualizado da NVIDIA ou da AMD. |
|
||||
| **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) |
|
||||
|
||||
O instalador cuida de todo o resto. Duas ou três placas podem dividir o modelo ([multi-GPU](docs/MULTI_GPU.md)).
|
||||
Você também precisa de:
|
||||
|
||||
Experimental, escrito e testado por membros da comunidade nas próprias máquinas:
|
||||
- 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
|
||||
|
||||
- **Placas de vídeo mais antigas** (Tesla P40 / V100, GTX 10, Radeon VII / MI50, RX 6700 XT, RX 5500 XT): [Older GPUs](docs/OLDER_GPUS.md).
|
||||
- **Intel Arc**, compilado a partir do código-fonte no Linux: [Intel Arc](docs/INTEL_ARC.md).
|
||||
- **AMD Ryzen AI Max (Strix Halo)**, compilado a partir do código-fonte no Linux: [Strix Halo](docs/STRIX_HALO.md).
|
||||
- **Processadores mais antigos sem AVX2**: funcionam, mas devagar. [Older CPUs](docs/INSTALL.md#older-cpus-experimental).
|
||||
|
||||
A lista completa: [docs/INSTALL.md](docs/INSTALL.md#what-you-need).
|
||||
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).
|
||||
|
||||
## Instalação
|
||||
|
||||
### Deixe sua IA instalar
|
||||
Escolha uma opção. Todas iniciam o mesmo mecanismo e usam os mesmos modelos.
|
||||
|
||||
Você usa um assistente de programação com IA (Claude Code, Cursor, Codex, GitHub Copilot, ...)? Cole isto nele:
|
||||
### Opção A: o app Strata
|
||||
|
||||
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.
|
||||
|
||||
| Sistema | Sua placa de vídeo | Arquivo para baixar |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, driver 580 ou mais novo | [`Strata-0.2.1-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda13.exe) |
|
||||
| | NVIDIA, placa mais antiga ou driver anterior ao 580 | [`Strata-0.2.1-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda12.exe) |
|
||||
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.1-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip.exe) |
|
||||
| | AMD Radeon série RX 7000 ou Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, driver 580 ou mais novo | [`Strata-0.2.1-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, placa mais antiga ou driver anterior ao 580 | [`Strata-0.2.1-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.1-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon série RX 7000 | [`Strata-0.2.1-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 ou mais novo) | integrada | [`Strata-0.2.1-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-mac.dmg) |
|
||||
|
||||
**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).
|
||||
|
||||
Depois:
|
||||
|
||||
- **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.
|
||||
|
||||
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.
|
||||
|
||||
### Opção B: o Terminal (uma linha)
|
||||
|
||||
O Terminal é o comando `strata`, para scripts, servidores e agentes de IA.
|
||||
|
||||
Linux e Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
```
|
||||
|
||||
Windows (PowerShell):
|
||||
|
||||
```powershell
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
O script confere o seu PC, baixa o Terminal, verifica o SHA-256 e descompacta na sua pasta pessoal. Não precisa de direitos de
|
||||
administrador. No Windows, abra uma nova janela do terminal depois. Então inicie o Strata:
|
||||
|
||||
```sh
|
||||
strata launch
|
||||
```
|
||||
|
||||
Ele faz uma pergunta por vez: modelo, contexto, imagens, quem pode se conectar. Depois baixa o mecanismo para a sua placa de vídeo
|
||||
(assinada e testada antes do uso) e o modelo, e começa a servir. Em scripts e agentes de IA, use flags:
|
||||
|
||||
```sh
|
||||
strata launch --model q2_0 --yes --no-tui
|
||||
```
|
||||
|
||||
Para ler o script de instalação antes de executá-lo, baixe-o primeiro com `curl -fsSL <address> -o install.sh`, leia e então rode
|
||||
`sh install.sh`. Todas as telas e flags: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md).
|
||||
|
||||
### Opção C: deixe a sua IA configurar
|
||||
|
||||
Cole isto no Claude Code, Cursor, Codex, GitHub Copilot ou outro assistente de programação:
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
Ele verifica sua placa de vídeo, RAM e disco e escolhe o modelo que cabe. Depois instala, inicia e explica como
|
||||
conectar seus apps. Ferramentas de IA também podem instalar, iniciar e parar o Strata pelo
|
||||
[servidor MCP](docs/MCP_SERVER.md) dele.
|
||||
Ele confere a sua placa de vídeo, a RAM e o disco, escolhe o modelo que cabe, instala e inicia, e diz como conectar os seus
|
||||
aplicativos. Ferramentas de IA também podem instalar, iniciar e parar o Strata pelo [servidor MCP](docs/MCP_SERVER.md) dele.
|
||||
|
||||
### Ou faça você mesmo
|
||||
Prefere compilar por conta própria? Veja [Compilar a partir do código-fonte](#compilar-a-partir-do-código-fonte).
|
||||
|
||||
[Baixe o Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) e descompacte (ou use `git clone`).
|
||||
**Windows:** clique duas vezes em **`START-HERE.bat`**. **Linux:** rode **`./setup.sh`** na pasta do Strata.
|
||||
## Modelos
|
||||
|
||||
**Mac (Apple Silicon, M1 a M5, macOS 15 ou mais recente):** instale uma vez as Command Line Tools da Apple (`xcode-select --install` no Terminal; o Xcode em si não é necessário). Depois abra o Terminal na pasta do Strata e rode **`./setup.sh`**. Ele compila o motor do Mac no seu Mac (uns 7 minutos), faz as mesmas perguntas, baixa o modelo e o inicia. Da próxima vez, `./setup.sh` inicia na hora. Qual tamanho cabe no seu Mac e qual a velocidade: [docs/MAC.md](docs/MAC.md).
|
||||
|
||||
Os passos são os mesmos para NVIDIA e AMD. O instalador encontra sua placa e configura o engine certo para ela.
|
||||
Ele faz algumas perguntas:
|
||||
|
||||
- qual modelo e qual tamanho,
|
||||
- quanto contexto (quanto texto o modelo guarda na memória),
|
||||
- se ele deve entender imagens.
|
||||
|
||||
Aperte Enter em cada uma para usar a resposta recomendada. Depois ele baixa o modelo (cerca de 70 GB) e o inicia.
|
||||
Se o download parar, rode de novo: ele continua de onde parou. Seu navegador abre o app do Strata em
|
||||
`http://127.0.0.1:8080`.
|
||||
|
||||
> **Enquanto o modelo inicia, seu PC pode ficar lento ou parar de responder por 1-3 minutos** (mais na primeira vez).
|
||||
> O Strata carrega 35-55 GB na sua RAM e reserva parte dela para a placa de vídeo. Isso é normal. Espere e não
|
||||
> feche a janela. A janela mostra o que o Strata está fazendo.
|
||||
|
||||
**Da próxima vez**, rode `START-HERE.bat` (ou `./setup.sh`) de novo. Ele inicia na hora e não baixa nada duas
|
||||
vezes. Feche a janela para parar o modelo. `UPDATE.bat` (`./update.sh`) atualiza o Strata sem iniciá-lo.
|
||||
Atualização, Docker, várias placas, onde ficam os arquivos e todas as opções: [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
## Qual modelo devo escolher?
|
||||
|
||||
O instalador recomenda um de acordo com sua RAM. O mesmo modelo vem em vários tamanhos, mais ou menos comprimidos.
|
||||
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.
|
||||
|
||||
| Sua RAM | Escolha | Por quê |
|
||||
### Pouca memória? Experimente o Gyro
|
||||
|
||||
O [Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (da agentionai) é a menor forma de rodar este modelo.
|
||||
|
||||
| Tamanho | Memória necessária (RAM + VRAM) | Download | O que é |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27,6 GB | 58,5 GB | Para PCs onde nenhum outro tamanho cabe. A qualidade é menor que a do IQ2_XS. |
|
||||
| **Gyro-M** | 35 GB | 92,0 GB | Qualidade próxima à do IQ3_XXS, com cerca de 20% menos memória da GPU. Ele escreve as respostas 17 a 21% mais devagar que o Q2_0 numa placa de 12 GB e numa R9700. |
|
||||
|
||||
O Gyro roda em placas NVIDIA e AMD (AMD é experimental), e não roda em Intel Arc nem no Mac. Detalhes e medições:
|
||||
[docs/GYRO.md](docs/GYRO.md). Inicie com `strata launch --model gyro-s`.
|
||||
|
||||
### Os outros tamanhos
|
||||
|
||||
| Sua memória (RAM + VRAM) | Escolha | Observações |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Coder** | cabe em 32 GB e é feito para código (com uma placa de 24 GB, Q2_0 e IQ2_XS também rodam) |
|
||||
| **48 GB** | **IQ2_XS** (ou Q2_0, o mais rápido) | os tamanhos maiores não cabem |
|
||||
| **64 GB** | **IQ2_XS** (recomendado), ou IQ3_XXS / IQ3_S | todos os tamanhos cabem; IQ3_S é o melhor e o mais lento |
|
||||
| **96 GB ou mais** | **IQ3_S**, ou o UD-IQ4_XS da Unsloth (~4-bit) | espaço para os maiores tamanhos com todo o resto aberto |
|
||||
| **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 17 a 21% mais devagar que o Q2_0 |
|
||||
| **48 GB** | **IQ3_XXS** (47,0 GB) | Mais lento, melhor |
|
||||
| **64 GB** | **IQ3_S** (54,8 GB), o original ou o Swift 1.5 | O mais lento, o melhor: empata com o modelo completo nos testes publicados |
|
||||
| **96 GB ou mais** | **UD-IQ4_XS** da Unsloth (cerca de 72 GB de RAM) até **Q8_0** (cerca de 136 GB), de 4 a 8 bits | Mais perto do modelo completo. Com menos RAM, o Strata lê parte deles do SSD, o que é bem mais lento |
|
||||
|
||||
- **[Coder](docs/MODELS.md#coder):** uma versão para programação com metade dos experts removida. Ela alcança 91%
|
||||
da pontuação do modelo completo no SWE-bench Verified (medido pelos autores) e cabe em 32 GB de RAM. É mais fraca
|
||||
fora do código, inclusive em chinês e outros textos CJK (#438). Para isso, escolha Q2_0, IQ2_XS ou IQ3_S, que
|
||||
mantêm todos os experts.
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15):** um fine-tune que pensa por bem menos tempo antes de responder. Você
|
||||
recebe a resposta mais cedo, com quase a mesma qualidade.
|
||||
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** a versão de ~4 bits da Unsloth, entre IQ3_S e
|
||||
UD-Q4_K_XL em qualidade. Um download de 94 GB. Com menos de ~80 GB de RAM, o Strata lê uma parte dele do SSD
|
||||
enquanto responde, então ali ele fica mais lento (um SSD NVMe ajuda).
|
||||
- **[Unsloth UD-Q4_K_XL, UD-Q5_K_XL, UD-Q6_K_XL e Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):** de 4 a 8 bits, mais próximos do modelo
|
||||
completo (93-95% o mesmo próximo token que o Q8_0). Rápidos quando todos os especialistas cabem na RAM (~88 GB para
|
||||
o UD-Q4_K_XL até ~136 GB para o Q8_0); com menos RAM, o Strata lê uma parte do SSD enquanto responde, o que é bem
|
||||
mais lento (7-8.5 tokens/s para o UD-Q4_K_XL num PC com 64 GB).
|
||||
- **[OrcaRouter's Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** você configura à mão. Ele
|
||||
não está no menu do instalador.
|
||||
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.
|
||||
|
||||
Tamanhos, downloads e o que cabe onde: [docs/MODELS.md](docs/MODELS.md). Para adicionar outro modelo depois, rode
|
||||
`SETUP.bat` (Linux: `./setup.sh --setup`).
|
||||
Mais opções:
|
||||
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** é um ajuste fino que pensa por menos tempo antes de responder, com qualidade e RAM parecidas.
|
||||
- **[NVFP4](docs/NVFP4.md)** é a versão de 4 bits da NVIDIA para placas NVIDIA. Precisa de cerca de 80 GB de RAM para manter todos os experts na RAM.
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** é outro modelo, menor, que lê imagens e pode atender vários pedidos ao mesmo tempo.
|
||||
- **[Uncensored IQ3_XXS da OrcaRouter](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** é configurado manualmente.
|
||||
|
||||
Todos os tamanhos, downloads e o que cabe em cada configuração: [docs/MODELS.md](docs/MODELS.md).
|
||||
|
||||
## Qual é a velocidade?
|
||||
|
||||
Com base numa placa de vídeo de 12 GB (RTX 5070, Ryzen 5 7600, 64 GB de RAM). Veja resultados de outras placas:
|
||||
[velocidade de cada modelo](docs/MODELS.md#how-fast-is-each-size), [resultados da comunidade](docs/COMMUNITY_BENCHMARKS.md).
|
||||
|
||||
| Modelo | As respostas aparecem a | Lê o seu prompt a |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████████` 94 tokens/s | `████████████` 2.650 tokens/s |
|
||||
| **IQ2_XS** | `██████████░░` 79 tokens/s | `█████████░░░` 2.090 tokens/s |
|
||||
| **IQ3_XXS** | `████████░░░░` 62 tokens/s | `████████░░░░` 1.750 tokens/s |
|
||||
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1.620 tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2.180 tokens/s |
|
||||
|
||||
Um token equivale a cerca de ¾ de palavra, então 60 tokens por segundo é mais rápido do que você lê. "Lê o seu prompt" é a velocidade com que o Strata
|
||||
absorve um documento longo, um código ou um histórico de conversa.
|
||||
|
||||
Medido numa placa AMD de 16 GB (RX 9070 XT, Ryzen 9 3900X, 47 GB de RAM):
|
||||
|
||||
| Modelo | As respostas aparecem a | Lê o seu prompt a |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████░░░░` 60 tokens/s | `█████░░░░░░░` 1.160 tokens/s |
|
||||
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1.110 tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1.420 tokens/s |
|
||||
|
||||
As barras usam a mesma escala nas duas tabelas. Uma placa com mais VRAM é mais rápida: uma RTX 3090 (24 GB) deve escrever cerca de 100 a 140
|
||||
tokens por segundo. A versão do mecanismo por trás de cada número está em [DETAILS.md](docs/DETAILS.md#speed-measured).
|
||||
|
||||
## Como usar
|
||||
|
||||
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="A aba Monitor do app Strata ao lado de um agente de programação"><br>
|
||||
<sub>O <b>Monitor</b> do app Strata (à esquerda) enquanto um agente de programação escreve o jardim com pagode do vídeo (à direita)</sub></p>
|
||||
### No app
|
||||
|
||||
- **No navegador:** abra `http://127.0.0.1:8080`. Lá tem o **Chat**, um **Monitor** ao vivo do modelo e da sua
|
||||
GPU/CPU/RAM, e o **About** com as configurações e os endereços.
|
||||
- **Seus apps e agentes de programação:** adicione um provedor "OpenAI-compatible" com a URL base
|
||||
**`http://127.0.0.1:8080/v1`**. Qualquer chave de API e qualquer nome de modelo funcionam.
|
||||
- Apps que usam a API da Anthropic: `http://127.0.0.1:8080/v1/messages` (Claude Code:
|
||||
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`).
|
||||
- Codex CLI e outros apps que usam a OpenAI Responses API: `/v1/responses`
|
||||
([configuração](docs/DETAILS.md#the-responses-api-and-codex-cli)).
|
||||
- **Raciocínio:** escolha **off, low, medium ou high** no menu do chat ou no "reasoning effort" do seu app. Off é o
|
||||
mais rápido. High é o melhor para perguntas difíceis.
|
||||
- **Imagens:** responda sim para "Images?" na instalação. Depois clique em **Picture** no chat, ou anexe imagens no
|
||||
seu app. Placas AMD entendem imagens no Linux usando o processador; no Windows, ainda não.
|
||||
- **Do seu celular ou de outro PC:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`. Sempre defina uma chave.
|
||||
- **Uma requisição por vez:** por padrão, o Strata responde uma requisição e as outras esperam. Para responder
|
||||
várias ao mesmo tempo, defina `"parallel": 2` ([BATCHING.md](docs/BATCHING.md)). Numa placa de 12 GB, isso deixa
|
||||
cada resposta mais lenta.
|
||||
- **Prompts longos:** o Strata lê a primeira mensagem de uma conversa inteira, cerca de 1 minuto a cada 30,000
|
||||
tokens. As mensagens seguintes começam em segundos.
|
||||
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.
|
||||
|
||||
Mais: [onde suas conversas ficam salvas](docs/INSTALL.md#where-things-are-stored), [a API](docs/DETAILS.md#using-it).
|
||||
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.
|
||||
|
||||
### Opcional: mais velocidade com uma placa quando o modelo inteiro cabe na RAM
|
||||
Para rodar dois modelos, ative **Executar vários modelos** e dê a cada modelo a sua própria placa da mesma marca.
|
||||
|
||||
Duas configurações opcionais (de RubenTorney, #1904 e #1905). Elas ajudam quando você usa uma placa de vídeo e sua RAM guarda todos os experts (o modo arena do Strata, por exemplo Q2_0 ou IQ3_XXS num PC com 64 GB). Abra sua configuração `strata-<model>.json` na pasta de dados do Strata ([onde](docs/INSTALL.md#where-things-are-stored)) e adicione:
|
||||
<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>
|
||||
|
||||
- **Geração mais rápida:** `"--adapt-async", "1"` na lista `"args"`. O cache de experts passa a ser atualizado numa thread auxiliar em vez de pausar a resposta. Medido numa RTX 5070 (Windows, Q2_0): +4-5% de geração, +7-10% em código e documentos longos. As respostas podem diferir um pouco do padrão (ficam iguais de uma execução para outra).
|
||||
- **Leitura do prompt mais rápida no Windows:** `"STRATA_PIN_BY_NEED": "1"` no bloco `"env"`. O Windows só consegue travar cerca de metade da RAM para a placa; isto trava primeiro os experts de que a placa realmente precisa. Medido na RTX 5070: +4,4% na leitura do prompt.
|
||||
### No Terminal
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<sub><code>strata monitor</code> enquanto o modelo responde: velocidade, GPU, memória e as solicitações recentes</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # telas guiadas: modelo, contexto, imagens, acesso e depois servir
|
||||
strata launch --model q2_0 --yes --no-tui # sem perguntas: serve, mostra o endereço e volta
|
||||
strata launch --plan --model q2_0 # mostra o que aconteceria; nada muda
|
||||
strata launch --status # o que está sendo servido agora
|
||||
strata launch --stop # para o serviço
|
||||
strata monitor --url http://127.0.0.1:8080 # acompanha velocidade, GPU, memória e requisições
|
||||
strata update --check # há um mecanismo novo? (strata update instala)
|
||||
strata doctor # confere os arquivos do mecanismo instalada
|
||||
```
|
||||
|
||||
`strata --help` lista tudo. Iniciar e servir nunca baixam nada. Só a instalação, o `strata launch` (quando você
|
||||
escolhe um modelo) e o `strata update` baixam. Para acessar o Strata do celular ou de outro PC, use
|
||||
`strata launch --access lan`. Ele cria uma chave de API e não inicia numa rede sem uma. Nenhum navegador abre, a menos que
|
||||
você adicione `--open`. Guia: [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md). Cliente completo: [docs/TERMINAL.md](docs/TERMINAL.md).
|
||||
|
||||
### Com agentes de IA
|
||||
|
||||
Um agente instala o Strata com o comando de uma linha da Opção B e executa
|
||||
`strata launch --model NAME --yes --no-tui`. Os passos, as flags e os códigos de saída estão em [docs/AI_SETUP.md](docs/AI_SETUP.md),
|
||||
e as regras do repositório estão em [AGENTS.md](AGENTS.md).
|
||||
|
||||
### Conecte os seus outros aplicativos
|
||||
|
||||
| Tipo de aplicativo | URL base |
|
||||
| --- | --- |
|
||||
| Compatível com OpenAI (a maioria dos apps e agentes) | `http://127.0.0.1:8080/v1` (qualquer chave de API, qualquer nome de modelo) |
|
||||
| API da Anthropic (Claude Code e outros) | `http://127.0.0.1:8080`, sem `/v1`. Esses aplicativos adicionam `/v1/messages` sozinhos. Claude Code: `ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API (Codex CLI) | `http://127.0.0.1:8080/v1/responses` ([configuração](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` mostra as configurações exatas para OpenCode, Codex CLI, Claude Code, oh-my-pi, pi, Hermes Agent e OpenClaw.
|
||||
`strata connect opencode --write` grava o arquivo depois de perguntar e mantém um backup.
|
||||
|
||||
### Imagens
|
||||
|
||||
As imagens vêm ativadas nos modelos que sabem lê-las. Anexe ou cole uma imagem no chat, ou envie pelo seu aplicativo.
|
||||
O leitor de imagens roda no processador quando a sua placa de vídeo tem menos de 24 GB (alguns segundos por imagem, sem tirar nada
|
||||
da memória da placa) e roda na placa de vídeo a partir de 24 GB. A regra é a mesma para NVIDIA e AMD. Desative as imagens
|
||||
por modelo, ou escolha o leitor com `--vision off`, `--vision gpu` ou `--vision cpu`.
|
||||
|
||||
Os números por trás da regra: numa Radeon AI PRO R9700, o leitor da placa levou 57 ms para uma imagem de 260 tokens e o do
|
||||
processador levou 3,1 s. Em placas de 16 GB (Tesla P100 e RTX A4000, Linux), o leitor da placa custou de 3 a 5% da velocidade do texto,
|
||||
porque ocupa cerca de 1,4 GB que o modelo usaria de outra forma. No Mac, a GPU faz a leitura, seja qual for a memória (0,45 s para uma imagem de 260 tokens em um M4 de 32 GB, cerca de 1,2 GB da memória compartilhada);
|
||||
o processador leva cerca de 50 s por imagem, então só lê com `--vision cpu`.
|
||||
|
||||
### Bom saber
|
||||
|
||||
- Escolha o nível de raciocínio (**off, low, medium ou high**) no menu do chat ou no "reasoning effort" do seu aplicativo. Off é o mais rápido.
|
||||
- Por padrão, o Strata responde a um pedido por vez. Para vários ao mesmo tempo, use `--parallel N` no `strata launch` ou
|
||||
`"parallel": 2` na configuração ([BATCHING.md](docs/BATCHING.md)). Numa placa de 12 GB, isso deixa cada resposta mais lenta.
|
||||
- O Strata lê a primeira mensagem de uma conversa por inteiro, cerca de 1 minuto a cada 30.000 tokens. As mensagens seguintes começam em segundos.
|
||||
- Onde ficam as suas conversas e a API completa: [docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored), [docs/DETAILS.md](docs/DETAILS.md#using-it).
|
||||
|
||||
## Compilar a partir do código-fonte
|
||||
|
||||
<details>
|
||||
<summary>Windows, Linux e Mac a partir do repositório</summary>
|
||||
|
||||
[Baixe o Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) e descompacte, ou use `git clone`.
|
||||
**Windows:** dê dois cliques em `START-HERE.bat`. **Linux:** execute `./setup.sh` na pasta do Strata.
|
||||
|
||||
**Mac (Apple Silicon, macOS 15 ou mais novo):** instale uma vez as Command Line Tools da Apple com `xcode-select --install`
|
||||
(o Xcode em si não é necessário). Abra o Terminal na pasta do Strata e execute `./setup.sh`. Ele compila o mecanismo do Mac no seu
|
||||
Mac (cerca de 7 minutos), faz as mesmas perguntas, baixa o modelo e inicia. Da próxima vez, `./setup.sh` inicia
|
||||
na hora. Qual tamanho cabe no seu Mac: [docs/MAC.md](docs/MAC.md).
|
||||
|
||||
O instalador encontra a sua placa e configura o mecanismo certo. Ele pergunta qual modelo e tamanho, quanto contexto (quanto texto
|
||||
o modelo mantém em mente) e se deve ler imagens. Pressione Enter para aceitar a resposta recomendada. Depois ele baixa o
|
||||
modelo (cerca de 70 GB) e inicia. Se o download parar, execute de novo e ele continua. O navegador abre a página do Strata em
|
||||
`http://127.0.0.1:8080`.
|
||||
|
||||
> Enquanto o modelo inicia, o seu PC pode ficar lento ou parar de responder por 1 a 3 minutos (mais tempo na primeira vez). O Strata carrega
|
||||
> de 35 a 55 GB na sua RAM e trava parte dela para a placa de vídeo. Espere e não feche a janela.
|
||||
|
||||
Da próxima vez, execute `START-HERE.bat` (ou `./setup.sh`) de novo. Ele inicia na hora e não baixa nada duas vezes. Feche a janela
|
||||
dele para parar o modelo. `UPDATE.bat` (`./update.sh`) atualiza o Strata sem iniciá-lo. Atualização, Docker, várias placas,
|
||||
onde ficam os arquivos e todas as opções: [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
Para medir o seu PC, execute `BENCH.bat` (`./bench.sh`). Leva de 5 a 10 minutos e gera um relatório que você pode compartilhar
|
||||
([como](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark)).
|
||||
|
||||
**Opcional: mais velocidade numa placa quando o modelo inteiro cabe na RAM.** Duas configurações opcionais do RubenTorney (#1904, #1905).
|
||||
Elas ajudam quando você usa uma placa de vídeo e a sua RAM comporta todos os experts, por exemplo Q2_0 ou IQ3_XXS num PC de 64 GB. Abra
|
||||
`strata-<model>.json` na pasta de dados do Strata ([onde fica](docs/INSTALL.md#where-things-are-stored)) e adicione:
|
||||
|
||||
- `"--adapt-async", "1"` à lista `"args"`. O cache de experts passa a ser atualizado numa thread auxiliar, em vez de pausar a
|
||||
resposta. Medido numa RTX 5070 (Windows, Q2_0): +4 a 5% na geração, +7 a 10% em código e documentos longos. As respostas podem diferir
|
||||
um pouco do padrão e continuam iguais de uma execução para outra.
|
||||
- `"STRATA_PIN_BY_NEED": "1"` no bloco `"env"` (Windows). O Windows só consegue travar cerca de metade da RAM para a placa
|
||||
de vídeo, e isso trava primeiro os experts de que a placa precisa. Medido na RTX 5070: +4,4% na leitura do prompt.
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
Reinicie o Strata (`START-HERE.bat` ou `./setup.sh`) depois da mudança. Para desfazer, remova as duas entradas.
|
||||
Reinicie o Strata depois da alteração. Para desfazer, remova as duas entradas.
|
||||
|
||||
## Algo deu errado?
|
||||
|
||||
- **Meu PC travou na primeira vez que o Strata iniciou.** Isso é normal enquanto ele carrega o modelo. 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.** Rode `START-HERE.bat` (ou `./setup.sh`) de novo. Ele continua de
|
||||
onde parou.
|
||||
- **Está muito lento e a luz do disco não para de piscar, ou aparece "the engine stopped unexpectedly".** Seu PC
|
||||
não tem RAM livre suficiente. Feche outros programas (navegadores usam muita), ou escolha um tamanho menor (Q2_0
|
||||
ou IQ2_XS).
|
||||
- **Diz que a porta 8080 já está em uso.** O Strata já está rodando. Procure a janela dele.
|
||||
|
||||
Mais problemas e como resolver: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). Ainda sem solução? Abra uma
|
||||
[issue](https://github.com/Niko1221/Strata/issues) e anexe o `strata-<model>.log` da pasta do Strata. Encontrou um
|
||||
problema de segurança? Informe em particular: [SECURITY.md](SECURITY.md).
|
||||
</details>
|
||||
|
||||
## Como funciona?
|
||||
|
||||
Modelos como este normalmente rodam em servidores com centenas de gigabytes de memória de vídeo. Sua placa de vídeo
|
||||
tem 12-24 GB. O Strata faz o modelo caber **dividindo o trabalho pelo PC inteiro**. Pense numa cozinha: o que você
|
||||
usa o tempo todo fica na bancada, e o resto espera na despensa.
|
||||
Modelos como este costumam rodar em servidores com centenas de gigabytes de memória de vídeo. A sua placa de vídeo tem
|
||||
12 a 24 GB. O Strata faz o modelo caber dividindo o trabalho entre o PC inteiro. Imagine uma biblioteca.
|
||||
|
||||
<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>
|
||||
- O modelo é uma equipe de 24.576 pequenos especialistas ("experts"), como 24.576 livros de consulta. Cada palavra precisa de apenas 10 deles.
|
||||
- **A sua placa de vídeo é a mesa.** Ela é pequena e rápida. Os cerca de 4.000 livros mais usados ficam abertos nela.
|
||||
- **A sua RAM são as estantes da sala de leitura.** Os 24.576 livros estão lá. Quando uma palavra precisa de um livro que não está na
|
||||
mesa, o seu processador lê da estante na hora, enquanto a placa continua trabalhando.
|
||||
- **O seu SSD é o arquivo.** Ele guarda uma tabela de consulta de 29 GB. O modelo lê algumas linhas dela por palavra, então ela não
|
||||
precisa ficar na RAM.
|
||||
|
||||
- **O modelo é uma equipe de 24,576 pequenos especialistas ("experts").** Cada palavra precisa de só 10 deles.
|
||||
- **Sua placa de vídeo** guarda os poucos milhares de experts usados com mais frequência. **Sua RAM** guarda todos
|
||||
eles, e **seu processador** trabalha no resto ao mesmo tempo. **Seu SSD** guarda uma grande tabela de consulta.
|
||||
<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/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>
|
||||
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.
|
||||
|
||||
- **Adivinhar e conferir:** um pequeno ajudante adivinha as próximas palavras. O modelo grande confere todas de
|
||||
uma vez. Você recebe a mesma resposta, 1.6-1.8x mais cedo.
|
||||
- **Textos longos são lidos em grandes pedaços** (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>
|
||||
|
||||
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).
|
||||
|
||||
## Algo deu errado?
|
||||
|
||||
- **Meu PC travou na primeira vez que o Strata iniciou.** Ele carrega o modelo na RAM. Espere e não feche a janela. Ainda
|
||||
travado depois de 10 minutos? Reinicie o PC, feche outros programas e tente de novo, ou escolha um tamanho menor.
|
||||
- **Parou durante o download ou a instalação.** Abra o app ou o `strata launch` de novo, ou execute `START-HERE.bat` (ou
|
||||
`./setup.sh`) outra vez. Ele continua de onde parou.
|
||||
- **Está muito lento, a luz do disco não para de piscar, ou aparece "o mecanismo parou inesperadamente".** O seu PC não tem
|
||||
RAM livre suficiente. Feche outros programas (os navegadores usam muita), ou escolha um tamanho menor, como Q2_0 ou IQ2_XS.
|
||||
- **Diz que não há mecanismo para este PC.** No app, abra **Configurações, Mecanismo** e clique em **Atualizar mecanismo**. No
|
||||
Terminal, execute `strata update`. O `strata doctor` mostra quais arquivos do mecanismo estão instalados.
|
||||
- **Diz que a porta 8080 já está em uso.** O Strata já está rodando. Procure a janela dele.
|
||||
|
||||
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).
|
||||
|
||||
## 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)
|
||||
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 [MIT License](LICENSE).
|
||||
Algumas partes e todos os modelos têm licenças próprias ([quais](docs/HOW_IT_WORKS.md#license)).
|
||||
[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
|
||||
|
||||
Encontrou um bug, ou quer enviar uma alteração ou um benchmark? Leia primeiro o [CONTRIBUTING.md](CONTRIBUTING.md). Ele diz o que
|
||||
incluir para que outras pessoas possam conferir o seu trabalho.
|
||||
|
||||
## Apoie o Strata
|
||||
|
||||
O Strata é gratuito e de código aberto. Se ele é útil para você, você pode apoiar o desenvolvimento:
|
||||
O Strata é gratuito e tem licença MIT. Eu o desenvolvo e testo em todo hardware que consigo arranjar. Se ele roda bem no seu PC, um café me ajuda
|
||||
a continuar testando placas novas e corrigindo o que você relatar. Sem pressão. Usar e relatar o que encontrar ajuda tanto quanto.
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a></p>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
|
||||
|
||||
Quer patrocinar o Strata? Fale comigo pelo [GitHub](https://github.com/Niko1221).
|
||||
@@ -1,217 +1,381 @@
|
||||
<h1 align="center">Strata</h1>
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="apps/host/public/brand/strata-lockup-dark.svg">
|
||||
<img src="apps/host/public/brand/strata-lockup-light.svg" alt="Strata" width="380">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[English](README.md) · **简体中文** · [日本語](README.ja.md) · [Deutsch](README.de.md) · [Français](README.fr.md) · [Español](README.es.md) · [Português](README.pt-BR.md)
|
||||
<p align="center"><a href="README.md">English</a> · <b>简体中文</b> · <a href="README.ja.md">日本語</a> · <a href="README.de.md">Deutsch</a> · <a href="README.fr.md">Français</a> · <a href="README.es.md">Español</a> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<p align="center"><b>在你自己的游戏电脑上运行 1250 亿参数的 AI 模型</b><br>
|
||||
NVIDIA 或 AMD 显卡(12 GB 及以上)· Windows 或 Linux · 免费开源</p>
|
||||
<h3 align="center">在你自己的游戏电脑上运行最先进的模型</h3>
|
||||
|
||||
<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 上下文)·
|
||||
<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>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4"><img src="docs/media/pagoda-preview.webp" width="720" alt="A voxel pagoda garden that Strata's model wrote, running in the browser"></a><br>
|
||||
<sub>一座体素宝塔花园,由一条提示词一次生成,在装有 RTX 5070 的电脑上用 Strata 运行(IQ3_S,128K 上下文) ·
|
||||
<a href="https://github.com/Niko1221/Strata/releases/download/v0.1.10/Pagoda.mp4">完整视频(49 秒)</a></sub></p>
|
||||
|
||||
Strata 能在普通电脑上运行 **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**。这是一个又大又聪明的
|
||||
AI 模型,通常要用服务器才能跑。它能聊天、写代码、看图片,还能配合你的应用和编程智能体一起工作。所有数据都留在你的电脑上。
|
||||
Strata 运行 **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)**。这是一个 1250 亿参数的模型,
|
||||
通常需要服务器才能跑,现在用普通电脑就行。它能聊天、写代码、看图片,也能配合你的应用和编程智能体使用。
|
||||
你的聊天记录和文件都留在你自己的电脑上。
|
||||
|
||||
## 速度有多快?
|
||||
**第一次用?** 先看[硬件要求](#硬件要求),再[安装应用](#方式-astrata-应用)。
|
||||
|
||||
我们在两台普通的游戏电脑上做了测试。一个 token 大约相当于 ¾ 个英文单词。
|
||||
- **游戏电脑就能跑。** 内存和显存加起来有 32 GB 就可以开始。
|
||||
- **能接入你的工具。** 支持 OpenAI 和 Anthropic 的 API,Claude Code、Codex CLI、OpenCode 等智能体都能连上。
|
||||
- **能看图片。** 默认开启。
|
||||
- **免费。** MIT 许可证,不用注册账号,不走云端。
|
||||
- **隐私。** 没有遥测。Strata 下载什么、什么时候联网、如何校验下载内容,都写在 [docs/SAFETY.md](docs/SAFETY.md)。
|
||||
|
||||
- **写回答:** 短对话中回复出现的速度。每秒 60 个 token 已经比你的阅读速度还快。
|
||||
- **读提示:** 读入你发送内容的速度(这里是一份 32K token 的文档、代码或聊天记录)。
|
||||
版本 0.2.1 · 引擎 0.1.46
|
||||
|
||||
<table>
|
||||
<tr><th>NVIDIA:RTX 5070(12 GB)、Ryzen 5 7600、64 GB 内存</th><th>AMD:RX 9070 XT(16 GB)、Ryzen 9 3900X、47 GB 内存</th></tr>
|
||||
<tr><td>
|
||||
## 硬件要求
|
||||
|
||||
| 规格 | 写回答 | 读提示 |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 94 tokens/s | 2,650 tokens/s |
|
||||
| **IQ2_XS** | 79 tokens/s | 2,090 tokens/s |
|
||||
| **IQ3_XXS** | 62 tokens/s | 1,750 tokens/s |
|
||||
| **IQ3_S** | 53 tokens/s | 1,620 tokens/s |
|
||||
| **Coder** | 55 tokens/s | 2,180 tokens/s |
|
||||
**内存和显存(VRAM)加起来至少要有 32 GB。** Mac 和 Ryzen AI Max 上指的是统一内存。内存越大,能跑的模型越大,
|
||||
或者速度越快。
|
||||
|
||||
</td><td>
|
||||
- 32 GB 内存加一张 12-16 GB 显存的显卡,可以跑 Coder。
|
||||
- 64 GB 内存可以跑所有尺寸。
|
||||
|
||||
| 规格 | 写回答 | 读提示 |
|
||||
| --- | ---: | ---: |
|
||||
| **Q2_0** | 60 tokens/s | 1,160 tokens/s |
|
||||
| **IQ2_XS** | 52 tokens/s | 1,110 tokens/s |
|
||||
| **Coder** | 44 tokens/s | 1,420 tokens/s |
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
|
||||
NVIDIA:Q2_0 用的是引擎 0.1.36,其他各行用的是 0.1.26(4K 回答,32K 提示)。完整表格见
|
||||
[DETAILS.md](docs/DETAILS.md#speed-measured)。显存越大的显卡越快:RTX 3090(24 GB)写回答应该能达到每秒约
|
||||
100-140 个 token。长对话和其他显卡的数据:[各模型的速度](docs/MODELS.md#how-fast-is-each-size)、
|
||||
[社区测试结果](docs/COMMUNITY_BENCHMARKS.md)。
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="请我喝杯咖啡" height="50"></a><br>
|
||||
<sub>Strata 是免费的。如果它在你的电脑上跑得不错,请我喝杯咖啡,让这个项目继续下去。</sub></p>
|
||||
|
||||
## 你需要什么
|
||||
|
||||
| | |
|
||||
| 你的电脑 | 说明 |
|
||||
| --- | --- |
|
||||
| **显卡** | **NVIDIA** GeForce RTX 20、30、40 或 50 系列,或 **AMD** Radeon RX 7900 XT / XTX、RX 7800 XT / 7700 XT、RX 9060 XT、RX 9070 / 9070 XT、Radeon AI PRO R9700 或 RX 6800 / 6900 系列。需要 **12 GB 或以上显存**。 |
|
||||
| **内存** | 32 GB 或以上。内存大小决定能装下[哪个模型](#该选哪个模型)。64 GB 可以运行所有规格。 |
|
||||
| **硬盘** | 约 80 GB 可用空间。尽量用 SSD:第一次启动会快很多。 |
|
||||
| **系统** | Windows 10 / 11 或 Linux,以及 NVIDIA 或 AMD 的最新显卡驱动。 |
|
||||
| **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) |
|
||||
|
||||
其他的都由安装程序搞定。两到三张显卡可以分担同一个模型([多 GPU](docs/MULTI_GPU.md))。
|
||||
另外还需要:
|
||||
|
||||
实验性支持,由社区成员在自己的机器上编写和测试:
|
||||
- 约 80 GB 的可用磁盘空间(用固态硬盘,首次启动会快很多)
|
||||
- Windows 10 / 11、Linux 或 macOS
|
||||
- 较新的显卡驱动
|
||||
|
||||
- **较老的显卡**(Tesla P40 / V100、GTX 10、Radeon VII / MI50、RX 6700 XT、RX 5500 XT):[较老的 GPU](docs/OLDER_GPUS.md)。
|
||||
- **Intel Arc**,在 Linux 上从源码构建:[Intel Arc](docs/INTEL_ARC.md)。
|
||||
- **AMD Ryzen AI Max(Strix Halo)**,在 Linux 上从源码构建:[Strix Halo](docs/STRIX_HALO.md)。
|
||||
- **不支持 AVX2 的老处理器**:能用,但很慢。[较老的 CPU](docs/INSTALL.md#older-cpus-experimental)。
|
||||
|
||||
完整列表:[docs/INSTALL.md](docs/INSTALL.md#what-you-need)。
|
||||
同一品牌的两三张显卡可以共同运行一个模型([多显卡](docs/MULTI_GPU.md))。Strata 不会把一个模型拆到 NVIDIA 和 AMD
|
||||
显卡上。完整列表见 [docs/INSTALL.md](docs/INSTALL.md#what-you-need)。
|
||||
|
||||
## 安装
|
||||
|
||||
### 让你的 AI 帮你装
|
||||
三种方式任选一种。它们启动的是同一个引擎,用的是同样的模型。
|
||||
|
||||
你在用 AI 编程助手吗(Claude Code、Cursor、Codex、GitHub Copilot 等)?把下面这段粘贴给它:
|
||||
### 方式 A:Strata 应用
|
||||
|
||||
最简单的方式。应用内置引擎:安装、选模型、开始聊天。请根据你的系统和显卡,从
|
||||
[发布页](https://github.com/Niko1221/Strata/releases/latest)下载对应的文件。不确定选哪个?看表格下面那一段。
|
||||
|
||||
| 系统 | 你的显卡 | 要下载的文件 |
|
||||
| --- | --- | --- |
|
||||
| **Windows 10 / 11** | NVIDIA RTX,驱动 580 或更新 | [`Strata-0.2.1-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda13.exe) |
|
||||
| | NVIDIA,较旧的显卡,或驱动低于 580 | [`Strata-0.2.1-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-cuda12.exe) |
|
||||
| | AMD Radeon RX 9000 系列或 Radeon AI PRO R9700 | [`Strata-0.2.1-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip.exe) |
|
||||
| | AMD Radeon RX 7000 系列或 Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX,驱动 580 或更新 | [`Strata-0.2.1-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA,较旧的显卡,或驱动低于 580 | [`Strata-0.2.1-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon RX 9000 系列或 Radeon AI PRO R9700 | [`Strata-0.2.1-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon RX 7000 系列 | [`Strata-0.2.1-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.1-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac**(Apple 芯片,macOS 15 及以上) | 内置 | [`Strata-0.2.1-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.1-mac.dmg) |
|
||||
|
||||
**不确定时:** NVIDIA 显卡的驱动版本在 580 及以上就选 `cuda13`,否则选 `cuda12`。`cuda12` 适用于较旧的显卡和驱动。
|
||||
`hip` 和 `amd` 的文件是给 AMD 显卡用的。其他 AMD 显卡(RX 6000)目前还没有现成的应用,
|
||||
请看 [AMD 显卡](docs/AMD_HIP.md)。
|
||||
|
||||
然后:
|
||||
|
||||
- **Windows:** 运行 `.exe`。它只为当前用户安装,不需要管理员权限。如果 Windows 提示“Windows 已保护你的电脑”,
|
||||
点击**更多信息**,再点**仍要运行**(安装程序还没有代码签名)。
|
||||
- **Linux:** 给文件加上可执行权限(`chmod +x`),然后启动。需要 FUSE 2:`sudo apt install libfuse2`(Ubuntu 22.04),
|
||||
或 `libfuse2t64`(Ubuntu 24.04 及更新)。
|
||||
- **Mac:** 打开 `.dmg`,把 Strata 拖到“应用程序”。
|
||||
|
||||
第一个界面里,应用会检测你的电脑,并推荐合适的模型。点击安装即可。模型很大,要下载约 40-70 GB,中断后会从断点继续。
|
||||
关闭窗口会同时停止 Strata 和引擎。
|
||||
|
||||
### 方式 B:终端(一行命令)
|
||||
|
||||
终端版就是 `strata` 命令,适合脚本、服务器和 AI 智能体。
|
||||
|
||||
Linux 和 Mac:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
```
|
||||
|
||||
Windows(PowerShell):
|
||||
|
||||
```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` 运行。
|
||||
所有界面和参数见 [docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md)。
|
||||
|
||||
### 方式 C:让你的 AI 来安装
|
||||
|
||||
把下面这段话粘贴到 Claude Code、Cursor、Codex、GitHub Copilot 或其他编程助手里:
|
||||
|
||||
```text
|
||||
Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow docs/AI_SETUP.md in that repository.
|
||||
```
|
||||
|
||||
它会检查你的显卡、内存和硬盘,选出合适的模型。然后安装并启动它,再告诉你怎么连接你的应用。AI 工具也可以通过
|
||||
Strata 的 [MCP 服务器](docs/MCP_SERVER.md)来安装、启动和停止 Strata。
|
||||
它会检查你的显卡、内存和磁盘,选出合适的模型,完成安装和启动,并告诉你怎么连接你的应用。AI 工具也可以通过 Strata 的
|
||||
[MCP 服务器](docs/MCP_SERVER.md)安装、启动和停止 Strata。
|
||||
|
||||
### 或者自己动手
|
||||
想自己动手编译?请看[从源码构建](#从源码构建)。
|
||||
|
||||
[下载 Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip) 并解压(或者用 `git clone`)。
|
||||
**Windows:** 双击 **`START-HERE.bat`**。**Linux:** 在 Strata 文件夹里运行 **`./setup.sh`**。
|
||||
## 模型
|
||||
|
||||
**Mac(Apple 芯片,M1 到 M5,macOS 15 或更新版本):** 先安装一次 Apple 的 Command Line Tools(在终端里运行 `xcode-select --install`;不需要 Xcode 本身)。然后在 Strata 文件夹里打开终端,运行 **`./setup.sh`**。它会在你的 Mac 上编译 Mac 版引擎(大约 7 分钟),问同样的问题,下载模型并启动。下次运行 `./setup.sh` 会马上启动。哪个大小适合你的 Mac、速度如何:[docs/MAC.md](docs/MAC.md)。
|
||||
应用和 `strata launch` 会按你的内存推荐模型。它是同一个模型的几种尺寸,压缩程度不同。尺寸小的更快,尺寸大的稍微更聪明一点。
|
||||
|
||||
NVIDIA 和 AMD 的步骤完全一样。安装程序会识别你的显卡,并装好对应的引擎。它会问你几个问题:
|
||||
### 内存不够?试试 Gyro
|
||||
|
||||
- 用哪个模型、哪个规格,
|
||||
- 上下文多大(模型能记住多少文字),
|
||||
- 是否要识别图片。
|
||||
[Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF)(作者 agentionai)是运行这个模型占用最小的方式。
|
||||
|
||||
每次直接按回车就是推荐选项。之后它会下载模型(约 70 GB)并启动。如果下载中断了,再运行一次即可:它会从中断的地方继续。
|
||||
浏览器会打开 Strata 应用,地址是 `http://127.0.0.1:8080`。
|
||||
| 尺寸 | 所需内存(RAM + VRAM) | 下载大小 | 说明 |
|
||||
| --- | ---: | ---: | --- |
|
||||
| **Gyro-S** | 27.6 GB | 58.5 GB | 给其他尺寸都放不下的电脑。质量低于 IQ2_XS。 |
|
||||
| **Gyro-M** | 35 GB | 92.0 GB | 质量约相当于 IQ3_XXS,显存占用少约 20%。在 12 GB 显卡和 R9700 上,输出回答比 Q2_0 慢 17-21%。 |
|
||||
|
||||
> **模型启动时,你的电脑可能会变慢或卡住 1-3 分钟**(第一次最久)。
|
||||
> Strata 会把 35-55 GB 加载到内存,并为显卡锁定其中一部分。这是正常的。请耐心等待,不要关闭窗口。
|
||||
> 窗口里会显示 Strata 正在做什么。
|
||||
Gyro 可在 NVIDIA 和 AMD 显卡上运行(AMD 为实验性支持),不支持 Intel Arc 和 Mac。详情和测试数据:
|
||||
[docs/GYRO.md](docs/GYRO.md)。用 `strata launch --model gyro-s` 启动。
|
||||
|
||||
**下次使用时**,再运行一次 `START-HERE.bat`(或 `./setup.sh`)。它会马上启动,已下载的东西不会重复下载。关闭它的窗口就能停止模型。
|
||||
`UPDATE.bat`(`./update.sh`)只更新 Strata,不启动。更新、Docker、多张显卡、文件存放位置以及所有选项:
|
||||
[docs/INSTALL.md](docs/INSTALL.md)。
|
||||
### 其他尺寸
|
||||
|
||||
## 该选哪个模型
|
||||
|
||||
安装程序会根据你的内存推荐一个。同一个模型有几种规格,压缩程度不同。规格越小越快,越大越聪明一些。
|
||||
|
||||
| 你的内存 | 选择 | 原因 |
|
||||
| 你的内存(RAM + VRAM) | 选择 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Coder** | 32 GB 装得下,而且专为代码打造(如果显卡是 24 GB,Q2_0 和 IQ2_XS 也能跑) |
|
||||
| **48 GB** | **IQ2_XS**(或 Q2_0,最快) | 更大的规格装不下 |
|
||||
| **64 GB** | **IQ2_XS**(推荐),或 IQ3_XXS / IQ3_S | 所有规格都装得下;IQ3_S 最好,也最慢 |
|
||||
| **96 GB 或以上** | **IQ3_S**,或 Unsloth 的 UD-IQ4_XS(约 4-bit) | 开着其他程序也能放下最大的规格 |
|
||||
| **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 慢 17-21% |
|
||||
| **48 GB** | **IQ3_XXS**(47.0 GB) | 更慢,更好 |
|
||||
| **64 GB** | **IQ3_S**(54.8 GB),原版或 Swift 1.5 | 最慢,最好:在公开测试中与完整模型持平 |
|
||||
| **96 GB 及以上** | Unsloth 的 **UD-IQ4_XS**(约 72 GB 内存)到 **Q8_0**(约 136 GB),4 到 8 位 | 更接近完整模型。内存不足时,Strata 会从固态硬盘读取一部分,速度会慢很多 |
|
||||
|
||||
- **[Coder](docs/MODELS.md#coder):** 编程版本,去掉了一半专家。它达到完整模型 SWE-bench Verified 分数的 91%(由其作者测得),
|
||||
32 GB 内存就能装下。代码以外的能力较弱,包括中文和其他中日韩文本(#438)。这类用途请选 Q2_0、IQ2_XS 或 IQ3_S,
|
||||
它们保留了所有专家。
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15):** 一个微调版本,回答前思考的时间短得多。你能更早拿到答案,质量基本不变。
|
||||
- **[Unsloth UD-IQ4_XS](docs/MODELS.md#unsloth-ud-iq4_xs):** Unsloth 的约 4-bit 版本,质量介于 IQ3_S 和 UD-Q4_K_XL 之间。
|
||||
下载 94 GB。内存少于约 80 GB 时,Strata 回答时要从 SSD 读取其中一部分,所以会更慢(NVMe SSD 有帮助)。
|
||||
- **[Unsloth UD-Q4_K_XL、UD-Q5_K_XL、UD-Q6_K_XL 和 Q8_0](docs/MODELS.md#unsloths-files-ud-iq4_xs-to-q8_0):** 4 到 8 位,更接近完整模型(下一个 token
|
||||
与 Q8_0 相同的比例为 93-95%)。所有专家都能放进内存时速度很快(UD-Q4_K_XL 约需 88 GB,Q8_0 约需 136 GB);内存不够时,
|
||||
Strata 回答时要从 SSD 读取一部分,会慢很多(64 GB 的电脑上 UD-Q4_K_XL 每秒 7-8.5 个 token)。
|
||||
- **[OrcaRouter 的 Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs):** 需要手动设置,不在安装程序的菜单里。
|
||||
内存比这一行写的少,也可能能用:Strata 会自动切换到[低内存模式](docs/MODELS.md#a-big-graphics-card-and-little-ram)。
|
||||
速度会变慢,显卡较小时会慢得多。
|
||||
|
||||
规格、下载以及各配置能装下什么:[docs/MODELS.md](docs/MODELS.md)。以后想再添加模型,运行
|
||||
`SETUP.bat`(Linux:`./setup.sh --setup`)。
|
||||
更多选择:
|
||||
|
||||
- **[Swift 1.5](docs/MODELS.md#swift-15)** 是微调版本,回答前思考的时间更短,质量和内存占用大致相同。
|
||||
- **[NVFP4](docs/NVFP4.md)** 是 NVIDIA 为 NVIDIA 显卡提供的 4 位版本。要把所有专家放进内存,需要约 80 GB 内存。
|
||||
- **[Qwen3.6-35B-A3B](docs/QWEN36.md)** 是另一个更小的模型,能看图片,还能同时回答多个请求。
|
||||
- **[OrcaRouter 的 Uncensored IQ3_XXS](docs/MODELS.md#orcarouter-uncensored-iq3_xxs)** 需要手动设置。
|
||||
|
||||
所有尺寸、下载大小,以及各种配置能跑什么:[docs/MODELS.md](docs/MODELS.md)。
|
||||
|
||||
## 速度如何?
|
||||
|
||||
以下数据基于 12 GB 显卡(RTX 5070、Ryzen 5 7600、64 GB 内存)。其他显卡的结果见:
|
||||
[各模型的速度](docs/MODELS.md#how-fast-is-each-size)、[社区测试结果](docs/COMMUNITY_BENCHMARKS.md)。
|
||||
|
||||
| 模型 | 回答输出速度 | 读取提示词速度 |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████████` 94 tokens/s | `████████████` 2,650 tokens/s |
|
||||
| **IQ2_XS** | `██████████░░` 79 tokens/s | `█████████░░░` 2,090 tokens/s |
|
||||
| **IQ3_XXS** | `████████░░░░` 62 tokens/s | `████████░░░░` 1,750 tokens/s |
|
||||
| **IQ3_S** | `███████░░░░░` 53 tokens/s | `███████░░░░░` 1,620 tokens/s |
|
||||
| **Coder** | `███████░░░░░` 55 tokens/s | `██████████░░` 2,180 tokens/s |
|
||||
|
||||
一个 token 约等于 ¾ 个英文单词,所以每秒 60 个 token 比你阅读的速度还快。“读取提示词”指 Strata 吸收长文档、代码或聊天历史的速度。
|
||||
|
||||
在 16 GB 的 AMD 显卡上测得的结果(RX 9070 XT、Ryzen 9 3900X、47 GB 内存):
|
||||
|
||||
| 模型 | 回答输出速度 | 读取提示词速度 |
|
||||
| --- | --- | --- |
|
||||
| **Q2_0** | `████████░░░░` 60 tokens/s | `█████░░░░░░░` 1,160 tokens/s |
|
||||
| **IQ2_XS** | `███████░░░░░` 52 tokens/s | `█████░░░░░░░` 1,110 tokens/s |
|
||||
| **Coder** | `██████░░░░░░` 44 tokens/s | `██████░░░░░░` 1,420 tokens/s |
|
||||
|
||||
两张表的进度条使用同一个比例。显存更大的显卡更快:RTX 3090(24 GB)每秒大约能输出 100-140 个 token。每个数字对应的引擎版本见
|
||||
[DETAILS.md](docs/DETAILS.md#speed-measured)。
|
||||
|
||||
## 使用方法
|
||||
|
||||
<p align="center"><img src="docs/media/runpagoda.png" width="900" alt="Strata 应用的 Monitor 标签页,旁边是一个编程智能体"><br>
|
||||
<sub>Strata 应用的 <b>Monitor</b>(左),编程智能体正在写视频里的宝塔花园(右)</sub></p>
|
||||
### 在应用里
|
||||
|
||||
- **在浏览器里:** 打开 `http://127.0.0.1:8080`。里面有 **Chat**(聊天)、实时 **Monitor**(监控模型和你的
|
||||
GPU/CPU/内存),以及 **About**(设置和地址)。
|
||||
- **你的应用和编程智能体:** 添加一个“OpenAI 兼容”的提供商,base URL 填 **`http://127.0.0.1:8080/v1`**。
|
||||
API key 和模型名随便填都行。
|
||||
- 使用 Anthropic API 的应用:`http://127.0.0.1:8080/v1/messages`(Claude Code:
|
||||
`ANTHROPIC_BASE_URL=http://127.0.0.1:8080`)。
|
||||
- Codex CLI 和其他使用 OpenAI Responses API 的应用:`/v1/responses`
|
||||
([设置方法](docs/DETAILS.md#the-responses-api-and-codex-cli))。
|
||||
- **思考:** 在聊天菜单或应用的“reasoning effort”里选择 **off、low、medium 或 high**。off 最快,
|
||||
high 最适合难题。
|
||||
- **图片:** 在安装时对“Images?”选是。然后在聊天里点 **Picture**,或在你的应用里附上图片。
|
||||
AMD 显卡在 Linux 上通过处理器识别图片;在 Windows 上暂时还不行。
|
||||
- **从手机或另一台电脑访问:** `START-HERE.bat --setup --host 0.0.0.0 --api-key <secret>`。一定要设置 key。
|
||||
- **一次处理一个请求:** 默认情况下 Strata 一次只回答一个请求,其他请求排队等待。想同时回答多个,
|
||||
设置 `"parallel": 2`([BATCHING.md](docs/BATCHING.md))。在 12 GB 显卡上,这会让每个回答变慢。
|
||||
- **长提示:** Strata 会完整读入对话的第一条消息,大约每 30,000 个 token 需要 1 分钟。之后的消息几秒内就开始回答。
|
||||
应用包含**聊天**、显示模型以及 GPU、CPU 和内存状态的**监控**、**模型**页面和**设置**。聊天可以在你电脑上的某个文件夹里工作。
|
||||
在**手动**模式下,修改文件或运行命令之前它会先询问你。输入 `/loop 10m <要做的事>`,在聊天保持打开期间每 10 分钟重复发送一次消息。
|
||||
|
||||
更多:[聊天记录存在哪里](docs/INSTALL.md#where-things-are-stored)、[API](docs/DETAILS.md#using-it)。
|
||||
聊天可以用 DuckDuckGo(或 Bing)搜索网页,不需要账号。你也可以在**设置,工具**里添加搜索密钥。搜索是唯一会把内容发出电脑的功能,
|
||||
发出的就是搜索词。
|
||||
|
||||
### 可选:整个模型都能放进内存时,单卡再提速
|
||||
要同时运行两个模型,请打开**运行多个模型**,并为每个模型分配同一品牌的单独一张显卡。
|
||||
|
||||
两个可选设置(来自 RubenTorney 的 #1904 和 #1905)。适用于只用一张显卡、并且内存能放下所有专家的情况(Strata 的 arena 模式,例如 64 GB 电脑上的 Q2_0 或 IQ3_XXS)。打开 Strata 数据文件夹里的配置文件 `strata-<model>.json`([在哪里](docs/INSTALL.md#where-things-are-stored)),加入:
|
||||
<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>
|
||||
|
||||
- **生成更快:** 在 `"args"` 列表里加 `"--adapt-async", "1"`。专家缓存改在辅助线程里更新,不再暂停回答。在 RTX 5070(Windows,Q2_0)上实测:生成 +4-5%,代码和长文档 +7-10%。回答可能与默认设置略有不同(但每次运行都一样)。
|
||||
- **Windows 上读取提示更快:** 在 `"env"` 块里加 `"STRATA_PIN_BY_NEED": "1"`。Windows 只能为显卡锁定大约一半的内存;这个设置会先锁定显卡真正需要的专家。在 RTX 5070 上实测:读取提示 +4.4%。
|
||||
### 在终端里
|
||||
|
||||
<p align="center"><img src="docs/media/terminal-monitor.png" width="900" alt="The Strata Terminal monitor overview while the model writes a reply"><br>
|
||||
<sub>模型回复时的 <code>strata monitor</code>:速度、GPU、内存和最近的请求</sub></p>
|
||||
|
||||
```sh
|
||||
strata launch # 引导界面:模型、上下文、图片、访问方式,然后开始服务
|
||||
strata launch --model q2_0 --yes --no-tui # 不提问:开始服务,打印地址,然后返回
|
||||
strata launch --plan --model q2_0 # 显示将要执行的操作;不做任何更改
|
||||
strata launch --status # 当前正在提供什么服务
|
||||
strata launch --stop # 停止服务
|
||||
strata monitor --url http://127.0.0.1:8080 # 查看速度、GPU、内存和请求
|
||||
strata update --check # 有新引擎吗?(strata update 会安装它)
|
||||
strata doctor # 检查已安装的引擎文件
|
||||
```
|
||||
|
||||
`strata --help` 会列出所有命令。启动和提供服务时不会下载任何东西。只有安装、`strata launch`(在你选择模型时)和
|
||||
`strata update` 会下载。要从手机或另一台电脑访问 Strata,请使用 `strata launch --access lan`。它会生成一个 API 密钥,
|
||||
没有密钥就不会在网络上启动。除非你加上 `--open`,否则不会打开浏览器。指南:[docs/TERMINAL_LAUNCH.md](docs/TERMINAL_LAUNCH.md)。
|
||||
完整的客户端说明:[docs/TERMINAL.md](docs/TERMINAL.md)。
|
||||
|
||||
### 配合 AI 智能体
|
||||
|
||||
智能体用方式 B 中的一行命令安装 Strata,然后运行 `strata launch --model NAME --yes --no-tui`。步骤、参数和退出码见
|
||||
[docs/AI_SETUP.md](docs/AI_SETUP.md),仓库规则见 [AGENTS.md](AGENTS.md)。
|
||||
|
||||
### 连接你的其他应用
|
||||
|
||||
| 应用类型 | 基础 URL |
|
||||
| --- | --- |
|
||||
| 兼容 OpenAI(大多数应用和智能体) | `http://127.0.0.1:8080/v1`(API 密钥和模型名称随便填) |
|
||||
| Anthropic API(Claude Code 等) | `http://127.0.0.1:8080`,不带 `/v1`。这些应用会自己加上 `/v1/messages`。Claude Code:`ANTHROPIC_BASE_URL=http://127.0.0.1:8080` |
|
||||
| OpenAI Responses API(Codex CLI) | `http://127.0.0.1:8080/v1/responses`([设置方法](docs/DETAILS.md#the-responses-api-and-codex-cli)) |
|
||||
|
||||
`strata connect` 会打印 OpenCode、Codex CLI、Claude Code、oh-my-pi、pi、Hermes Agent 和 OpenClaw 的具体设置。
|
||||
`strata connect opencode --write` 会在询问后写入配置文件,并保留备份。
|
||||
|
||||
### 图片
|
||||
|
||||
能看图片的模型默认开启图片功能。在聊天里附加或粘贴图片,或者从你的应用发送图片。显卡的显存小于 24 GB 时,读图由处理器完成
|
||||
(每张图片几秒钟,不占用显卡的显存);显存达到 24 GB 时,由显卡完成。NVIDIA 和 AMD 的规则相同。你可以按模型关闭图片功能,
|
||||
也可以用 `--vision off`、`--vision gpu` 或 `--vision cpu` 选择读图方式。
|
||||
|
||||
这条规则背后的数据:在 Radeon AI PRO R9700 上,显卡读一张 260 token 的图片用了 57 ms,处理器用了 3.1 秒。在 16 GB 的显卡
|
||||
(Tesla P100 和 RTX A4000,Linux)上,用显卡读图会让文字速度下降 3-5%,因为它占用约 1.4 GB 显存,否则这部分会被模型使用。
|
||||
在 Mac 上,无论内存多大,都由 GPU 读图(32 GB 的 M4 上读一张 260 token 的图片用 0.45 秒,占用约 1.2 GB 共享内存);处理器读一张约需 50 秒,所以只有指定 `--vision cpu` 时才用。
|
||||
|
||||
### 使用须知
|
||||
|
||||
- 在聊天菜单或你的应用的“reasoning effort”里选择思考级别(**off、low、medium 或 high**)。off 最快。
|
||||
- Strata 默认一次回答一个请求。要同时处理多个,请在 `strata launch` 中使用 `--parallel N`,或在配置里写
|
||||
`"parallel": 2`([BATCHING.md](docs/BATCHING.md))。在 12 GB 显卡上,这会让每个回答变慢。
|
||||
- Strata 会完整读取聊天的第一条消息,每 30,000 个 token 约需 1 分钟。之后的消息几秒钟就开始。
|
||||
- 聊天记录存放的位置和完整的 API:[docs/INSTALL.md](docs/INSTALL.md#where-things-are-stored)、[docs/DETAILS.md](docs/DETAILS.md#using-it)。
|
||||
|
||||
## 从源码构建
|
||||
|
||||
<details>
|
||||
<summary>从仓库构建 Windows、Linux 和 Mac 版本</summary>
|
||||
|
||||
[下载 Strata](https://github.com/Niko1221/Strata/archive/refs/heads/main.zip)并解压,或者用 `git clone` 获取。
|
||||
**Windows:** 双击 `START-HERE.bat`。**Linux:** 在 Strata 文件夹里运行 `./setup.sh`。
|
||||
|
||||
**Mac(Apple 芯片,macOS 15 及以上):** 先用 `xcode-select --install` 安装一次 Apple 的命令行工具(不需要完整的 Xcode)。
|
||||
在 Strata 文件夹里打开终端,运行 `./setup.sh`。它会在你的 Mac 上构建 Mac 引擎(约 7 分钟),问同样的问题,下载模型并启动。
|
||||
下次运行 `./setup.sh` 会直接启动。你的 Mac 适合哪个尺寸:[docs/MAC.md](docs/MAC.md)。
|
||||
|
||||
安装程序会找到你的显卡,并设置合适的引擎。它会问你用哪个模型和尺寸、上下文多长(模型能记住多少文字),以及是否要读取图片。
|
||||
直接按回车就是推荐的选择。然后它会下载模型(约 70 GB)并启动。如果下载中断,再运行一次就会继续。浏览器会打开
|
||||
`http://127.0.0.1:8080` 上的 Strata 页面。
|
||||
|
||||
> 模型启动期间,你的电脑可能会变慢,或者在 1-3 分钟内没有响应(第一次最久)。Strata 会把 35-55 GB 加载到内存里,并锁定其中一部分
|
||||
> 给显卡使用。请等待,不要关闭窗口。
|
||||
|
||||
下次再运行 `START-HERE.bat`(或 `./setup.sh`)会立即启动,不会重复下载。关闭它的窗口就能停止模型。`UPDATE.bat`
|
||||
(`./update.sh`)只更新 Strata,不启动。更新、Docker、多显卡、文件存放位置和所有选项:[docs/INSTALL.md](docs/INSTALL.md)。
|
||||
|
||||
要测试你的电脑,运行 `BENCH.bat`(`./bench.sh`)。需要 5-10 分钟,并生成一份可以分享的报告
|
||||
([方法](docs/COMMUNITY_BENCHMARKS.md#run-the-benchmark))。
|
||||
|
||||
**可选:整个模型都放得进内存时,让单卡更快。** 这是 RubenTorney 提供的两个需要手动开启的设置(#1904、#1905)。
|
||||
当你只用一张显卡,并且内存装得下所有专家时,它们会有帮助,例如在 64 GB 的电脑上跑 Q2_0 或 IQ3_XXS。打开 Strata 数据文件夹里的
|
||||
`strata-<model>.json`([位置](docs/INSTALL.md#where-things-are-stored)),然后添加:
|
||||
|
||||
- 在 `"args"` 列表中加入 `"--adapt-async", "1"`。这样专家缓存会在辅助线程上更新,不会让回答暂停。在 RTX 5070
|
||||
(Windows,Q2_0)上测得:解码快 4-5%,代码和长文档快 7-10%。回答可能与默认设置略有不同,但每次运行的结果保持一致。
|
||||
- 在 `"env"` 块中加入 `"STRATA_PIN_BY_NEED": "1"`(Windows)。Windows 只能为显卡锁定大约一半的内存,
|
||||
这个设置会优先锁定显卡最先需要的专家。在 RTX 5070 上测得:读取提示词快 4.4%。
|
||||
|
||||
```json
|
||||
"args": [ ..., "--adapt-async", "1" ],
|
||||
"env": { "STRATA_PIN_BY_NEED": "1" }
|
||||
```
|
||||
|
||||
修改后重启 Strata(`START-HERE.bat` 或 `./setup.sh`)。要撤销,删掉这两项即可。
|
||||
修改后请重启 Strata。要撤销,删除这两项即可。
|
||||
|
||||
</details>
|
||||
|
||||
## 工作原理
|
||||
|
||||
这类模型通常运行在有几百 GB 显存的服务器上。你的显卡只有 12-24 GB。Strata 把工作分给整台电脑,让模型装得下。
|
||||
可以把它想成一座图书馆。
|
||||
|
||||
- 模型是一个由 24,576 位小专家(“experts”)组成的团队,好比 24,576 本参考书。每个词只需要其中 10 本。
|
||||
- **你的显卡是书桌。** 它小而快。最常用的大约 4,000 本书摊开放在上面。
|
||||
- **你的内存是阅览室的书架。** 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>
|
||||
|
||||
另外有一个小助手会猜接下来的几个词。大模型一次检查所有猜测,保留正确的部分。得到的答案不变,但快了 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>
|
||||
|
||||
更详细的解释:[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)。每个部分和它的数据:
|
||||
[详细说明](docs/DETAILS.md#how-it-works)和[论文](docs/paper/Strata-Paper.pdf)。
|
||||
|
||||
## 出问题了?
|
||||
|
||||
- **Strata 第一次启动时电脑卡死了。** 加载模型时这是正常的。请耐心等待,不要关闭窗口。
|
||||
10 分钟后还卡着?重启电脑,关掉其他程序再试一次,或者换一个更小的规格。
|
||||
- **下载或安装时中断了。** 再运行一次 `START-HERE.bat`(或 `./setup.sh`)。它会从中断的地方继续。
|
||||
- **非常慢,硬盘灯一直闪,或者提示“the engine stopped unexpectedly”。** 你的电脑可用内存不够。
|
||||
关掉其他程序(浏览器很占内存),或者换一个更小的规格(Q2_0 或 IQ2_XS)。
|
||||
- **第一次启动 Strata 时我的电脑卡住了。** 它在把模型加载到内存。请等待,不要关闭窗口。10 分钟后还是卡住?重启电脑,
|
||||
关闭其他程序后再试,或者选一个更小的尺寸。
|
||||
- **下载或安装时中断了。** 重新启动应用或 `strata launch`,或者再运行一次 `START-HERE.bat`(或 `./setup.sh`)。
|
||||
它会从中断的地方继续。
|
||||
- **非常慢、硬盘灯一直闪,或者提示“引擎意外停止”。** 你的电脑可用内存不够。关闭其他程序(浏览器很占内存),
|
||||
或者选一个更小的尺寸,比如 Q2_0 或 IQ2_XS。
|
||||
- **提示这台电脑没有可用的引擎。** 在应用里打开**设置,引擎**,点击**更新引擎**。在终端里运行 `strata update`。
|
||||
`strata doctor` 会显示已安装哪些引擎文件。
|
||||
- **提示 8080 端口已被占用。** Strata 已经在运行了。找一下它的窗口。
|
||||
|
||||
更多问题和解决办法:[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)。还是解决不了?提交一个
|
||||
[issue](https://github.com/Niko1221/Strata/issues),并附上 Strata 文件夹里的 `strata-<model>.log`。发现了安全问题?
|
||||
请私下报告:[SECURITY.md](SECURITY.md)。
|
||||
|
||||
## 它是怎么工作的?
|
||||
|
||||
这类模型通常运行在拥有数百 GB 显存的服务器上。而你的显卡只有 12-24 GB。Strata 让模型装得下的办法是
|
||||
**把工作分摊到整台电脑上**。可以想象一个厨房:常用的东西放在台面上,其余的放在储藏室里。
|
||||
|
||||
<p align="center"><img src="docs/media/how-it-works.svg" width="860" alt="模型的 24,576 个专家:最常用的在显卡上,全部在内存里,一张查找表在 SSD 上"></p>
|
||||
|
||||
- **这个模型是由 24,576 个小专家(“experts”)组成的团队。** 每个词只需要其中 10 个。
|
||||
- **你的显卡** 存放最常用的几千个专家。**你的内存** 存放全部专家,
|
||||
**你的处理器** 同时处理其余的部分。**你的 SSD** 存放一张大查找表。
|
||||
|
||||
<p align="center"><img src="docs/media/guess-and-check.svg" width="860" alt="一个小助手猜测接下来的几个词;大模型一次性全部检查,保留正确的"></p>
|
||||
|
||||
- **先猜,再检查:** 一个小助手先猜接下来的几个词,大模型再一次性检查它们。答案完全一样,但快 1.6-1.8 倍。
|
||||
- **长文本分大块读入**(每次最多 8,192 个 token),速度超过每秒 1,000 个 token。
|
||||
|
||||
更详细的解释:[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md)。每个部分及其数据:
|
||||
[详细说明](docs/DETAILS.md#how-it-works)和[论文](docs/paper/Strata-Paper.pdf)。
|
||||
更多问题和解决办法:[docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)。还是解决不了?请提交
|
||||
[issue](https://github.com/Niko1221/Strata/issues)。在应用里,附上**设置,引擎,诊断,查看日志,保存日志**得到的文件。
|
||||
在终端里,粘贴 `strata doctor` 的输出。从源码运行时,附上 Strata 文件夹里的 `strata-<model>.log`。
|
||||
如果是安全问题,请私下报告:[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)
|
||||
和 Unsloth 压缩。Strata 使用了 [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) 的部分代码。完整致谢:
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits)。Strata 以 [MIT 许可证](LICENSE)开源。少数部分和每个模型
|
||||
有各自的许可证([具体是哪些](docs/HOW_IT_WORKS.md#license))。
|
||||
模型是 Qwen 团队的 [Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next)。压缩工作由
|
||||
[ISTA-DASLab](https://huggingface.co/ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF)、UkisAI(Swift 1.5)、agentionai(Gyro)
|
||||
和 Unsloth 完成。Strata 使用了 [llama.cpp / ggml](https://github.com/ggml-org/llama.cpp) 的部分代码。全部致谢:
|
||||
[docs/HOW_IT_WORKS.md](docs/HOW_IT_WORKS.md#credits)。Strata 以 [MIT 许可证](LICENSE)开源。少数部分和所有模型有各自的许可证
|
||||
([具体是哪些](docs/HOW_IT_WORKS.md#license))。
|
||||
|
||||
## 参与贡献
|
||||
|
||||
发现了 bug,或者想提交修改或基准测试结果?请先阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。其中说明了需要附上哪些内容,
|
||||
方便其他人核实你的工作。
|
||||
|
||||
## 支持 Strata
|
||||
|
||||
Strata 免费且开源。如果它对你有用,欢迎支持它的开发:
|
||||
Strata 免费,采用 MIT 许可证。我在能拿到的各种硬件上开发和测试它。如果它在你的电脑上跑得不错,请我喝杯咖啡,能帮我继续测试新显卡、
|
||||
修复你们反馈的问题。不用有压力。使用它并反馈你的发现,同样很有帮助。
|
||||
|
||||
<p align="center"><a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="请我喝杯咖啡" height="50"></a></p>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50"></a>
|
||||
|
||||
有兴趣赞助 Strata?请通过 [GitHub](https://github.com/Niko1221) 联系我。
|
||||
@@ -43,4 +43,53 @@ this PC only. The details and every setting are in [docs/DETAILS.md](docs/DETAIL
|
||||
last few prompts, whatever `"api_monitor"` says, to encode the next turn faster; no endpoint shows them, and they
|
||||
are gone when the server stops.
|
||||
|
||||
## The desktop app and the Terminal
|
||||
|
||||
Strata comes in three parts that share one engine: the **desktop app**, the **Terminal** (`strata`) and the **engine** that runs
|
||||
the model. The section above is about the engine's HTTP server. This is what the other two add.
|
||||
|
||||
- **The app's host listens on this PC only.** The app serves its page from a local host process on `127.0.0.1` (port 8791 unless
|
||||
changed). A request to its `/api/` endpoints that carries a browser `Origin` from any other site gets 403. The app shell holds a
|
||||
random secret (32 characters or more, compared in constant time) that is needed to quit the host or tell it which window is open,
|
||||
and the host gives its own helper processes a second random secret. By default the models the app starts are served on this PC only.
|
||||
- **Sharing is a separate, key-protected listener.** "Share with other devices" (and the Cloudflare tunnel) will not start until an
|
||||
API key is set. It opens a different listener that answers only `/v1/models`, `/v1/chat/completions` and `/v1/messages`, never the
|
||||
app's page or its host endpoints.
|
||||
- **App and Terminal talk through an owner channel.** One owner process per installation starts and ends the engine, and a second
|
||||
client (the Terminal) attaches to it. On macOS and Linux that is a private Unix socket in a folder only you can open. On Windows it
|
||||
is a TCP listener on `127.0.0.1` that answers only requests carrying a 256-bit token kept in an owner-only folder. That is a weaker
|
||||
guarantee than a socket other users cannot open: on Windows the token is what protects it, as with any loopback service that has a
|
||||
token (`strata_control/owner_transport.py`). The bridge between the app shell and the host (used for the app update) is also loopback and protected by a token.
|
||||
- **Starting a model and serving never download anything.** Downloads and updates are one explicit step: `strata update`, or "Update
|
||||
engine" and "Update Strata" in the app's Settings. Models and saved tuning are never replaced by an update.
|
||||
- **Updates come from a signed feed.** The release publishes `strata-feed.json` and `strata-feed.json.sig` (Ed25519). An installed
|
||||
Strata checks the signature against the public key it carries (`strata_control/update_keys.json`), refuses a feed older than the last
|
||||
one it accepted (a replayed old feed) or expired, and checks every engine package against its SHA-256, its signed manifest and the
|
||||
card in your PC before it test-starts it and switches to it. Any failure keeps the engine you had. The app installer itself is signed
|
||||
with a separate key (Tauri's minisign key) that the app and the Terminal both check. The private keys are kept offline by the owner.
|
||||
See [packaging/UPDATES.md](packaging/UPDATES.md).
|
||||
|
||||
### How far to trust `curl | sh` and `irm | iex`
|
||||
|
||||
The Terminal installs with one line:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://github.com/Niko1221/Strata/releases/latest/download/install.sh | sh
|
||||
irm https://github.com/Niko1221/Strata/releases/latest/download/install.ps1 | iex
|
||||
```
|
||||
|
||||
Piping a script into a shell runs whatever the server sends, so it deserves the same care as running any downloaded program.
|
||||
What the script does is short and written at its top: it fetches the Terminal archive named in the release's `install-manifest.txt`,
|
||||
checks its SHA-256, unpacks it into your home folder (no administrator rights, nothing outside your home), checks the release's signed feed
|
||||
with the Terminal it just unpacked and removes it again if that fails, and puts `strata` on your PATH.
|
||||
|
||||
Those checks have a limit. The checksum comes from the same release as the archive, and the public key that checks the feed arrives inside
|
||||
that archive, so for the first install all trust rests on HTTPS and on the `Niko1221/Strata` GitHub release itself. The checks catch a
|
||||
damaged or half-finished download and a feed that was not signed by the release key; they do not protect you if the release has been replaced
|
||||
by someone who controls it. Later updates are checked against the key already on your PC, which is a stronger position.
|
||||
|
||||
If that is not enough: save the script (`curl -fsSLO https://github.com/Niko1221/Strata/releases/latest/download/install.sh`), read it, and
|
||||
run it from the file; or install the desktop app or build from source instead and check the code you build.
|
||||
|
||||
Strata has not had an outside security audit yet.
|
||||
What Strata downloads, when it uses the internet, what stays on your PC and how downloads are checked is written up in [docs/SAFETY.md](docs/SAFETY.md).
|
||||
@@ -2,8 +2,13 @@
|
||||
rem Strata for Windows: the first run installs everything and starts the model; later runs just start it.
|
||||
rem Needs only an NVIDIA or AMD graphics driver. Python is installed for your user account if it is missing (no admin needed).
|
||||
setlocal
|
||||
rem Python reads and writes text as UTF-8, not in the Windows code page (cp1251, cp866, ...): a folder or user name with Cyrillic or other letters stays intact (#3202).
|
||||
set "PYTHONUTF8=1"
|
||||
title Strata
|
||||
cd /d "%~dp0"
|
||||
rem A bundled Python runtime (the python\ folder of the terminal archive) is used when it is there: nothing to find,
|
||||
rem install or download. SETUP.bat and UPDATE.bat run this file, so they use it too.
|
||||
if exist "python\python.exe" goto bundled
|
||||
if exist ".venv\Scripts\python.exe" goto run
|
||||
|
||||
call :findpy
|
||||
@@ -36,6 +41,14 @@ echo Could not create the Python environment in .venv
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:bundled
|
||||
set "PYTHONNOUSERSITE=1"
|
||||
set "PYTHONHOME="
|
||||
set "PYTHONPATH="
|
||||
"python\python.exe" setup.py %*
|
||||
if errorlevel 1 pause
|
||||
exit /b
|
||||
|
||||
:run
|
||||
".venv\Scripts\python.exe" setup.py %*
|
||||
if errorlevel 1 pause
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
rem Update Strata without starting the model (#475): the newest code (git pull, when this folder is a git clone), then
|
||||
rem what START-HERE.bat does before a start - the engine (a new one when this version needs it), the Python packages,
|
||||
rem each installed model's settings and draft subset. The model files are not touched. Start the model later with
|
||||
rem START-HERE.bat. Options are passed on to setup.py.
|
||||
rem START-HERE.bat. Options are passed on to setup.py. The bundled Python (python\ beside this file) is used when
|
||||
rem there is one: START-HERE.bat, which this file runs, picks it.
|
||||
setlocal
|
||||
title Strata - update
|
||||
cd /d "%~dp0"
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
{
|
||||
"name": "strata-desktop",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "strata-desktop",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "2.12.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.12.3.tgz",
|
||||
"integrity": "sha512-f7/LsY74SVb1ChmOPw1ajOpxii0GSzDAgmWHRci8JUYuuai04rXsSbFobpmB0sP2DdlvMD/B5ZotEn8GOGMRUg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bin": {
|
||||
"tauri": "tauri.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.12.3",
|
||||
"@tauri-apps/cli-darwin-x64": "2.12.3",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.12.3",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.12.3",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.12.3",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.12.3",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.12.3",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.12.3",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.12.3",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.12.3",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.12.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.12.3.tgz",
|
||||
"integrity": "sha512-xmVNoJOOKzKLj7GLHXmXLimRby9M42aBIJa0sB80N7o24eKV5JiVmToKTRLVH9yhL05GZ/B6rSSOmbzNFJwBAA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.12.3.tgz",
|
||||
"integrity": "sha512-URd+0usfmUQQyb5kNPiKt2OT8gp7oGU7QR+qOppB6e35mpVBFFVNhWc9oWb3p0u71fDJ09DpF4T6m9xYxoIB8g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.12.3.tgz",
|
||||
"integrity": "sha512-0kwUVx2LbmOoTRlwF1oDww7th4/NSluv4ZBQ6KRZsFKg4g0TlTcIiJ0VehqkQYUeC9xIT1Mk6XtF52mI155VSQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.12.3.tgz",
|
||||
"integrity": "sha512-gKSGDx6fx+HBLQmG6guChFdisbrCSdx9Yli+j5teOa64gm1CeM+C6NLru5Pj3ZkkEep05BmCFjxvX+cjqAycZQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.12.3.tgz",
|
||||
"integrity": "sha512-Yr59qMy/gcvAfBuFdCGlAmGiLsYWMFDbsP4bAI6iNBzcJTGrYWZGeiQhGDo29MQwfF/NUBqyM2qljXu5tZ6wjQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.12.3.tgz",
|
||||
"integrity": "sha512-jKr0/REBMnLKC0OwPQZkyge0mKDaNZ0b/AbfGwVyCgFfwS730EVLfVDo3nt7MUu6jJaytRq08+xSj5qfy+U1+Q==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.12.3.tgz",
|
||||
"integrity": "sha512-gTkEqJAGCMROGanxHnzaJzQ+/rXl39HWVg0/hDODS1wDWBGqoceqGC6Qm1+mQbyT11xnsB8mKu/gyVID/YpFIw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.12.3.tgz",
|
||||
"integrity": "sha512-T2dTxL1svRym71fZtaW4pZYXbthbfFsEJr1NeOUg/p7laWXZbUt/8DMSZf8recu9f7+1F/Lv00HtzsEu6vDO9w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.12.3.tgz",
|
||||
"integrity": "sha512-eoiQwpEZWbpsVHcM9BAzqqe4TCHszvr3rsM7pUQ4ofLK0lF1i1d/jX0Qo5DYMqcrUtwQuDdDuOCbcT0Izf1VwQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.12.3.tgz",
|
||||
"integrity": "sha512-kE+KUg6Dbbkgvcw0m99wJVIToFQzJqpMBAXah74WqxaLzssNwSTUccrHp6+QVzMhjQP330tvekFmnQF+0NbE6Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.12.3",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.12.3.tgz",
|
||||
"integrity": "sha512-L7wndWAgX2mLW7alswpL3d1R0P9FN2WYBgHaQkAYEBxHHGkY4vFX9CjcRQ1LAxDAMIJxrY7/aDziFJeChS84NQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "strata-desktop",
|
||||
"productName": "Strata",
|
||||
"version": "0.2.1",
|
||||
"private": true,
|
||||
"description": "Strata Desktop - local AI, tuned to one model.",
|
||||
"scripts": {
|
||||
"build:ui": "npm --prefix ../host run build",
|
||||
"typecheck:ui": "npm --prefix ../host run typecheck",
|
||||
"test:ui": "npm --prefix ../host test",
|
||||
"tauri": "tauri",
|
||||
"dev": "npm run build:ui && tauri dev",
|
||||
"build": "npm run build:ui && tauri build",
|
||||
"build:app": "npm run build:ui && tauri build --bundles app",
|
||||
"dmg": "npm run build:ui && tauri build --bundles dmg"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "2.12.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The installed Windows app, closed every way, with a stub engine: nothing may be left running.
|
||||
|
||||
python apps/desktop/scripts/app-close-matrix.py --install <folder the installer wrote> [--scenarios all] [--report out.json]
|
||||
|
||||
For each case it starts the REAL installed `strata-desktop.exe` (its window opens), loads a stub engine through the host's API
|
||||
(profile + `runtime start` under Control's owner runtime, exactly the app's path), records every process of the tree (the app,
|
||||
the host, Control, the owner host, the server, the engine), ends the app one way, and asserts:
|
||||
* no process of the tree is left, the engine's port is free, the runtime lease can be taken again;
|
||||
* the engine got the clean stop where the way of closing allows one (normal close, window X);
|
||||
* the next load of the same slot works (a lost owner's state is cleaned, not a blocker).
|
||||
It also proves a second launch focuses the open window and starts nothing, and that one app runs two engines and closes both.
|
||||
Everything per-user lives under a scratch folder; the stub engine is a script. No real engine ever runs.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from ctypes import wintypes
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import psutil # noqa: E402
|
||||
|
||||
from strata_control import procs, runtime # noqa: E402
|
||||
from tests.terminal.lifecycle_matrix import install_fake_engine, port_open # noqa: E402
|
||||
|
||||
if os.name != "nt":
|
||||
raise SystemExit("this matrix drives the Windows installer's app")
|
||||
|
||||
user32 = ctypes.WinDLL("user32", use_last_error=True)
|
||||
WM_CLOSE = 0x0010
|
||||
|
||||
|
||||
def main_window(pid: int) -> int | None:
|
||||
found = []
|
||||
|
||||
@ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
||||
def visit(hwnd, _):
|
||||
owner = wintypes.DWORD()
|
||||
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(owner))
|
||||
if owner.value == pid and user32.IsWindowVisible(hwnd):
|
||||
buf = ctypes.create_unicode_buffer(256)
|
||||
user32.GetWindowTextW(hwnd, buf, 256)
|
||||
if buf.value.strip() == "Strata":
|
||||
found.append(hwnd)
|
||||
return True
|
||||
user32.EnumWindows(visit, 0)
|
||||
return found[0] if found else None
|
||||
|
||||
|
||||
class Scratch:
|
||||
def __init__(self, root: Path, install: Path):
|
||||
self.root, self.install = root, install
|
||||
self.components = root / "components"
|
||||
self.models = root / "models"
|
||||
self.mark = root / "mark"
|
||||
for d in (self.components, self.models, self.mark, root / "local", root / "home", root / "appdata", root / "webview"):
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
install_fake_engine(self.components)
|
||||
from tools.strata_tokenizer import BYTE_TO_UNICODE
|
||||
tok = self.models / "tokenizer"
|
||||
tok.mkdir()
|
||||
tokens = [BYTE_TO_UNICODE[i] for i in range(256)] + ["ab", "<|im_start|>", "<|im_end|>", "<|endoftext|>"]
|
||||
(tok / "vocab.json").write_text(json.dumps({t: i for i, t in enumerate(tokens)}))
|
||||
(tok / "merges.txt").write_text("a b")
|
||||
(tok / "token_type.json").write_text(json.dumps([0] * 257 + [3, 3, 3]))
|
||||
for name in ("stub", "stub2"):
|
||||
(self.models / f"strata-{name}.json").write_text(json.dumps({
|
||||
"exe": "component:strata-engine", "backend": "cuda", "tokenizer": str(tok), "args": [], "cwd": str(root),
|
||||
"model_name": name, "host": "127.0.0.1", "port": 0}))
|
||||
self.env = {**os.environ, "STRATA_COMPONENTS_DIR": str(self.components), "STRATA_PROFILE_DIR": str(root / "profiles"),
|
||||
"STRATA_RUNTIME_DIR": str(root / "runtime"), "STRATA_INSTALL_DIR": str(self.models),
|
||||
"STRATA_MODELS_DIR": str(root / "models-unused"), "STRATA_ENGINE_BACKEND": "cuda",
|
||||
"STRATA_HOME": str(root / "home"), "APPDATA": str(root / "appdata"), "LOCALAPPDATA": str(root / "local"),
|
||||
"WEBVIEW2_USER_DATA_FOLDER": str(root / "webview"), "FAKE_ENGINE_MARK": str(self.mark),
|
||||
"FAKE_ENGINE_SLOW_STEPS": "0", "PYTHONDONTWRITEBYTECODE": "1"}
|
||||
self.env.pop("STRATA_API_KEY", None)
|
||||
for key in ("STRATA_COMPONENTS_DIR", "STRATA_PROFILE_DIR", "STRATA_RUNTIME_DIR", "LOCALAPPDATA", "STRATA_ENGINE_BACKEND",
|
||||
"STRATA_NO_ACQUIRE", "FAKE_ENGINE_MARK"):
|
||||
os.environ[key] = self.env.get(key, "1") # this process reads the same runtime and records folders
|
||||
|
||||
|
||||
class App:
|
||||
def __init__(self, scratch: Scratch):
|
||||
self.s = scratch
|
||||
self.proc = subprocess.Popen([str(scratch.install / "strata-desktop.exe")], cwd=scratch.install, env=scratch.env,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
self.base = None
|
||||
|
||||
def host(self):
|
||||
for child in psutil.Process(self.proc.pid).children(recursive=True):
|
||||
if child.name().lower() == "node.exe":
|
||||
return child
|
||||
return None
|
||||
|
||||
def wait_up(self, seconds=90):
|
||||
deadline = time.monotonic() + seconds
|
||||
while time.monotonic() < deadline:
|
||||
if self.proc.poll() is not None:
|
||||
raise RuntimeError(f"the app exited with {self.proc.returncode}")
|
||||
try:
|
||||
host = self.host()
|
||||
if host:
|
||||
for c in host.net_connections(kind="tcp"):
|
||||
if c.status == "LISTEN" and c.laddr.ip == "127.0.0.1":
|
||||
base = f"http://127.0.0.1:{c.laddr.port}"
|
||||
try:
|
||||
if urllib.request.urlopen(base + "/api/health", timeout=2).status == 200 and main_window(self.proc.pid):
|
||||
self.base = base
|
||||
return
|
||||
except OSError:
|
||||
pass
|
||||
except psutil.Error:
|
||||
pass
|
||||
time.sleep(0.5)
|
||||
raise RuntimeError("the app did not come up")
|
||||
|
||||
def call(self, path, method="GET", body=None):
|
||||
req = urllib.request.Request(self.base + path, method=method, data=None if body is None else json.dumps(body).encode(),
|
||||
headers={"content-type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=120) as r:
|
||||
return json.loads(r.read() or b"null")
|
||||
except urllib.error.HTTPError as exc:
|
||||
raise RuntimeError(f"{method} {path} -> {exc.code} {exc.read()[:300]!r}") from None
|
||||
|
||||
def load(self, model, multiple=False):
|
||||
self.call("/api/native?refresh=1")
|
||||
started = self.call("/api/native/operations", "POST", {"kind": "load", "args": {"model": model, **({"multiple": True} if multiple else {})}})
|
||||
deadline = time.monotonic() + 180
|
||||
while time.monotonic() < deadline:
|
||||
state = self.call("/api/native")
|
||||
job = next((j for j in state["jobs"] if j["id"] == started["id"]), None)
|
||||
if job and job["state"] != "running":
|
||||
if job["state"] != "done":
|
||||
raise RuntimeError(f"load {model}: {job['state']} {job.get('error')}")
|
||||
return state
|
||||
time.sleep(0.3)
|
||||
raise RuntimeError("load did not finish")
|
||||
|
||||
|
||||
def tree_of(app: App, slots=("app-1",)) -> tuple[list[procs.Ident], list[int]]:
|
||||
idents, ports = [], []
|
||||
members = [psutil.Process(app.proc.pid), *psutil.Process(app.proc.pid).children(recursive=True)]
|
||||
pids = {p.pid for p in members}
|
||||
for slot in slots:
|
||||
state = runtime._read_state(slot)
|
||||
if state:
|
||||
pids.add(state["host_pid"])
|
||||
for r in procs.records():
|
||||
if r.get("profile") in slots: # the app's own slots; a Terminal service is not part of the app's tree
|
||||
pids.add(r["pid"])
|
||||
for pid in pids:
|
||||
token = procs.start_token(pid)
|
||||
if token:
|
||||
idents.append(procs.Ident(pid, token, tuple(procs.command_line(pid) or [])))
|
||||
return idents, ports
|
||||
|
||||
|
||||
def survivors(idents):
|
||||
return [f"{i.pid} {' '.join(i.cmd)[:80]}" for i in idents if procs.start_token(i.pid) == i.token]
|
||||
|
||||
|
||||
def wait_gone(idents, seconds):
|
||||
deadline = time.monotonic() + seconds
|
||||
left = survivors(idents)
|
||||
while left and time.monotonic() < deadline:
|
||||
time.sleep(0.25)
|
||||
left = survivors(idents)
|
||||
return left
|
||||
|
||||
|
||||
def lease_free() -> bool:
|
||||
try:
|
||||
fd = runtime.acquire_activity_lease(shared=False)
|
||||
except Exception: # noqa: BLE001
|
||||
return False
|
||||
runtime._unlock_file(fd)
|
||||
os.close(fd)
|
||||
return True
|
||||
|
||||
|
||||
def control(scratch: Scratch, *argv: str, timeout: int = 300) -> dict:
|
||||
py = scratch.install / "control" / "python" / "python.exe"
|
||||
out = subprocess.run([str(py), str(scratch.install / "control" / "strata.py"), *argv, "--output", "json"],
|
||||
env={**scratch.env, "PYTHONNOUSERSITE": "1", "STRATA_NO_ACQUIRE": "1"},
|
||||
capture_output=True, text=True, timeout=timeout)
|
||||
try:
|
||||
return json.loads(out.stdout)
|
||||
except ValueError:
|
||||
return {"ok": False, "raw": out.stdout[-300:] + out.stderr[-300:]}
|
||||
|
||||
|
||||
def stop_cli(scratch: Scratch, slot: str) -> dict:
|
||||
py = scratch.install / "control" / "python" / "python.exe"
|
||||
out = subprocess.run([str(py), str(scratch.install / "control" / "strata.py"), "runtime", "stop", "--profile", slot, "--wait", "30",
|
||||
"--output", "json"], env={**scratch.env, "PYTHONNOUSERSITE": "1", "STRATA_NO_ACQUIRE": "1"},
|
||||
capture_output=True, text=True, timeout=120)
|
||||
try:
|
||||
return json.loads(out.stdout)
|
||||
except ValueError:
|
||||
return {"state": "?", "raw": out.stdout[-200:] + out.stderr[-200:]}
|
||||
|
||||
|
||||
def case(name: str, install: Path, how: str) -> dict:
|
||||
with tempfile.TemporaryDirectory(prefix="strata-app-matrix-", ignore_cleanup_errors=True) as tmp:
|
||||
saved = dict(os.environ)
|
||||
scratch = Scratch(Path(tmp), install)
|
||||
app = App(scratch)
|
||||
problems, note = [], ""
|
||||
try:
|
||||
app.wait_up()
|
||||
slots = ("app-1", "app-2") if how == "two-engines" else ("app-1",)
|
||||
terminal = None
|
||||
if how == "window-x-terminal-engine":
|
||||
# an engine the Terminal started (its own profile, no tie to the app) must survive the app's close
|
||||
control(scratch, "profile", "create", "terminal-1", "--from-config", str(scratch.models / "strata-stub2.json"))
|
||||
terminal = control(scratch, "runtime", "start", "--profile", "terminal-1", "--wait", "60")
|
||||
if terminal.get("state") != "ready":
|
||||
raise RuntimeError(f"the Terminal's service did not start: {terminal}")
|
||||
terminal_pids = {r["pid"] for r in procs.records(profile="terminal-1")} | {runtime._read_state("terminal-1")["host_pid"]}
|
||||
state = app.load("stub")
|
||||
if how == "two-engines":
|
||||
state = app.load("stub2", multiple=True)
|
||||
engine_ports = [i["port"] for i in state["instances"]]
|
||||
idents, _ = tree_of(app, slots)
|
||||
kinds = sorted({("engine" if "engine" in " ".join(i.cmd) else "server" if "serve.server" in " ".join(i.cmd) else
|
||||
"owner" if "strata_control.runtime" in " ".join(i.cmd) else "app" if "strata-desktop" in i.cmd[0].lower()
|
||||
else "host" if "node" in i.cmd[0].lower() else "other") for i in idents})
|
||||
note = f"tree {len(idents)} processes: {', '.join(kinds)}; engine ports {engine_ports}"
|
||||
t0 = time.monotonic()
|
||||
if how == "normal-close":
|
||||
subprocess.run(["taskkill", "/PID", str(app.proc.pid)], capture_output=True)
|
||||
elif how in ("window-x", "two-engines", "window-x-terminal-engine"):
|
||||
hwnd = main_window(app.proc.pid)
|
||||
if not hwnd:
|
||||
raise RuntimeError("no main window")
|
||||
user32.PostMessageW(hwnd, WM_CLOSE, 0, 0)
|
||||
elif how == "kill-app":
|
||||
subprocess.run(["taskkill", "/F", "/PID", str(app.proc.pid)], capture_output=True)
|
||||
elif how == "host-crash":
|
||||
host = app.host()
|
||||
subprocess.run(["taskkill", "/F", "/PID", str(host.pid)], capture_output=True)
|
||||
elif how == "second-launch":
|
||||
second = subprocess.Popen([str(install / "strata-desktop.exe")], cwd=install, env=scratch.env,
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
try:
|
||||
code = second.wait(timeout=30)
|
||||
except subprocess.TimeoutExpired:
|
||||
second.kill()
|
||||
problems.append("the second launch did not exit")
|
||||
code = None
|
||||
time.sleep(2)
|
||||
hosts = [p for p in psutil.process_iter(["name", "pid"]) if p.info["name"].lower() == "node.exe"
|
||||
and (p.parent() and p.parent().pid == app.proc.pid)]
|
||||
apps = [p for p in psutil.process_iter(["name"]) if p.info["name"].lower() == "strata-desktop.exe"]
|
||||
if app.proc.poll() is not None:
|
||||
problems.append("the first app went away")
|
||||
if len(apps) != 1:
|
||||
problems.append(f"{len(apps)} app processes (want 1)")
|
||||
if len(hosts) != 1:
|
||||
problems.append(f"{len(hosts)} hosts under the first app (want 1)")
|
||||
after, _ = tree_of(app, slots)
|
||||
new = [i for i in after if i.pid not in {x.pid for x in idents}]
|
||||
if new:
|
||||
problems.append(f"the second launch started: {[' '.join(i.cmd)[:60] for i in new]}")
|
||||
note += f"; second launch exit code {code}, app processes {len(apps)}, hosts {len(hosts)}"
|
||||
subprocess.run(["taskkill", "/PID", str(app.proc.pid)], capture_output=True)
|
||||
if how == "host-crash":
|
||||
# the shell and its window stay (nothing here restarts a dead host); the host, Control's owner, the server and
|
||||
# the engine must not
|
||||
idents = [i for i in idents if any(k in " ".join(i.cmd) for k in ("strata_control.runtime", "serve.server", "engine", "node.exe"))]
|
||||
left = wait_gone(idents, 90)
|
||||
if left:
|
||||
problems.append(f"processes left: {left}")
|
||||
for port in engine_ports:
|
||||
deadline = time.monotonic() + 10
|
||||
while port_open(port) and time.monotonic() < deadline:
|
||||
time.sleep(0.2)
|
||||
if port_open(port):
|
||||
problems.append(f"engine port {port} still open")
|
||||
if terminal is None and not lease_free():
|
||||
problems.append("lease still held")
|
||||
marks = sorted(p.name for p in scratch.mark.iterdir())
|
||||
if terminal is not None:
|
||||
status = control(scratch, "runtime", "status", "--profile", "terminal-1")
|
||||
alive_pids = [p for p in terminal_pids if procs.start_token(p)]
|
||||
if status.get("state") != "ready" or len(alive_pids) != len(terminal_pids):
|
||||
problems.append(f"closing the app disturbed the Terminal's service: {status.get('state')}, alive {alive_pids} of {sorted(terminal_pids)}")
|
||||
note += f"; the Terminal's service is {status.get('state')} after the app closed"
|
||||
stopped = control(scratch, "runtime", "stop", "--profile", "terminal-1", "--wait", "30")
|
||||
if stopped.get("state") != "stopped" or [p for p in terminal_pids if procs.start_token(p)]:
|
||||
problems.append(f"the Terminal's service did not stop on request: {stopped}")
|
||||
time.sleep(1)
|
||||
if not lease_free():
|
||||
problems.append("lease still held after the Terminal's service stopped")
|
||||
if how in ("normal-close", "window-x", "two-engines", "second-launch", "window-x-terminal-engine") and not ({"quit", "eof"} & set(marks)):
|
||||
problems.append(f"the engine was not stopped the clean way (marks {marks})")
|
||||
seconds = round(time.monotonic() - t0, 1)
|
||||
if how == "host-crash":
|
||||
subprocess.run(["taskkill", "/PID", str(app.proc.pid)], capture_output=True)
|
||||
note += "; the app shell stays open after a host crash (by design today)"
|
||||
# the slot must be usable again: stopping a lost owner's slot cleans its state; nothing blocks the next load
|
||||
for slot in slots:
|
||||
res = stop_cli(scratch, slot)
|
||||
already = "not managed or is stopped" in json.dumps(res) # nothing was left to stop: the clean outcome
|
||||
if res.get("state") not in ("stopped",) and not already:
|
||||
problems.append(f"runtime stop {slot} after the close: {res}")
|
||||
if runtime._read_state(slot) is not None:
|
||||
problems.append(f"state of {slot} left behind")
|
||||
return {"name": f"app-{how}", "ok": not problems, "problems": problems, "seconds": seconds, "marks": marks, "note": note}
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {"name": f"app-{how}", "ok": False, "problems": [f"{type(exc).__name__}: {exc}"] + problems, "note": note}
|
||||
finally:
|
||||
for p in psutil.process_iter(["name", "exe"]):
|
||||
try:
|
||||
if (p.info["exe"] or "").lower().startswith(str(install).lower()):
|
||||
p.kill()
|
||||
except psutil.Error:
|
||||
pass
|
||||
for r in procs.records():
|
||||
try:
|
||||
psutil.Process(r["pid"]).kill()
|
||||
except psutil.Error:
|
||||
pass
|
||||
os.environ.clear(); os.environ.update(saved)
|
||||
|
||||
|
||||
SCENARIOS = ["normal-close", "window-x", "window-x-terminal-engine", "kill-app", "host-crash", "second-launch", "two-engines"]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--install", required=True)
|
||||
ap.add_argument("--scenarios", default="all")
|
||||
ap.add_argument("--report")
|
||||
a = ap.parse_args()
|
||||
names = SCENARIOS if a.scenarios == "all" else a.scenarios.split(",")
|
||||
results = []
|
||||
for how in names:
|
||||
r = case(how, Path(a.install), how)
|
||||
results.append(r)
|
||||
print(("PASS " if r["ok"] else "FAIL ") + r["name"], r.get("seconds", ""), r.get("problems") or "", r.get("note", ""), flush=True)
|
||||
if a.report:
|
||||
Path(a.report).write_text(json.dumps({"platform": "windows", "engine": "fake", "results": results}, indent=1), encoding="utf-8")
|
||||
return 0 if all(r["ok"] for r in results) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* The Graphics cards choice of the app, end to end on a box with two cards, through the installed app layout (packaged host + bundled Control + a real
|
||||
* engine component): the model list carries the options (one rule, strata_control/gpu_fit.py), a model that fits one card recommends that card, then
|
||||
* load with ONE card -> answers, only that card holds the model; load with BOTH cards (the layer split) -> answers, both cards hold the model.
|
||||
*
|
||||
* node cards-e2e.mjs --install <folder with node/ host/ control/> --components <folder holding engine/<package>> --work <scratch> \
|
||||
* --family qwen --model IQ3_XXS --data <data folder that has the model files> [--smi "nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits"]
|
||||
*/
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, readdirSync } from 'node:fs';
|
||||
import { createServer } from 'node:net';
|
||||
import { join, resolve } from 'node:path';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]?.startsWith('--') || all[i + 1] === undefined ? true : all[i + 1]]] : acc), []));
|
||||
for (const need of ['install', 'components', 'work', 'model']) if (!args[need]) { console.error('missing --' + need); process.exit(2); }
|
||||
const install = resolve(args.install), work = resolve(args.work), components = resolve(args.components), family = String(args.family ?? 'qwen'), modelId = String(args.model);
|
||||
const node = join(install, 'node', 'node'), hostEntry = join(install, 'host', 'server', 'index.mjs');
|
||||
const dirs = { models: join(work, 'models'), profiles: join(work, 'profiles'), runtime: join(work, 'runtime'), home: join(work, 'home'), xdg: join(work, 'xdg'), appdata: join(work, 'appdata') };
|
||||
for (const d of [dirs.models, dirs.home, dirs.xdg, dirs.appdata]) mkdirSync(d, { recursive: true });
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
let failed = false;
|
||||
const record = (name, ok, detail = '') => { if (!ok) failed = true; console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ' ' + String(detail).slice(0, 500) : ''}`); };
|
||||
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
|
||||
const env = { ...process.env, STRATA_COMPONENTS_DIR: components, STRATA_PROFILE_DIR: dirs.profiles, STRATA_RUNTIME_DIR: dirs.runtime, STRATA_INSTALL_DIR: dirs.models,
|
||||
STRATA_HOME: dirs.home, STRATA_MODELS_DIR: join(work, 'models-unused'), APPDATA: dirs.appdata, XDG_CONFIG_HOME: dirs.xdg, PYTHONDONTWRITEBYTECODE: '1' };
|
||||
delete env.STRATA_API_KEY;
|
||||
const port = await freePort();
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
const host = spawn(node, [hostEntry], { cwd: join(install, 'host'), stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: { ...env, STRATA_PORT: String(port), STRATA_DIST: join(install, 'host', 'dist'), NODE_ENV: 'production', STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1', STRATA_NO_ACQUIRE: '1',
|
||||
STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()) } });
|
||||
let hostLog = '';
|
||||
host.stdout.on('data', c => { hostLog += c; }); host.stderr.on('data', c => { hostLog += c; });
|
||||
const call = async (path, method = 'GET', body) => {
|
||||
const response = await fetch(base + path, { method, headers: { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
|
||||
return { status: response.status, body: await response.json().catch(() => null) };
|
||||
};
|
||||
async function waitJob(id, seconds) {
|
||||
let last = '';
|
||||
for (let i = 0; i < seconds * 2; i++) {
|
||||
const state = (await call('/api/native')).body;
|
||||
const job = state.jobs.find(j => j.id === id);
|
||||
if (job && job.summary !== last) { last = job.summary; console.log(' ... ' + String(last).slice(0, 120)); }
|
||||
if (job && job.state !== 'running') return { job, state };
|
||||
await delay(500);
|
||||
}
|
||||
throw new Error('operation did not finish');
|
||||
}
|
||||
const smi = () => {
|
||||
const cmd = String(args.smi ?? 'nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits').split(' ');
|
||||
const r = spawnSync(cmd[0], cmd.slice(1), { encoding: 'utf8' });
|
||||
return (r.stdout ?? '').split('\n').map(s => Number(s.trim())).filter(Number.isFinite);
|
||||
};
|
||||
async function chatPong(instance) {
|
||||
const chat = await call('/v1/chat/completions', 'POST', { model: instance?.apiModelId ?? modelId.toLowerCase(), max_tokens: 400, stream: false, temperature: 0, messages: [{ role: 'user', content: 'Reply with exactly one word: pong' }] });
|
||||
const m = chat.body?.choices?.[0]?.message ?? {};
|
||||
return { ok: chat.status === 200 && /pong/i.test(`${m.content ?? ''}`), text: `${m.content ?? ''}`.slice(0, 60), status: chat.status };
|
||||
}
|
||||
async function main() {
|
||||
let up = false;
|
||||
for (let i = 0; i < 200 && !up; i++) { try { up = (await fetch(base + '/api/health')).ok; } catch { /* not yet */ } if (!up) await delay(150); }
|
||||
record('host healthy', up, up ? base : hostLog.slice(-500));
|
||||
if (!up) return;
|
||||
const packages = existsSync(join(components, 'engine')) ? readdirSync(join(components, 'engine')) : [];
|
||||
record('an engine component is installed', packages.length > 0, packages.join(', '));
|
||||
|
||||
// ---- the model list carries the choice
|
||||
const native = (await call('/api/native?refresh=1')).body;
|
||||
const gpus = native.hardware?.gpus ?? [];
|
||||
record('the host sees two or more graphics cards', gpus.filter(g => g.usable !== false).length >= 2, JSON.stringify(gpus.map(g => ({ i: g.index, n: g.name, gb: g.vram_gb }))));
|
||||
const size = native.catalog?.families?.find(f => f.family === family)?.sizes.find(s => s.model === modelId);
|
||||
const opt = size?.gpu_options;
|
||||
record('the model list offers the cards choice for this model', !!opt && opt.options.length >= 2, JSON.stringify(opt?.options?.map(o => ({ key: o.key, cards: o.cards, verdict: o.verdict, rec: o.recommended }))));
|
||||
record('a model that fits one card recommends the single largest card (the split is offered, not forced)', opt?.recommended === 'single' && opt.options.find(o => o.key === 'split'), `recommended=${opt?.recommended}`);
|
||||
const single = opt?.options.find(o => o.key === 'single')?.cards ?? [0];
|
||||
const both = opt?.options.find(o => o.key === 'split')?.cards ?? [0, 1];
|
||||
|
||||
// ---- Download and prepare with the recommended card
|
||||
const values = { family, model: modelId, context: 32768, vision: 'no', gpu: single[0], ...(args.data ? { data_dir: resolve(args.data) } : {}) };
|
||||
const started = await call('/api/native/operations', 'POST', { kind: 'install', args: values });
|
||||
record('the install operation starts', started.status === 200 || started.status === 202, JSON.stringify(started.body).slice(0, 160));
|
||||
const done = await waitJob(started.body.id, 3600);
|
||||
record('Download and prepare finishes', done.job.state === 'done', `${done.job.state} ${done.job.error ?? done.job.summary}`);
|
||||
const installed = (await call('/api/native?refresh=1')).body.installed?.models?.find(m => String(m.model).toLowerCase() === modelId.toLowerCase());
|
||||
const model = installed?.model ?? modelId.toLowerCase();
|
||||
|
||||
for (const [label, cards] of [['ONE card', single], ['BOTH cards (layer split)', both]]) {
|
||||
const before = smi();
|
||||
const load = await call('/api/native/operations', 'POST', { kind: 'load', args: { model, gpus: cards } });
|
||||
const loaded = await waitJob(load.body.id, 900);
|
||||
const instance = loaded.state.instances[0];
|
||||
record(`load with ${label} starts the engine`, loaded.job.state === 'done' && instance?.status === 'ready', `${loaded.job.state} ${loaded.job.error ?? ''}; instance=${instance?.status}; gpus=${JSON.stringify(instance?.gpus)}`);
|
||||
record(`the instance runs on exactly the chosen card(s)`, JSON.stringify([...(instance?.gpus ?? [])].sort()) === JSON.stringify([...cards].sort()), `want ${JSON.stringify(cards)} got ${JSON.stringify(instance?.gpus)}`);
|
||||
await delay(3000);
|
||||
const used = smi();
|
||||
const holding = used.map((mb, i) => (mb - (before[i] ?? 0)) > 1000 ? i : -1).filter(i => i >= 0);
|
||||
record(`only the chosen card(s) hold the model (graphics memory in use: ${used.join(', ')} MiB)`, JSON.stringify(holding) === JSON.stringify([...cards].sort()), `holding ${JSON.stringify(holding)}`);
|
||||
const pong = await chatPong(instance);
|
||||
record(`it answers (${label})`, pong.ok, `${pong.status} ${JSON.stringify(pong.text)}`);
|
||||
const unloaded = await call(`/api/native/instances/${instance?.id}/unload`, 'POST', {});
|
||||
record(`unload (${label})`, unloaded.status === 200, unloaded.status);
|
||||
await delay(4000);
|
||||
}
|
||||
}
|
||||
try { await main(); } catch (e) { record('run', false, e.stack ?? e); }
|
||||
host.kill();
|
||||
await delay(1500);
|
||||
console.log(failed ? 'CARDS-E2E FAIL' : 'CARDS-E2E PASS');
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,72 @@
|
||||
# Fetch and verify the private Node runtime the Windows desktop bundle ships (win-x64).
|
||||
#
|
||||
# The PowerShell twin of fetch-node.sh: same pinned version, same SHA-256 pin, same refusal to
|
||||
# install anything whose digest does not match. Not committed output: node.exe is a ~90 MB binary.
|
||||
#
|
||||
# Usage:
|
||||
# powershell -NoProfile -ExecutionPolicy Bypass -File apps\desktop\scripts\fetch-node.ps1
|
||||
# ... -File fetch-node.ps1 -Check # verify what is already installed
|
||||
# ... -File fetch-node.ps1 -FromFile node.zip # verify and install a zip you already have (offline)
|
||||
#
|
||||
# The Node version must satisfy the App Host's own engines.node (>= 24.12.0).
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$Check,
|
||||
[string]$FromFile = '',
|
||||
[string]$Dest = ''
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$NodeVersion = if ($env:STRATA_NODE_VERSION) { $env:STRATA_NODE_VERSION } else { '24.12.0' }
|
||||
$Target = 'win-x64'
|
||||
# SHA-256 of node-v24.12.0-win-x64.zip from nodejs.org's SHASUMS256.txt; keep equal to fetch-node.sh.
|
||||
$Expected = '9c125f61ae947b52e779095830f9cac267846a043ef7192183c84016aaad2812'
|
||||
if (-not $Dest) { $Dest = Join-Path $PSScriptRoot '..\src-tauri\resources\node' }
|
||||
$Installed = Join-Path $Dest 'node.exe'
|
||||
|
||||
function Fail([string]$Message) { [Console]::Error.WriteLine("fetch-node: $Message"); exit 1 }
|
||||
|
||||
if ($Check) {
|
||||
if (-not (Test-Path -LiteralPath $Installed)) { Fail "not installed at $Installed" }
|
||||
$got = (& $Installed --version).Trim()
|
||||
Write-Output "fetch-node: $Installed is $got"
|
||||
if ($got -ne "v$NodeVersion") { Fail "expected v$NodeVersion" }
|
||||
exit 0
|
||||
}
|
||||
|
||||
$Archive = "node-v$NodeVersion-$Target.zip"
|
||||
$Work = Join-Path ([IO.Path]::GetTempPath()) ("strata-node-" + [Guid]::NewGuid().ToString('N'))
|
||||
New-Item -ItemType Directory -Path $Work | Out-Null
|
||||
try {
|
||||
$Zip = Join-Path $Work $Archive
|
||||
if ($FromFile) {
|
||||
Copy-Item -LiteralPath $FromFile -Destination $Zip
|
||||
} else {
|
||||
$Url = "https://nodejs.org/dist/v$NodeVersion/$Archive"
|
||||
Write-Output "fetch-node: $Url"
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
Invoke-WebRequest -UseBasicParsing -Uri $Url -OutFile $Zip
|
||||
}
|
||||
$Actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $Zip).Hash.ToLowerInvariant()
|
||||
if ($NodeVersion -ne '24.12.0') {
|
||||
# another version has no recorded digest: say so rather than implying a verification
|
||||
[Console]::Error.WriteLine("fetch-node: WARNING - no recorded digest for v$NodeVersion; this download is NOT authenticated.")
|
||||
[Console]::Error.WriteLine("fetch-node: computed sha256 $Actual - record it in this script and fetch-node.sh.")
|
||||
} elseif ($Actual -ne $Expected) {
|
||||
Fail "REFUSING - $Archive hashes $Actual, expected $Expected"
|
||||
} else {
|
||||
Write-Output 'fetch-node: sha256 verified against the recorded digest'
|
||||
}
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
$zipFile = [IO.Compression.ZipFile]::OpenRead($Zip)
|
||||
try {
|
||||
$entry = $zipFile.GetEntry("node-v$NodeVersion-$Target/node.exe")
|
||||
if (-not $entry) { Fail "node.exe is not inside $Archive" }
|
||||
New-Item -ItemType Directory -Force -Path $Dest | Out-Null
|
||||
[IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $Installed, $true)
|
||||
} finally { $zipFile.Dispose() }
|
||||
Write-Output "fetch-node: installed at $Installed"
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $Work -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fetch and verify the private Node runtime the desktop bundle ships.
|
||||
#
|
||||
# The App Host is an ordinary Node program, and the packaging requirement is that an end user
|
||||
# supplies no runtime of their own. The desktop bundle therefore carries a pinned standalone Node
|
||||
# beside the host.
|
||||
#
|
||||
# It is NOT committed. It is a ~117 MB platform binary: committing it would bloat every clone, it
|
||||
# cannot be reviewed in a diff, and it is not source. This script is the reproducible substitute —
|
||||
# it downloads the official build for the target and refuses to install anything whose SHA-256 does
|
||||
# not match the recorded digest.
|
||||
#
|
||||
# Usage:
|
||||
# apps/desktop/scripts/fetch-node.sh # host platform
|
||||
# apps/desktop/scripts/fetch-node.sh --target linux-x64
|
||||
# apps/desktop/scripts/fetch-node.sh --check # verify what is already installed
|
||||
#
|
||||
# The Node version must satisfy the App Host's own `engines.node` (>= 24.12.0).
|
||||
set -euo pipefail
|
||||
|
||||
NODE_VERSION="${STRATA_NODE_VERSION:-24.12.0}"
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEST="$HERE/../src-tauri/resources/node"
|
||||
|
||||
# Published SHA-256 of each official nodejs.org archive, recorded so a tampered or truncated
|
||||
# download is refused rather than shipped. Add a row when bumping the version.
|
||||
# Taken from nodejs.org's own SHASUMS256.txt for v24.12.0, which is published over HTTPS beside
|
||||
# the archives. Recorded here so the fetch is verified against a value that does not come from the
|
||||
# same response as the payload.
|
||||
digest_for() {
|
||||
case "$1" in
|
||||
darwin-arm64) echo "319f221adc5e44ff0ed57e8a441b2284f02b8dc6fc87b8eb92a6a93643fd8080" ;;
|
||||
linux-x64) echo "6159227e0af7d7c3c6bb2fa900452b04a6cb8841a702a79acc613209d70b04d0" ;;
|
||||
linux-arm64) echo "9b2a2eeb98a8eb37361224e2a1d060300ad2dd143af58dfdb16de785df0f1228" ;;
|
||||
win-x64) echo "9c125f61ae947b52e779095830f9cac267846a043ef7192183c84016aaad2812" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
target() {
|
||||
if [ "${1:-}" = "--target" ]; then echo "${2:?--target needs a value}"; return; fi
|
||||
case "$(uname -s)-$(uname -m)" in
|
||||
Darwin-arm64) echo darwin-arm64 ;;
|
||||
Linux-x86_64) echo linux-x64 ;;
|
||||
Linux-aarch64) echo linux-arm64 ;;
|
||||
MINGW*|MSYS*|CYGWIN*) echo win-x64 ;;
|
||||
*) echo "unsupported host: $(uname -s)-$(uname -m)" >&2; exit 2 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
TARGET="$(target "$@")"
|
||||
EXE="node"
|
||||
case "$TARGET" in win-*) EXE="node.exe" ;; esac
|
||||
INSTALLED="$DEST/$EXE"
|
||||
|
||||
if [ "${1:-}" = "--check" ]; then
|
||||
if [ ! -x "$INSTALLED" ]; then echo "fetch-node: not installed at $INSTALLED" >&2; exit 1; fi
|
||||
got="$("$INSTALLED" --version)"
|
||||
echo "fetch-node: $INSTALLED is $got"
|
||||
case "$got" in "v$NODE_VERSION") exit 0 ;; *) echo "fetch-node: expected v$NODE_VERSION" >&2; exit 1 ;; esac
|
||||
fi
|
||||
|
||||
# The official archive for this target.
|
||||
case "$TARGET" in
|
||||
darwin-arm64) ARCHIVE="node-v$NODE_VERSION-darwin-arm64.tar.gz" ;;
|
||||
darwin-x64) ARCHIVE="node-v$NODE_VERSION-darwin-x64.tar.gz" ;;
|
||||
linux-x64) ARCHIVE="node-v$NODE_VERSION-linux-x64.tar.gz" ;;
|
||||
linux-arm64) ARCHIVE="node-v$NODE_VERSION-linux-arm64.tar.gz" ;;
|
||||
win-x64) ARCHIVE="node-v$NODE_VERSION-win-x64.zip" ;;
|
||||
*) echo "fetch-node: no archive known for $TARGET" >&2; exit 2 ;;
|
||||
esac
|
||||
URL="https://nodejs.org/dist/v$NODE_VERSION/$ARCHIVE"
|
||||
|
||||
EXPECTED="$(digest_for "$TARGET")"
|
||||
WORK="$(mktemp -d "${TMPDIR:-/tmp}/strata-node.XXXXXX")" # GNU mktemp needs the X template; BSD accepts it too
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
echo "fetch-node: $URL"
|
||||
curl -fsSL -o "$WORK/$ARCHIVE" "$URL"
|
||||
|
||||
ACTUAL="$(shasum -a 256 "$WORK/$ARCHIVE" 2>/dev/null | cut -d' ' -f1 || sha256sum "$WORK/$ARCHIVE" | cut -d' ' -f1)"
|
||||
if [ -n "$EXPECTED" ]; then
|
||||
if [ "$ACTUAL" != "$EXPECTED" ]; then
|
||||
echo "fetch-node: REFUSING — $ARCHIVE hashes $ACTUAL, expected $EXPECTED" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "fetch-node: sha256 verified against the recorded digest"
|
||||
else
|
||||
# Without a recorded digest the download cannot be authenticated. Say so loudly and keep the
|
||||
# computed value so it can be recorded, rather than implying a verification that did not happen.
|
||||
echo "fetch-node: WARNING — no recorded digest for $TARGET; this download is NOT authenticated." >&2
|
||||
echo "fetch-node: computed sha256 $ACTUAL — record it in $(basename "${BASH_SOURCE[0]}") to enable verification." >&2
|
||||
fi
|
||||
|
||||
mkdir -p "$DEST"
|
||||
case "$ARCHIVE" in
|
||||
*.zip) unzip -q -j "$WORK/$ARCHIVE" "node-v$NODE_VERSION-win-x64/node.exe" -d "$DEST" ;;
|
||||
*) tar xzf "$WORK/$ARCHIVE" -C "$WORK"
|
||||
cp "$WORK/node-v$NODE_VERSION-$TARGET/bin/node" "$DEST/node" ;;
|
||||
esac
|
||||
chmod +x "$DEST/$EXE"
|
||||
|
||||
echo "fetch-node: installed $("$DEST/$EXE" --version) at $DEST/$EXE"
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* The first run of the installed app, headless: the host as the Tauri shell starts it (STRATA_PACKAGED=1, STRATA_NO_ACQUIRE=1,
|
||||
* the staged Control, nothing installed, nothing in the per-user folders) and Chrome on its onboarding.
|
||||
*
|
||||
* node apps/desktop/scripts/first-run-ui.mjs [--control <staged control>] [--node <node.exe>] [--work <scratch>] [--shots <dir>]
|
||||
*
|
||||
* Checks, one line each (exit 1 if any fails):
|
||||
* the host answers; /api/native has a real catalog (families with sizes), a recommendation and the GPUs of this PC
|
||||
* (from Control's bundled installer on Control's Python); the page shows "Let's get started", and after it the model
|
||||
* list with sizes (no "couldn't show this screen"); no page error; the install plan for the recommended model is a
|
||||
* plan (nothing is downloaded or started: a plan changes nothing); the spy sees no pip/git/compiler/engine fetch.
|
||||
* Per-user folders (STRATA_HOME, APPDATA, LOCALAPPDATA, the components and install folders) are all under --work.
|
||||
* No model is downloaded and no engine runs.
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { createServer } from 'node:net';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]?.startsWith('--') ? true : all[i + 1]]] : acc), []));
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const repoHost = resolve(here, '../../host');
|
||||
const staged = resolve(here, '../src-tauri/resources');
|
||||
const control = resolve(args.control ?? join(staged, 'control'));
|
||||
const nodeExe = resolve(args.node ?? join(staged, 'node', process.platform === 'win32' ? 'node.exe' : 'node'));
|
||||
const work = resolve(args.work ?? mkdtempSync(join(tmpdir(), 'strata-first-run-')));
|
||||
const shots = resolve(args.shots ?? join(work, 'shots'));
|
||||
mkdirSync(work, { recursive: true }); mkdirSync(shots, { recursive: true });
|
||||
const spy = pathToFileURL(join(repoHost, 'tests/helpers/acquire-spy.mjs')).href;
|
||||
const spyLog = join(work, 'spy.log'); writeFileSync(spyLog, '');
|
||||
|
||||
const { chromium } = await import(pathToFileURL(join(repoHost, 'node_modules/playwright-core/index.mjs')).href).catch(() => import('playwright-core'));
|
||||
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome', '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'].find(existsSync);
|
||||
|
||||
let failed = false;
|
||||
const check = (ok, label, detail = '') => { console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${detail ? ' ' + detail : ''}`); if (!ok) failed = true; };
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
|
||||
|
||||
for (const need of [nodeExe, join(control, 'strata.py'), join(repoHost, 'dist', 'index.html')]) if (!existsSync(need)) { console.error(`missing ${need} (run stage-control.py, fetch-node and "npm run build" in apps/host first)`); process.exit(2); }
|
||||
|
||||
const dirs = { home: join(work, 'home'), appdata: join(work, 'appdata'), local: join(work, 'local'), components: join(work, 'local', 'Strata', 'components') };
|
||||
for (const d of Object.values(dirs)) mkdirSync(d, { recursive: true });
|
||||
const port = await freePort();
|
||||
const env = {
|
||||
...process.env,
|
||||
STRATA_PORT: String(port), STRATA_DIST: join(repoHost, 'dist'), STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1', STRATA_NO_ACQUIRE: '1',
|
||||
STRATA_CONTROL_DIR: control, STRATA_HOME: dirs.home, STRATA_MODELS_DIR: join(work, 'models'), STRATA_COMPONENTS_DIR: dirs.components,
|
||||
APPDATA: dirs.appdata, LOCALAPPDATA: dirs.local, XDG_CONFIG_HOME: join(work, 'xdg'), XDG_DATA_HOME: join(work, 'xdgdata'),
|
||||
STRATA_PROFILE_DIR: join(work, 'profiles'), STRATA_RUNTIME_DIR: join(work, 'runtime'), ACQUIRE_SPY_LOG: spyLog,
|
||||
};
|
||||
delete env.STRATA_INSTALL_DIR;
|
||||
mkdirSync(env.STRATA_MODELS_DIR, { recursive: true });
|
||||
const host = spawn(nodeExe, [...(args['no-spy'] === undefined ? ['--import', spy] : []), join(repoHost, 'server', 'index.mjs')], { cwd: repoHost, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let hostLog = '';
|
||||
host.stdout.on('data', c => { hostLog += c; }); host.stderr.on('data', c => { hostLog += c; });
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
const api = async (path, init) => { const r = await fetch(base + path, init); return { status: r.status, body: await r.json().catch(() => null) }; };
|
||||
|
||||
let browser;
|
||||
try {
|
||||
let up = false;
|
||||
for (let i = 0; i < 150 && !up; i++) { try { up = (await api('/api/health')).status === 200; } catch { await delay(200); } }
|
||||
check(up, 'the packaged host starts', `port ${port}`);
|
||||
if (!up) throw new Error(hostLog.slice(-2000));
|
||||
|
||||
const native = await api('/api/native?refresh=1');
|
||||
const state = native.body;
|
||||
check(native.status === 200 && state?.available === true, 'the host reads Control', state?.error ?? '');
|
||||
const sizes = (state?.catalog?.families ?? []).reduce((n, f) => n + (f.sizes?.length ?? 0), 0);
|
||||
check((state?.catalog?.families?.length ?? 0) > 0 && sizes >= 3, 'the catalog has real families and sizes', `${state?.catalog?.families?.length} families, ${sizes} sizes`);
|
||||
check(!!state?.catalog?.recommendation, 'the catalog has a recommendation', JSON.stringify(state?.catalog?.recommendation ?? null).slice(0, 120));
|
||||
check(Array.isArray(state?.hardware?.gpus), 'the hardware lists GPUs', (state?.hardware?.gpus ?? []).map(g => `${g.name} ${g.vram_gb} GB`).join('; ') || 'none on this PC');
|
||||
check(state?.installDir && realpathSync.native(state.installDir).toLowerCase().startsWith(realpathSync.native(work).toLowerCase()), 'models go to a writable per-user folder, not the bundle', state?.installDir);
|
||||
const hw = (await api('/api/hardware')).body;
|
||||
console.log('info host hardware probe:', JSON.stringify((hw?.gpus ?? []).map(g => ({ name: g.name, backend: g.backend, vramGb: g.vramGb, unified: g.unified }))));
|
||||
check(!state?.installerError, 'the bundled installer answers', state?.installerError ?? '');
|
||||
|
||||
browser = await chromium.launch({ executablePath: CHROME, headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 1400, height: 950 } });
|
||||
const problems = [];
|
||||
page.on('pageerror', e => problems.push(`pageerror: ${e.message}`));
|
||||
await page.goto(base, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('text=/Let.s get started|Choose a version|Connect your Strata/i', { timeout: 30000 }).catch(() => {});
|
||||
await page.screenshot({ path: join(shots, '1-welcome.png') });
|
||||
const broken = await page.locator('text=/couldn.t show this screen/i').count();
|
||||
check(broken === 0, 'the first screen renders (not "Strata couldn\'t show this screen")');
|
||||
const start = page.getByRole('button', { name: /get started/i });
|
||||
check(await start.count() > 0, 'the welcome step offers "Let\'s get started"');
|
||||
if (await start.count()) await start.first().click();
|
||||
await page.waitForSelector('text=/Choose a version/i', { timeout: 30000 }).catch(() => {});
|
||||
await delay(1200);
|
||||
await page.screenshot({ path: join(shots, '2-models.png') });
|
||||
check(await page.locator('text=/couldn.t show this screen/i').count() === 0, 'the model step renders');
|
||||
check(await page.getByText(/Choose a version/i).count() > 0, 'the model step lists the versions');
|
||||
const labels = await page.locator('[role=radio]').allInnerTexts();
|
||||
check(labels.length >= 3, 'the versions are the real catalog\'s sizes', labels.map(l => l.replace(/\s+/g, ' ').slice(0, 40)).join(' | '));
|
||||
check(labels.some(l => /\d+\.\d\s*GB/.test(l)), 'each version shows its download size');
|
||||
check(problems.length === 0, 'no page error', problems.join('; '));
|
||||
|
||||
const recommended = state?.catalog?.recommendation;
|
||||
if (recommended?.model) {
|
||||
const plan = await api('/api/native/plan', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ family: recommended.family, model: recommended.model, context: recommended.context, data_dir: join(work, 'Strata-data') }) });
|
||||
check(plan.status === 200 && !!plan.body?.plan, 'the install plan for the recommended model is a plan (nothing downloaded)', plan.body?.summary?.slice(0, 100) ?? JSON.stringify(plan.body).slice(0, 150));
|
||||
check(!existsSync(join(work, 'Strata-data', 'models')), 'a plan wrote no model files');
|
||||
}
|
||||
} catch (error) {
|
||||
check(false, 'the run', String(error?.message ?? error).slice(0, 600));
|
||||
} finally {
|
||||
await browser?.close().catch(() => {});
|
||||
host.kill();
|
||||
await new Promise(r => host.once('exit', r)); // only the PID this script started
|
||||
const attempts = readFileSync(spyLog, 'utf8').split('\n').filter(Boolean);
|
||||
check(attempts.length === 0, 'the spy saw no pip, git, compiler, engine or internet access', attempts.slice(0, 3).join(' '));
|
||||
console.log(`shots in ${shots}`);
|
||||
if (!args.work) rmSync(work, { recursive: true, force: true, maxRetries: 3 });
|
||||
}
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Turn a pnpm "isolated" node_modules (a virtual store in `.pnpm` full of links) into an ordinary hoisted tree of real folders.
|
||||
*
|
||||
* Why: the installer copies the Harness folder file by file. pnpm's layout is thousands of links (junctions on Windows); a copy
|
||||
* that follows them duplicates every package once per dependent and one that does not breaks on the first dangling link. A hoisted
|
||||
* tree has each package once (nested only where two versions of the same name are needed), no links, and Node resolves it
|
||||
* exactly as it resolves any npm install.
|
||||
*
|
||||
* Algorithm (the one npm uses, in short): the packages the root names come first, at the top. Then, walking each placed package's
|
||||
* own dependencies: look the name up from the package's own folder upwards; if the same instance is already visible, nothing to
|
||||
* do; if a DIFFERENT instance of that name is visible, nest this one inside the dependent; if none is visible, hoist it to the top.
|
||||
*
|
||||
* @param {string} out folder holding `node_modules`
|
||||
* @param {{skip?: RegExp}} [options] package names never copied (the caller removes them anyway)
|
||||
* @returns {{packages:number, copied:number, nested:number}}
|
||||
*/
|
||||
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync } from 'node:fs';
|
||||
import { dirname, join, relative, sep } from 'node:path';
|
||||
|
||||
export function flattenPnpm(out, { skip = null } = {}) {
|
||||
const nm = join(out, 'node_modules');
|
||||
const store = join(nm, '.pnpm');
|
||||
const flatRoot = join(out, 'node_modules.flat');
|
||||
rmSync(flatRoot, { recursive: true, force: true });
|
||||
mkdirSync(join(flatRoot, 'node_modules'), { recursive: true });
|
||||
|
||||
const manifest = dir => { try { return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')); } catch { return null; } };
|
||||
const nameOf = dir => manifest(dir)?.name ?? null;
|
||||
/**
|
||||
* What makes two instances "the same package": name and version. pnpm keeps one copy per set of peer packages (the same workspace
|
||||
* package under many hashed folders); in one hoisted tree they are one package.
|
||||
*/
|
||||
const identity = dir => { const m = manifest(dir); return m ? `${m.name}@${m.version}` : dir; };
|
||||
/** The sibling links of a store instance: its dependencies, by name -> real instance folder. */
|
||||
function storeDeps(instance, name) {
|
||||
const container = name.startsWith('@') ? dirname(dirname(instance)) : dirname(instance);
|
||||
const found = new Map();
|
||||
for (const e of readdirSync(container, { withFileTypes: true })) {
|
||||
if (e.name === '.bin') continue;
|
||||
const entries = e.name.startsWith('@') ? readdirSync(join(container, e.name), { withFileTypes: true }).map(x => [`${e.name}/${x.name}`, join(container, e.name, x.name)]) : [[e.name, join(container, e.name)]];
|
||||
for (const [dep, p] of entries) {
|
||||
if (dep === name) continue;
|
||||
try { if (lstatSync(p).isSymbolicLink() || existsSync(p)) found.set(dep, realpathSync(p)); } catch { /* a dangling link: the package was removed on purpose */ }
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
/** Every instance in the store by package name, for packages that arrived without a store entry (copied peers). */
|
||||
const byName = new Map();
|
||||
if (existsSync(store)) {
|
||||
for (const id of readdirSync(store, { withFileTypes: true })) {
|
||||
if (!id.isDirectory()) continue;
|
||||
const container = join(store, id.name, 'node_modules');
|
||||
if (!existsSync(container)) continue;
|
||||
for (const e of readdirSync(container, { withFileTypes: true })) {
|
||||
if (e.name === '.bin') continue;
|
||||
const dirs = e.name.startsWith('@') ? readdirSync(join(container, e.name)).map(x => join(container, e.name, x)) : [join(container, e.name)];
|
||||
for (const d of dirs) {
|
||||
let real; try { real = realpathSync(d); } catch { continue; }
|
||||
if (!real.startsWith(store)) continue; // a link to a workspace package outside the store is handled through the instance table
|
||||
const n = nameOf(real);
|
||||
if (n && !byName.has(n)) byName.set(n, real);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const placed = new Map(); // folder (in the flat tree) -> real instance folder it holds
|
||||
let copied = 0, nested = 0;
|
||||
|
||||
const visibleFrom = (chain, name) => {
|
||||
for (const dir of chain) {
|
||||
const slot = join(dir, 'node_modules', ...name.split('/'));
|
||||
if (placed.has(slot)) return { slot, instance: placed.get(slot) };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const copyInto = (slot, instance) => {
|
||||
mkdirSync(dirname(slot), { recursive: true });
|
||||
cpSync(instance, slot, { recursive: true, dereference: true, filter: f => !relative(instance, f).split(sep).includes('node_modules') });
|
||||
placed.set(slot, identity(instance));
|
||||
copied++;
|
||||
};
|
||||
|
||||
const queue = []; // { slotDir (folder of the placed package), chain (folders searched upwards), instance, name }
|
||||
const rootChain = [flatRoot];
|
||||
const place = (name, instance, chain, forceHere) => {
|
||||
if (skip?.test(name)) return;
|
||||
const seen = visibleFrom(chain, name);
|
||||
if (seen && seen.instance === identity(instance)) return;
|
||||
const hoist = !seen && !forceHere;
|
||||
const base = hoist ? flatRoot : chain[0];
|
||||
const slot = join(base, 'node_modules', ...name.split('/'));
|
||||
if (placed.has(slot)) return; // another instance already sits where this one would go (it nests below, handled by the caller's chain)
|
||||
copyInto(slot, instance);
|
||||
if (!hoist && base !== flatRoot) nested++;
|
||||
queue.push({ slotDir: slot, chain: [slot, ...(hoist ? rootChain : chain)], instance, name });
|
||||
};
|
||||
|
||||
// 1. the packages the root names
|
||||
const rootEntries = [];
|
||||
for (const e of readdirSync(nm, { withFileTypes: true })) {
|
||||
if (e.name === '.pnpm' || e.name === '.bin' || e.name.startsWith('.')) continue;
|
||||
const entries = e.name.startsWith('@') ? readdirSync(join(nm, e.name)).map(x => [`${e.name}/${x}`, join(nm, e.name, x)]) : [[e.name, join(nm, e.name)]];
|
||||
for (const [name, p] of entries) rootEntries.push([name, p]);
|
||||
}
|
||||
for (const [name, p] of rootEntries) {
|
||||
let real; try { real = realpathSync(p); } catch { continue; }
|
||||
place(name, real, rootChain, true);
|
||||
}
|
||||
// 2. their dependencies, breadth first
|
||||
while (queue.length) {
|
||||
const { slotDir, chain, instance, name } = queue.shift();
|
||||
let wants;
|
||||
if (instance.startsWith(store)) wants = storeDeps(instance, name);
|
||||
else {
|
||||
// a package copied in without a store entry: its declared requirements, taken from the store by name when not visible yet
|
||||
const m = manifest(instance) ?? {};
|
||||
wants = new Map();
|
||||
for (const dep of [...Object.keys(m.dependencies ?? {}), ...Object.keys(m.peerDependencies ?? {}), ...Object.keys(m.optionalDependencies ?? {})]) {
|
||||
if (visibleFrom(chain, dep)) continue;
|
||||
const real = byName.get(dep);
|
||||
if (real) wants.set(dep, real);
|
||||
}
|
||||
}
|
||||
for (const [dep, real] of wants) place(dep, real, chain, false);
|
||||
}
|
||||
|
||||
rmSync(nm, { recursive: true, force: true });
|
||||
renameSync(join(flatRoot, 'node_modules'), nm);
|
||||
rmSync(flatRoot, { recursive: true, force: true });
|
||||
return { packages: placed.size, copied, nested };
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Which files and folders of the staged Harness are dropped, and the check that nothing the Harness imports is gone.
|
||||
*
|
||||
* The 2026-10-17 tester report: on Linux and Mac every agent chat failed ("session/create ... waiting for workflowEngine").
|
||||
* Cause: the old rule "drop any folder named after another operating system" also removed the Harness's own JavaScript package
|
||||
* `@deepseek-ai/dsh-win32-process`. It is plain JS (no binary), and `dsh-subprocess-local` and `dsh-sandbox-local` import it by
|
||||
* name on every platform, so those two plugins failed to load, and everything waiting on them (shell, subprocess, ptcRuntime,
|
||||
* workflowEngine) never started. Windows kept it (the rule differs there), so the Windows tests passed.
|
||||
*/
|
||||
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
||||
import { join, sep } from 'node:path';
|
||||
|
||||
export const OTHER_OS = {
|
||||
win32: /(^|[\\/._-])(darwin|linux|android|freebsd|musl)([\\/._-]|$)/i,
|
||||
darwin: /(^|[\\/._-])(win32|linux|android|freebsd|musl)([\\/._-]|$)/i,
|
||||
// Linux: the AppImage bundler (linuxdeploy) resolves the libraries of every ELF file under resources and stops on one it
|
||||
// cannot satisfy (koffi's musl build wants libc.musl). The app is glibc x86_64, so musl and other-CPU builds are dropped.
|
||||
linux: /(^|[\\/._-])(win32|darwin|android|freebsd|musl|arm64|aarch64|armv7|arm|ia32|riscv64|ppc64|s390x|loong64)([\\/._-]|$)/i,
|
||||
};
|
||||
const NATIVE_FOLDER = /prebuild|bin|native|platform|-(win32|darwin|linux)/i;
|
||||
|
||||
/** The Harness's own JavaScript packages (`@deepseek-ai/dsh-*`) are code every platform imports by name; never drop one. */
|
||||
export function isHarnessCodePackage(path) {
|
||||
const parts = path.split(sep);
|
||||
const i = parts.lastIndexOf('@deepseek-ai');
|
||||
return i >= 0 && i === parts.length - 2 && parts[i + 1].startsWith('dsh-');
|
||||
}
|
||||
|
||||
/** True when the folder `path` (a directory named `name`) is another operating system's prebuilt binary and can go. */
|
||||
export function isOtherOsBinaryFolder(platform, name, path) {
|
||||
const rule = OTHER_OS[platform] ?? OTHER_OS.linux;
|
||||
if (isHarnessCodePackage(path)) return false;
|
||||
return rule.test(name) && NATIVE_FOLDER.test(path);
|
||||
}
|
||||
|
||||
/** Imports the Windows copy (which works) is also without: browser-side or dev-only bundles, and the document converter the app drops on purpose. */
|
||||
const KNOWN_ABSENT = new Set([
|
||||
'@deepseek-ai/libreoffice-kit', '@deepseek-ai/dsh-client-ui-dockkit', '@deepseek-ai/dsh-client-web',
|
||||
'@deepseek-ai/dsh-experimental-webworker-runtime', '@deepseek-ai/dsh-remote-mock', '@deepseek-ai/dsh-client-test-runtime',
|
||||
]);
|
||||
|
||||
function jsFiles(dir, out = []) {
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = join(dir, e.name);
|
||||
if (e.isSymbolicLink()) continue;
|
||||
if (e.isDirectory()) jsFiles(p, out);
|
||||
else if (/\.(m?js|cjs)$/.test(e.name)) out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every bare `@deepseek-ai/<package>` the staged Harness's own packages import (static `from "x"` and `import("x")`) must exist
|
||||
* in the staged node_modules. Returns the list of `{from, missing}` problems (empty = good). Plugins that are only loaded on
|
||||
* another platform still count: Node resolves a static import whether or not the code runs.
|
||||
*/
|
||||
export function missingHarnessImports(harnessDir) {
|
||||
const scope = join(harnessDir, 'node_modules', '@deepseek-ai');
|
||||
if (!existsSync(scope)) return [{ from: scope, missing: '@deepseek-ai (no installed packages)' }];
|
||||
const problems = [];
|
||||
const seen = new Set();
|
||||
const re = /(?:from|import)\s*\(?\s*["'](@deepseek-ai\/[^"'/]+)(?:\/[^"']*)?["']/g;
|
||||
for (const pkg of readdirSync(scope)) {
|
||||
const lib = join(scope, pkg, 'lib');
|
||||
if (!existsSync(lib)) continue;
|
||||
for (const file of jsFiles(lib)) {
|
||||
const text = readFileSync(file, 'utf8');
|
||||
for (const m of text.matchAll(re)) {
|
||||
const name = m[1];
|
||||
const key = `${pkg}>${name}`;
|
||||
if (seen.has(key) || KNOWN_ABSENT.has(name)) continue;
|
||||
seen.add(key);
|
||||
if (!existsSync(join(harnessDir, 'node_modules', ...name.split('/')))
|
||||
&& !existsSync(join(scope, pkg, 'node_modules', ...name.split('/')))) problems.push({ from: `${pkg}`, missing: name });
|
||||
}
|
||||
}
|
||||
}
|
||||
return problems;
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Prove the INSTALLED app layout can start and stop an engine with nothing acquired, using a stub engine.
|
||||
*
|
||||
* node apps/desktop/scripts/installed-flow.mjs --install <folder the installer wrote> [--work <scratch>] [--spy <acquire-spy.mjs>]
|
||||
*
|
||||
* It starts the installed host with the installed node.exe exactly as the Tauri shell does (STRATA_PACKAGED=1,
|
||||
* STRATA_NO_ACQUIRE=1; Control found beside the host), against a scratch component folder holding a fake engine
|
||||
* package and a scratch install folder holding one run config. Everything per-user (profiles, runtime state, host data,
|
||||
* components) lives under --work, so the real profile of whoever runs this is never read or written.
|
||||
*
|
||||
* Steps (each one printed, the run exits 1 if any fails):
|
||||
* host health, Control status (engine usable, acquiring forbidden), models from Control, load through the host
|
||||
* (profile + `runtime start` under the owner runtime), a chat answer from the stub engine, a second client attaches to
|
||||
* the same owner/instance, unload stops it, no engine for this PC is a notice with nothing started, the Tauri-style
|
||||
* clean shutdown stops the engines the host started, a hard-killed host takes its engine with it.
|
||||
* With --spy the host runs under tests/helpers/acquire-spy.mjs and the run fails on any attempt to reach the internet or
|
||||
* run an installer.
|
||||
*
|
||||
* REAL mode (a test box with a GPU): --components <folder holding engine/<package>> --models <folder with strata-<id>.json
|
||||
* whose engine is "component:strata-engine"> --model <id> [--backend cuda]. The same host and Control, a real engine and a
|
||||
* real model: load, a real answer, ownership, unload, the missing-engine notice, load again, clean shutdown. The stub-only
|
||||
* steps (the marker file, the hard-kill report) are left out.
|
||||
*/
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { createHash, randomBytes } from 'node:crypto';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, chmodSync } from 'node:fs';
|
||||
import { createServer } from 'node:net';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]?.startsWith('--') || all[i + 1] === undefined ? true : all[i + 1]]] : acc), []));
|
||||
if (!args.install) { console.error('usage: installed-flow.mjs --install <folder> [--work <dir>] [--spy <acquire-spy.mjs>]'); process.exit(2); }
|
||||
const install = resolve(args.install);
|
||||
const isWin = process.platform === 'win32';
|
||||
const node = join(install, 'node', isWin ? 'node.exe' : 'node');
|
||||
const hostEntry = join(install, 'host', 'server', 'index.mjs');
|
||||
const controlDir = join(install, 'control');
|
||||
const python = isWin ? join(controlDir, 'python', 'python.exe') : join(controlDir, 'python', 'bin', 'python3');
|
||||
const real = !!args.components;
|
||||
const modelId = String(args.model ?? 'stub');
|
||||
const work = resolve(args.work ?? mkdtempSync(join(tmpdir(), 'strata-installed-flow-')));
|
||||
mkdirSync(work, { recursive: true });
|
||||
const dirs = { components: real ? resolve(args.components) : join(work, 'components'), models: real ? resolve(args.models) : join(work, 'models'), profiles: join(work, 'profiles'), runtime: join(work, 'runtime'), home: join(work, 'home') };
|
||||
// the runtime and profile folders are created by Control itself, with the owner-only ACL it insists on
|
||||
for (const [name, d] of Object.entries(dirs)) if (!['runtime', 'profiles'].includes(name) && !(real && ['components', 'models'].includes(name))) mkdirSync(d, { recursive: true });
|
||||
|
||||
const steps = [];
|
||||
const record = (name, ok, detail = '') => { steps.push({ name, ok: !!ok, detail: String(detail) }); console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ' ' + detail : ''}`); };
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
|
||||
const sha = buf => createHash('sha256').update(buf).digest('hex');
|
||||
|
||||
// ------------------------------------------------------------------------------ fixtures
|
||||
const baseEnv = {
|
||||
...process.env,
|
||||
STRATA_COMPONENTS_DIR: dirs.components, STRATA_PROFILE_DIR: dirs.profiles, STRATA_RUNTIME_DIR: dirs.runtime,
|
||||
STRATA_INSTALL_DIR: dirs.models, STRATA_ENGINE_BACKEND: String(args.backend ?? 'cuda'), STRATA_HOME: dirs.home, STRATA_MODELS_DIR: real ? join(work, 'models-unused') : dirs.models,
|
||||
APPDATA: join(work, 'appdata'), XDG_CONFIG_HOME: join(work, 'xdg'), PYTHONDONTWRITEBYTECODE: '1',
|
||||
};
|
||||
delete baseEnv.STRATA_API_KEY;
|
||||
mkdirSync(baseEnv.APPDATA, { recursive: true });
|
||||
const mark = join(work, 'engine-ran.json');
|
||||
|
||||
const FAKE_ENGINE = `import os, sys, json
|
||||
from pathlib import Path
|
||||
Path(os.environ['FAKE_ENGINE_MARK']).write_text(json.dumps({'pid': os.getpid(), 'script': os.path.abspath(__file__), 'argv': sys.argv[1:]}))
|
||||
print('READY 4096 stop', flush=True)
|
||||
for line in sys.stdin:
|
||||
if line.strip() == 'QUIT': break
|
||||
if line.startswith(('GEN ', 'BGEN ')):
|
||||
slot = int(line.split()[1]) if line.startswith('BGEN ') else None
|
||||
print('T 97', flush=True)
|
||||
print('DONE 1 1 1.0 1.0 length 0 0 0', flush=True)
|
||||
if slot is not None: print(f'BADM {slot} 0', flush=True)
|
||||
`;
|
||||
|
||||
function installFakeEngine() {
|
||||
const platform = isWin ? 'windows-x86_64' : 'linux-x86_64';
|
||||
const folder = join(dirs.components, 'engine', `0.1.45-${platform}-cuda`);
|
||||
mkdirSync(folder, { recursive: true });
|
||||
writeFileSync(join(folder, 'engine_main.py'), FAKE_ENGINE);
|
||||
const entry = isWin ? 'engine.cmd' : 'engine';
|
||||
writeFileSync(join(folder, entry), isWin
|
||||
? `@echo off\r\n"${python}" "%~dp0engine_main.py" %*\r\n`
|
||||
: `#!/bin/sh\nexec "${python}" "$(dirname "$0")/engine_main.py" "$@"\n`);
|
||||
if (!isWin) chmodSync(join(folder, entry), 0o755);
|
||||
const files = [entry, 'engine_main.py'].map(p => { const b = readFileSync(join(folder, p)); return { path: p, sha256: sha(b), bytes: b.length }; });
|
||||
writeFileSync(join(folder, 'engine-component.json'), JSON.stringify({ schema_version: 1, id: 'strata-engine', version: '0.1.45', platform, backend: 'cuda', entrypoint: entry, files }));
|
||||
return folder;
|
||||
}
|
||||
|
||||
function makeTokenizerAndConfig() {
|
||||
const tok = join(dirs.models, 'tokenizer');
|
||||
mkdirSync(tok, { recursive: true });
|
||||
const script = `import json, sys\nfrom tools.strata_tokenizer import BYTE_TO_UNICODE\ntok = sys.argv[1]\ntokens = [BYTE_TO_UNICODE[i] for i in range(256)] + ['ab', '<|im_start|>', '<|im_end|>', '<|endoftext|>']\njson.dump({t: i for i, t in enumerate(tokens)}, open(tok + '/vocab.json', 'w'))\nopen(tok + '/merges.txt', 'w').write('a b')\njson.dump([0] * 257 + [3, 3, 3], open(tok + '/token_type.json', 'w'))\n`;
|
||||
const done = spawnSync(python, ['-c', script, tok], { cwd: controlDir, env: { ...baseEnv, PYTHONNOUSERSITE: '1' }, encoding: 'utf8' });
|
||||
if (done.status !== 0) throw new Error('tokenizer fixture failed: ' + done.stderr);
|
||||
writeFileSync(join(dirs.models, 'strata-stub.json'), JSON.stringify({ exe: 'component:strata-engine', backend: 'cuda', tokenizer: tok, args: [], cwd: work, model_name: 'stub', host: '127.0.0.1', port: 0 }));
|
||||
}
|
||||
|
||||
const control = (...argv) => spawnSync(python, [join(controlDir, 'strata.py'), ...argv, '--output', 'json'], { cwd: controlDir, env: { ...baseEnv, PYTHONNOUSERSITE: '1', STRATA_NO_ACQUIRE: '1', FAKE_ENGINE_MARK: mark }, encoding: 'utf8', timeout: 120000 });
|
||||
const controlJson = (...argv) => { const r = control(...argv); let json = null; try { json = JSON.parse(r.stdout.slice(r.stdout.indexOf('{'))); } catch { /* not json */ } return { code: r.status, json, text: r.stdout + r.stderr }; };
|
||||
const alive = pid => { try { process.kill(pid, 0); return true; } catch { return false; } };
|
||||
|
||||
// ------------------------------------------------------------------------------ the host
|
||||
let host = null, port = 0, base = '', spyLog = null, shutdownToken = randomBytes(32).toString('hex');
|
||||
async function startHost() {
|
||||
port = await freePort(); base = `http://127.0.0.1:${port}`;
|
||||
const hostArgs = [...(args.spy ? ['--import', pathToFileURL(resolve(args.spy)).href] : []), hostEntry];
|
||||
if (args.spy) { spyLog = join(work, 'acquire-spy.log'); writeFileSync(spyLog, ''); }
|
||||
host = spawn(node, hostArgs, {
|
||||
cwd: join(install, 'host'), stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
|
||||
env: { ...baseEnv, STRATA_PORT: String(port), STRATA_DIST: join(install, 'host', 'dist'), NODE_ENV: 'production', STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1',
|
||||
STRATA_NO_ACQUIRE: '1', STRATA_SHUTDOWN_TOKEN: shutdownToken, FAKE_ENGINE_MARK: mark, STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()),
|
||||
...(spyLog ? { ACQUIRE_SPY_LOG: spyLog } : {}) },
|
||||
});
|
||||
let log = '';
|
||||
host.stdout.on('data', c => { log += c; }); host.stderr.on('data', c => { log += c; });
|
||||
host.hostLog = () => log;
|
||||
for (let i = 0; i < 200; i++) { try { if ((await fetch(base + '/api/health')).ok) return true; } catch { /* not yet */ } await delay(150); }
|
||||
return false;
|
||||
}
|
||||
/** The host started the way the Tauri shell starts it, by a stand-in shell (a node process) that can then be killed hard. */
|
||||
async function startHostViaShell() {
|
||||
port = await freePort(); base = `http://127.0.0.1:${port}`;
|
||||
const spec = { command: node, args: [hostEntry], options: {
|
||||
cwd: join(install, 'host'), stdio: 'ignore', windowsHide: true,
|
||||
env: { ...baseEnv, STRATA_PORT: String(port), STRATA_DIST: join(install, 'host', 'dist'), NODE_ENV: 'production', STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1',
|
||||
STRATA_NO_ACQUIRE: '1', STRATA_SHUTDOWN_TOKEN: shutdownToken, STRATA_EXIT_WITH_PARENT: '1', FAKE_ENGINE_MARK: mark,
|
||||
STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()) } } };
|
||||
const shell = spawn(node, ['-e', "const s=JSON.parse(process.env.HOST_SPEC);const c=require('node:child_process').spawn(s.command,s.args,s.options);console.log(c.pid);setInterval(()=>{},1000);"],
|
||||
{ env: { ...process.env, HOST_SPEC: JSON.stringify(spec) }, stdio: ['ignore', 'pipe', 'inherit'], windowsHide: true });
|
||||
const hostPid = await new Promise(r => shell.stdout.once('data', c => r(Number(String(c).trim()))));
|
||||
for (let i = 0; i < 200; i++) { try { if ((await fetch(base + '/api/health')).ok) return { shell, hostPid }; } catch { /* not yet */ } await delay(150); }
|
||||
return { shell, hostPid, failed: true };
|
||||
}
|
||||
const call = async (path, method = 'GET', body) => {
|
||||
const response = await fetch(base + path, { method, headers: { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
|
||||
return { status: response.status, body: await response.json().catch(() => null) };
|
||||
};
|
||||
async function waitJob(id, seconds = 120) {
|
||||
for (let i = 0; i < seconds * 5; i++) {
|
||||
const state = (await call('/api/native')).body;
|
||||
const job = state.jobs.find(j => j.id === id);
|
||||
if (job && job.state !== 'running') return { job, state };
|
||||
await delay(200);
|
||||
}
|
||||
throw new Error('operation did not finish');
|
||||
}
|
||||
async function stopHost(hard = false) {
|
||||
if (!host || host.exitCode !== null) return;
|
||||
if (hard) { if (isWin) spawnSync('taskkill', ['/PID', String(host.pid), '/T', '/F'], { windowsHide: true }); else host.kill('SIGKILL'); }
|
||||
else host.kill();
|
||||
for (let i = 0; i < 100 && host.exitCode === null; i++) await delay(100);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
for (const need of [node, hostEntry, python, join(controlDir, 'strata.py')]) if (!existsSync(need)) { record('installed layout', false, `missing ${need}`); return; }
|
||||
record('installed layout', true, install);
|
||||
if (real) {
|
||||
const packages = existsSync(join(dirs.components, 'engine')) ? readdirSync(join(dirs.components, 'engine')) : [];
|
||||
record('fixtures', packages.length > 0 && existsSync(join(dirs.models, `strata-${modelId}.json`)), `engine package(s) ${packages.join(', ')} in ${dirs.components}; run config strata-${modelId}.json in ${dirs.models}`);
|
||||
} else {
|
||||
installFakeEngine();
|
||||
makeTokenizerAndConfig();
|
||||
record('fixtures', true, `stub engine package in ${dirs.components}; run config strata-stub.json`);
|
||||
}
|
||||
|
||||
if (!(await startHost())) { record('host healthy', false, host.hostLog().slice(-600)); return; }
|
||||
record('host healthy', true, base);
|
||||
|
||||
const status = (await call('/api/control/status')).body;
|
||||
record('Control status: bundled, engine usable, acquiring forbidden', status.control?.python?.startsWith(controlDir) && status.engine?.usable === true && status.acquisition?.forbidden === true,
|
||||
`python=${status.control?.python}; engine=${status.engine?.engine?.version}; forbidden=${status.acquisition?.forbidden}`);
|
||||
|
||||
const native = (await call('/api/native?refresh=1')).body;
|
||||
const model = native.installed?.models?.find(m => m.model === modelId);
|
||||
record('models come from Control', native.available && model?.ready === true && native.packaged === true, `available=${native.available}; model ready=${model?.ready}; ${native.summary ?? native.error}`);
|
||||
|
||||
// ---- load
|
||||
const started = (await call('/api/native/operations', 'POST', { kind: 'load', args: { model: modelId } })).body;
|
||||
const loaded = await waitJob(started.id, 400);
|
||||
const instance = loaded.state.instances[0];
|
||||
record('load through the host starts the engine under Control', loaded.job.state === 'done' && instance?.status === 'ready', `job=${loaded.job.state} ${loaded.job.error ?? ''}; instance=${instance?.status}`);
|
||||
let ran = existsSync(mark) ? JSON.parse(readFileSync(mark, 'utf8')) : null;
|
||||
if (real) {
|
||||
const listed = spawnSync('ps', ['-eo', 'pid,args'], { encoding: 'utf8' }).stdout.split('\n').filter(l => l.includes(`${dirs.components}/engine/`) && l.includes('--serve'));
|
||||
ran = listed.length ? { pid: Number(listed[0].trim().split(/\s+/)[0]), script: listed[0].trim().split(/\s+/)[1] } : null;
|
||||
record('the engine process running is the binary from the component folder', !!ran, listed[0] ? listed[0].trim().slice(0, 160) : 'no engine process under the component folder');
|
||||
} else {
|
||||
record('the engine that ran is the one in the component folder', ran && ran.script.toLowerCase().startsWith(dirs.components.toLowerCase()), ran?.script ?? 'engine never ran');
|
||||
}
|
||||
|
||||
const runtime = controlJson('runtime', 'status', '--profile', 'app-1');
|
||||
record('a second client sees the same owner and instance (ownership)', runtime.json?.state === 'ready' && !!runtime.json.owner_id && !!runtime.json.instance_id && runtime.json.host_pid !== host.pid,
|
||||
`state=${runtime.json?.state}; owner=${String(runtime.json?.owner_id).slice(0, 8)}; instance=${String(runtime.json?.instance_id).slice(0, 8)}; owner host pid ${runtime.json?.host_pid} is not the app host (${host.pid})`);
|
||||
|
||||
// ---- answer
|
||||
const chat = await call('/v1/chat/completions', 'POST', real
|
||||
? { model: instance?.apiModelId ?? modelId, messages: [{ role: 'user', content: 'Reply with exactly one word: pong' }], max_tokens: 200, temperature: 0, reasoning_effort: 'none', stream: false }
|
||||
: { model: instance?.apiModelId ?? modelId, messages: [{ role: 'user', content: 'hi' }], max_tokens: 4, stream: false });
|
||||
const message = chat.body?.choices?.[0]?.message ?? {};
|
||||
const answer = `${message.content ?? ''}${message.reasoning_content ?? ''}`; // the stub emits token 97 ("a"); the server files it as reasoning
|
||||
record(real ? 'the real model answers through the host (the answer text is checked)' : 'the engine answers through the host (the stub token, not an error)',
|
||||
chat.status === 200 && (real ? /pong/i.test(answer) : answer === 'a'), `status=${chat.status}; answer=${JSON.stringify(answer.slice(0, 200))}${chat.status === 200 ? '' : ' ' + JSON.stringify(chat.body).slice(0, 160)}`);
|
||||
|
||||
// ---- unload
|
||||
const unloaded = await call(`/api/native/instances/${instance.id}/unload`, 'POST', {});
|
||||
await delay(500);
|
||||
const after = controlJson('runtime', 'status', '--profile', 'app-1');
|
||||
for (let i = 0; i < 100 && ran && alive(ran.pid); i++) await delay(100); // a real engine takes a moment to release the GPU
|
||||
record('unload stops the engine', unloaded.status === 200 && after.json?.state === 'stopped' && (!ran || !alive(ran.pid)), `status=${unloaded.status}; runtime=${after.json?.state}; engine pid ${ran?.pid} alive=${ran ? alive(ran.pid) : 'n/a'}`);
|
||||
|
||||
// ---- no engine for this PC
|
||||
const hidden = join(dirs.components, real ? 'engine.hidden-by-flow' : 'engine.gone');
|
||||
renameSync(join(dirs.components, 'engine'), hidden);
|
||||
rmSync(mark, { force: true });
|
||||
const missing = await waitJob((await call('/api/native/operations', 'POST', { kind: 'load', args: { model: modelId } })).body.id);
|
||||
record('no engine for this PC is a notice and nothing is started', missing.job.state === 'error' && /No engine for this PC/.test(missing.job.error) && /never downloads/.test(missing.job.error) && !existsSync(mark),
|
||||
`${missing.job.state}: ${String(missing.job.error).slice(0, 160)}`);
|
||||
const refused = await waitJob((await call('/api/native/operations', 'POST', { kind: 'install', args: { family: 'qwen', model: 'IQ2_XS' } })).body.id);
|
||||
record('installing from the app is refused (Terminal only)', refused.job.state === 'error' && /Terminal/.test(refused.job.error), refused.job.error);
|
||||
const binary = await call('/api/engine/binary', 'POST');
|
||||
record('the llama.cpp download route is refused', binary.status === 409 && /never downloads/.test(binary.body?.error ?? ''), `${binary.status} ${binary.body?.error}`);
|
||||
|
||||
// ---- clean shutdown (what the Tauri shell does on exit)
|
||||
renameSync(hidden, join(dirs.components, 'engine'));
|
||||
rmSync(mark, { force: true });
|
||||
const again = await waitJob((await call('/api/native/operations', 'POST', { kind: 'load', args: { model: modelId } })).body.id, 400);
|
||||
let ran2 = existsSync(mark) ? JSON.parse(readFileSync(mark, 'utf8')) : null;
|
||||
if (real) {
|
||||
const listed = spawnSync('ps', ['-eo', 'pid,args'], { encoding: 'utf8' }).stdout.split('\n').filter(l => l.includes(`${dirs.components}/engine/`) && l.includes('--serve'));
|
||||
ran2 = listed.length ? { pid: Number(listed[0].trim().split(/\s+/)[0]) } : null;
|
||||
}
|
||||
record('load again after the engine package returns', again.job.state === 'done', again.job.error ?? '');
|
||||
const wrong = await fetch(base + '/api/control/shutdown', { method: 'POST', headers: { 'x-strata-shutdown': 'nope' } });
|
||||
record('shutdown without the shell\'s secret is refused', wrong.status === 403, String(wrong.status));
|
||||
const exited = new Promise(r => host.once('exit', code => r(code)));
|
||||
const stopping = await fetch(base + '/api/control/shutdown', { method: 'POST', headers: { 'x-strata-shutdown': shutdownToken } });
|
||||
const code = await Promise.race([exited, delay(90000).then(() => 'timeout')]);
|
||||
for (let i = 0; i < 100 && ran2 && alive(ran2.pid); i++) await delay(100);
|
||||
const final = controlJson('runtime', 'status', '--profile', 'app-1');
|
||||
record('clean shutdown stops the engine the app started and exits', stopping.status === 202 && code === 0 && final.json?.state === 'stopped' && (!ran2 || !alive(ran2.pid)), `status=${stopping.status}; host exit=${code}; runtime=${final.json?.state}; engine alive=${ran2 ? alive(ran2.pid) : 'n/a'}`);
|
||||
|
||||
// ---- hard kill: an engine whose app host dies ends with it (the owner follows the host: runtime start --die-with-pid)
|
||||
if (!(await startHost())) { record('host restarts', false, host.hostLog().slice(-400)); return; }
|
||||
await call('/api/native?refresh=1');
|
||||
rmSync(mark, { force: true });
|
||||
const third = await waitJob((await call('/api/native/operations', 'POST', { kind: 'load', args: { model: modelId } })).body.id, 400);
|
||||
let ran3 = existsSync(mark) ? JSON.parse(readFileSync(mark, 'utf8')) : null;
|
||||
if (real) {
|
||||
const listed = spawnSync('ps', ['-eo', 'pid,args'], { encoding: 'utf8' }).stdout.split('\n').filter(l => l.includes(`${dirs.components}/engine/`) && l.includes('--serve'));
|
||||
ran3 = listed.length ? { pid: Number(listed[0].trim().split(/\s+/)[0]) } : null;
|
||||
record('(real) the engine is up before the host is killed', !!ran3, listed[0] ? listed[0].trim().slice(0, 120) : 'no engine process');
|
||||
}
|
||||
await stopHost(true);
|
||||
for (let i = 0; i < 600 && ran3 && alive(ran3.pid); i++) await delay(100); // the owner notices within a second, drains, stops
|
||||
const orphan = controlJson('runtime', 'status', '--profile', 'app-1');
|
||||
record('a hard-killed app host takes its engine with it', third.job.state === 'done' && (!ran3 || !alive(ran3.pid)), `engine alive=${ran3 ? alive(ran3.pid) : 'n/a'}; runtime=${orphan.json?.state ?? orphan.json?.error?.message}`);
|
||||
const cleanup = controlJson('runtime', 'stop', '--profile', 'app-1');
|
||||
record('nothing is left to stop afterwards', /stopped|not managed/.test(JSON.stringify(cleanup.json)) && controlJson('runtime', 'status', '--profile', 'app-1').json?.state === 'stopped', JSON.stringify(cleanup.json).slice(0, 160));
|
||||
|
||||
// ---- the shell itself is killed (what the desktop shell's crash or kill looks like to the host): the host leaves, and
|
||||
// its engine with it, with nothing announced. Windows also has the job object; this is the path that works everywhere.
|
||||
const started2 = await startHostViaShell();
|
||||
if (started2.failed) { record('host starts under a stand-in shell', false, ''); try { started2.shell.kill('SIGKILL'); } catch { /* gone */ } }
|
||||
else {
|
||||
await call('/api/native?refresh=1');
|
||||
rmSync(mark, { force: true });
|
||||
const fourth = await waitJob((await call('/api/native/operations', 'POST', { kind: 'load', args: { model: modelId } })).body.id, 400);
|
||||
let ran4 = existsSync(mark) ? JSON.parse(readFileSync(mark, 'utf8')) : null;
|
||||
if (real) {
|
||||
const listed = spawnSync('ps', ['-eo', 'pid,args'], { encoding: 'utf8' }).stdout.split('\n').filter(l => l.includes(`${dirs.components}/engine/`) && l.includes('--serve'));
|
||||
ran4 = listed.length ? { pid: Number(listed[0].trim().split(/\s+/)[0]) } : null;
|
||||
}
|
||||
if (isWin) spawnSync('taskkill', ['/PID', String(started2.shell.pid), '/F'], { windowsHide: true }); else started2.shell.kill('SIGKILL');
|
||||
for (let i = 0; i < 100 && alive(started2.hostPid); i++) await delay(100);
|
||||
const hostGone = !alive(started2.hostPid);
|
||||
for (let i = 0; i < 900 && ran4 && alive(ran4.pid); i++) await delay(100);
|
||||
const engineGone = !ran4 || !alive(ran4.pid);
|
||||
record('a killed shell takes the host and its engine with it', fourth.job.state === 'done' && hostGone && engineGone,
|
||||
`host pid ${started2.hostPid} gone=${hostGone}; engine pid ${ran4?.pid} gone=${engineGone}; runtime=${controlJson('runtime', 'status', '--profile', 'app-1').json?.state}`);
|
||||
controlJson('runtime', 'stop', '--profile', 'app-1');
|
||||
}
|
||||
|
||||
await finishSpy();
|
||||
}
|
||||
|
||||
async function finishSpy() {
|
||||
if (spyLog) {
|
||||
const attempts = readFileSync(spyLog, 'utf8').split('\n').filter(Boolean);
|
||||
record('no acquisition attempt by the host during the whole run (spy)', attempts.length === 0, attempts.join(' | '));
|
||||
}
|
||||
}
|
||||
|
||||
try { await main(); }
|
||||
catch (error) { record('flow', false, error.stack ?? error.message); }
|
||||
finally {
|
||||
try { const hid = join(dirs.components, 'engine.hidden-by-flow'); if (real && existsSync(hid) && !existsSync(join(dirs.components, 'engine'))) renameSync(hid, join(dirs.components, 'engine')); } catch { /* reported by the run */ }
|
||||
await stopHost(true);
|
||||
controlJson('runtime', 'stop', '--profile', 'app-1');
|
||||
controlJson('runtime', 'stop', '--profile', 'app-2');
|
||||
}
|
||||
const failed = steps.filter(s => !s.ok);
|
||||
writeFileSync(join(work, 'installed-flow.json'), JSON.stringify({ ok: failed.length === 0, install, work, steps }, null, 1));
|
||||
console.log(`\n${steps.length - failed.length}/${steps.length} ok; report ${join(work, 'installed-flow.json')}`);
|
||||
if (!args.keep && !args.work && failed.length === 0) { try { rmSync(work, { recursive: true, force: true }); } catch { /* leave it */ } }
|
||||
process.exit(failed.length ? 1 : 0);
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Mock-ups of the Windows installer in Strata's own look: four screens (welcome, where, installing, done), light and dark, drawn with
|
||||
* the app's built CSS (apps/host/dist) and brand files, then photographed with Chrome. Nothing here is built or shipped: it is what
|
||||
* the owner is shown before an installer is chosen.
|
||||
*
|
||||
* node apps/desktop/scripts/installer-mockups.mjs [--out <folder>]
|
||||
*/
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]]] : acc), []));
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const host = resolve(here, '../../host');
|
||||
const out = resolve(args.out ?? join(here, '../installer-mockups'));
|
||||
mkdirSync(out, { recursive: true });
|
||||
const dist = join(host, 'dist');
|
||||
const css = readdirSync(join(dist, 'assets')).filter(f => f.endsWith('.css')).map(f => readFileSync(join(dist, 'assets', f), 'utf8')).join('\n');
|
||||
const logo = theme => readFileSync(join(dist, 'brand', `strata-lockup-${theme}.svg`), 'utf8');
|
||||
const { chromium } = await import(pathToFileURL(join(host, 'node_modules/playwright-core/index.mjs')).href).catch(() => import('playwright-core'));
|
||||
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome'].find(existsSync);
|
||||
|
||||
const check = '<svg class="st-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>';
|
||||
const folder = '<svg class="st-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>';
|
||||
|
||||
const screens = {
|
||||
welcome: theme => `<header class="onboard-header"><span class="mock-logo">${logo(theme)}</span></header>
|
||||
<div class="onboard-welcome-copy"><h1>State of the art. At home.</h1><p>Strata runs AI on your own computer.<br>Setting it up takes a few minutes.</p>
|
||||
<div class="onboard-benefits"><span>${check}Private</span><span>${check}Fast</span><span>${check}Easy to use</span></div></div>
|
||||
<footer class="onboard-footer onboard-footer--welcome"><button class="st-btn st-btn--primary">Install Strata</button><small>Installs for you only. No administrator needed.</small></footer>`,
|
||||
where: theme => `<header class="onboard-header"><span class="mock-logo">${logo(theme)}</span></header>
|
||||
<div class="mock-body"><h2 class="mock-title">Where should Strata go?</h2><p class="mock-sub">This is the right place for most people.</p>
|
||||
<div class="onboard-folder">${folder}<div><strong>Your Strata folder</strong><small>C:\\Users\\Alex\\AppData\\Local\\Strata</small></div><button class="st-btn st-btn--sm">Change</button></div>
|
||||
<p class="mock-note">About 310 MB. Your models and chats are kept somewhere else and are never removed by an update.</p></div>
|
||||
<footer class="onboard-footer"><button class="st-btn st-btn--ghost">Back</button><span></span><button class="st-btn st-btn--primary">Install</button></footer>`,
|
||||
installing: theme => `<header class="onboard-header"><span class="mock-logo">${logo(theme)}</span></header>
|
||||
<div class="mock-body"><h2 class="mock-title">Installing Strata…</h2><p class="mock-sub">Copying the app and its engine room. You can keep working meanwhile.</p>
|
||||
<div class="onboard-progress" style="--p:62%"><progress max="100" value="62"></progress></div>
|
||||
<div class="onboard-progress-meta"><span>62% · 190 / 310 MB</span><div class="onboard-progress-actions"><span>About 1 min left</span><button>Cancel</button></div></div></div>
|
||||
<footer class="onboard-footer"><span></span><span class="mock-hint">Next: choose your first model inside Strata.</span><button class="st-btn st-btn--primary" disabled>Installing…</button></footer>`,
|
||||
done: theme => `<header class="onboard-header"><span class="mock-logo">${logo(theme)}</span></header>
|
||||
<div class="onboard-welcome-copy"><h1>Strata is ready.</h1><p>Open it to pick your first model. Strata downloads it for you and gets it ready.</p></div>
|
||||
<footer class="onboard-footer onboard-footer--welcome"><button class="st-btn st-btn--primary">Open Strata</button><small>A shortcut was added to your Start menu and desktop.</small></footer>`,
|
||||
};
|
||||
|
||||
const page = (name, theme) => `<!doctype html><html data-theme="${theme}"><head><meta charset="utf-8"><style>${css}
|
||||
body{background:${theme === 'dark' ? '#0b0d0e' : '#e9edef'};margin:0;display:grid;place-items:center;min-height:100vh}
|
||||
.mock-window{width:720px;border-radius:14px;overflow:hidden;background:var(--st-surface,#fff);border:1px solid var(--st-line,#dde);box-shadow:0 30px 80px #0003}
|
||||
.mock-titlebar{height:34px;display:flex;align-items:center;justify-content:space-between;padding:0 14px;font-size:12px;color:var(--st-ink-muted);background:var(--st-surface-2,#f3f5f6);border-bottom:1px solid var(--st-line-soft,#e6e9eb)}
|
||||
.mock-logo svg{height:22px;width:auto;display:block}
|
||||
.mock-body{padding:8px 36px 28px}.mock-title{font-size:28px;margin:16px 0 8px}.mock-sub{color:var(--st-ink-muted);margin:0 0 22px;line-height:1.6}
|
||||
.mock-note{color:var(--st-ink-muted);font-size:12px;margin-top:16px;line-height:1.6}.mock-hint{font-size:12px;color:var(--st-ink-muted)}
|
||||
.onboard-progress progress{width:100%}
|
||||
.guided-onboarding{min-height:430px;display:flex;flex-direction:column}.onboard-footer{margin-top:auto}
|
||||
</style></head><body><div class="mock-window"><div class="mock-titlebar"><span>Strata setup</span><span>– ☐ ✕</span></div>
|
||||
<div class="setup guided-onboarding" data-step="${name === 'welcome' || name === 'done' ? 1 : 2}">${screens[name](theme)}</div></div></body></html>`;
|
||||
|
||||
const browser = await chromium.launch({ executablePath: CHROME, headless: true });
|
||||
const written = [];
|
||||
for (const theme of ['light', 'dark']) {
|
||||
for (const name of Object.keys(screens)) {
|
||||
const tab = await browser.newPage({ viewport: { width: 860, height: 620 }, deviceScaleFactor: 1 });
|
||||
await tab.setContent(page(name, theme));
|
||||
await tab.waitForTimeout(250);
|
||||
const file = join(out, `${name}-${theme}.png`);
|
||||
await tab.screenshot({ path: file });
|
||||
written.push(file);
|
||||
await tab.close();
|
||||
}
|
||||
}
|
||||
await browser.close();
|
||||
console.log(written.join('\n'));
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* "Use your own GGUF" end to end on a box, through the installed app layout (packaged host + bundled Control + a real engine component):
|
||||
* preview a folder of a supported model, preview an unsupported GGUF (must be refused in plain terms), prepare the supported one
|
||||
* (pack, from the folder, nothing copied), load it, get a chat answer, unload, shut down, and check nothing is left running.
|
||||
*
|
||||
* node local-gguf-e2e.mjs --root /data/ui4-e2e --good /data/141-gyro/models/IQ2_XS --bad /data/ui4-e2e/bad [--context 8192] [--backend cuda|metal]
|
||||
* (the app folder is <root>/app: node/, host/, control/ ...; <root>/components gets the engine, from <root>/app/engine-bundle when the app carries one)
|
||||
*/
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { createServer } from 'node:net';
|
||||
import { join, resolve } from 'node:path';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]]] : acc), []));
|
||||
const root = resolve(args.root);
|
||||
const good = args.good, bad = args.bad;
|
||||
const context = Number(args.context ?? 8192);
|
||||
const app = join(root, 'app');
|
||||
const dirs = { components: join(root, 'components'), install: join(root, 'install'), profiles: join(root, 'profiles'), runtime: join(root, 'runtime'), home: join(root, 'home'), xdg: join(root, 'xdg'), appdata: join(root, 'appdata') };
|
||||
for (const d of [dirs.install, dirs.home, dirs.xdg, dirs.appdata]) mkdirSync(d, { recursive: true });
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
const steps = [];
|
||||
let failed = false;
|
||||
const record = (name, ok, detail = '') => { steps.push({ name, ok: !!ok, detail: String(detail).slice(0, 400) }); console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ' ' + String(detail).slice(0, 300) : ''}`); if (!ok) failed = true; };
|
||||
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
|
||||
|
||||
const env = { ...process.env, STRATA_COMPONENTS_DIR: dirs.components, STRATA_PROFILE_DIR: dirs.profiles, STRATA_RUNTIME_DIR: dirs.runtime, STRATA_INSTALL_DIR: dirs.install, STRATA_HOME: dirs.home, STRATA_MODELS_DIR: join(root, 'Strata-data'), STRATA_ENGINE_BACKEND: args.backend ?? 'cuda', XDG_CONFIG_HOME: dirs.xdg, APPDATA: dirs.appdata, PYTHONDONTWRITEBYTECODE: '1' };
|
||||
delete env.STRATA_API_KEY;
|
||||
const port = await freePort();
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
const host = spawn(join(app, 'node', 'node'), [join(app, 'host', 'server', 'index.mjs')], { cwd: join(app, 'host'), stdio: ['ignore', 'pipe', 'pipe'], detached: false,
|
||||
env: { ...env, STRATA_PORT: String(port), STRATA_DIST: join(app, 'host', 'dist'), NODE_ENV: 'production', STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1', STRATA_NO_ACQUIRE: '1', STRATA_EXIT_WITH_PARENT: '1',
|
||||
STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()), STRATA_WORKSPACE: join(root, 'workspace') } });
|
||||
mkdirSync(join(root, 'workspace'), { recursive: true });
|
||||
let hostLog = ''; host.stdout.on('data', c => { hostLog += c; }); host.stderr.on('data', c => { hostLog += c; });
|
||||
const call = async (path, method = 'GET', body) => { const r = await fetch(base + path, { method, headers: { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) }); return { status: r.status, body: await r.json().catch(() => null) }; };
|
||||
async function waitJob(id, seconds) {
|
||||
const until = Date.now() + seconds * 1000;
|
||||
let last = '';
|
||||
while (Date.now() < until) {
|
||||
const state = (await call('/api/native')).body;
|
||||
const job = state.jobs.find(j => j.id === id);
|
||||
if (job && job.state !== 'running') return { job, state };
|
||||
if (job && job.summary !== last) { last = job.summary; console.log(' ...', String(job.summary).slice(0, 140)); }
|
||||
await delay(2000);
|
||||
}
|
||||
throw new Error('operation did not finish in time');
|
||||
}
|
||||
const engineProcs = () => spawnSync('ps', ['-eo', 'pid,args'], { encoding: 'utf8' }).stdout.split('\n').filter(l => l.includes(join(dirs.components, 'engine')) && /\/strata( |$)/.test(l));
|
||||
|
||||
async function main() {
|
||||
let up = false;
|
||||
for (let i = 0; i < 200 && !up; i++) { try { up = (await fetch(base + '/api/health')).ok; } catch { await delay(150); } }
|
||||
record('host healthy', up, up ? base : hostLog.slice(-500));
|
||||
if (!up) return;
|
||||
let status = (await call('/api/control/status')).body;
|
||||
for (let i = 0; i < 180 && status?.engineSetup; i++) { await delay(1000); status = (await call('/api/control/status')).body; } // the engine that came with the app is copied on the first start
|
||||
record('Control status: bundled, engine usable', status?.engine?.usable === true, `engine=${status?.engine?.engine?.version} ${status?.engine?.notice ?? ''}`);
|
||||
await call('/api/native?refresh=1');
|
||||
|
||||
// ---- the unsupported GGUF: a plain refusal
|
||||
const badPreview = await call('/api/native/local/preview', 'POST', { path: bad });
|
||||
const badIssues = badPreview.body?.preview?.issues ?? badPreview.body?.issues ?? [];
|
||||
const badStatus = badPreview.body?.preview?.file_set_status;
|
||||
console.log(' bad preview:', badPreview.status, JSON.stringify(badPreview.body).slice(0, 400));
|
||||
record('an unsupported GGUF is refused, nothing adopted', badPreview.status === 200 ? (badStatus !== 'plausible' || !badPreview.body.preview.candidate_selection) : badPreview.status >= 400, `status=${badPreview.status}; file_set_status=${badStatus}; issues=${JSON.stringify(badIssues)}`);
|
||||
|
||||
// ---- the supported folder
|
||||
const goodPreview = await call('/api/native/local/preview', 'POST', { path: good });
|
||||
const selection = goodPreview.body?.preview?.candidate_selection;
|
||||
console.log(' good preview:', JSON.stringify(goodPreview.body).slice(0, 500));
|
||||
record('a folder of a supported model is recognised', goodPreview.status === 200 && goodPreview.body?.preview?.file_set_status === 'plausible' && !!selection, `family=${selection?.family}; model=${selection?.model}; files=${goodPreview.body?.preview?.file_count}`);
|
||||
if (!selection || args.only === 'preview') return;
|
||||
|
||||
// ---- prepare (what the page's button does): install from the folder, nothing copied or downloaded
|
||||
const native0 = (await call('/api/native?refresh=1')).body;
|
||||
const catalogSize = native0.catalog?.families?.find(f => f.family === selection.family)?.sizes.find(s => s.model === selection.model);
|
||||
record('the catalog lists the recognised version', !!catalogSize, catalogSize ? `id=${catalogSize.id}` : `no ${selection.family}/${selection.model} in the catalog`);
|
||||
const started = await call('/api/native/operations', 'POST', { kind: 'install', args: { family: selection.family, model: selection.model, gguf_dir: good, context, vision: 'no' } });
|
||||
record('prepare starts', started.status < 300 && !!started.body?.id, `status=${started.status} ${JSON.stringify(started.body).slice(0, 200)}`);
|
||||
if (!started.body?.id) return;
|
||||
const prepared = await waitJob(started.body.id, 3600);
|
||||
record('prepare finishes', prepared.job.state === 'done', `${prepared.job.state}: ${prepared.job.error ?? prepared.job.summary}`);
|
||||
if (prepared.job.state !== 'done') return;
|
||||
const native1 = (await call('/api/native?refresh=1')).body;
|
||||
const modelId = catalogSize?.id ?? `${selection.family === 'qwen' ? '' : selection.family + '-'}${String(selection.model).toLowerCase()}`;
|
||||
const entry = native1.installed?.models?.find(m => m.model === modelId);
|
||||
record('the model is ready and points at the folder, not a copy', entry?.ready === true, `model=${modelId}; ready=${entry?.ready}; dataFolder=${entry?.dataFolder}`);
|
||||
if (!entry?.ready) return;
|
||||
|
||||
// ---- load
|
||||
const load = await call('/api/native/operations', 'POST', { kind: 'load', args: { model: modelId } });
|
||||
const loaded = await waitJob(load.body.id, 1800);
|
||||
const instance = loaded.state.instances[0];
|
||||
record('load starts the engine from the component folder', loaded.job.state === 'done' && instance?.status === 'ready', `job=${loaded.job.state} ${loaded.job.error ?? ''}; instance=${instance?.status}`);
|
||||
if (instance?.status !== 'ready') return;
|
||||
record('one engine process is running', engineProcs().length === 1, engineProcs().map(l => l.trim().slice(0, 140)).join(' | '));
|
||||
|
||||
// ---- a chat answer, checked
|
||||
const chat = await fetch(base + '/api/chat/stream', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ sessionId: 'e2e-chat', message: 'Reply with exactly one word: pong', history: [], nativeModelId: instance.id, thinking: 'Off', agent: false, workspaceId: null }) });
|
||||
let text = '', done = null;
|
||||
for await (const chunk of chat.body) {
|
||||
for (const line of Buffer.from(chunk).toString('utf8').split('\n')) {
|
||||
if (!line.trim()) continue;
|
||||
try { const f = JSON.parse(line); if (f.type === 'delta') text += f.text ?? ''; if (f.type === 'done') done = f; if (f.type === 'error') text += ` [error: ${f.message}]`; } catch { /* partial line */ }
|
||||
}
|
||||
}
|
||||
record('the model answers through the app (the answer text is checked)', chat.status === 200 && /pong/i.test(text), `status=${chat.status}; answer=${JSON.stringify(text.slice(0, 120))}; tokensOut=${done?.tokensOut}; tps=${done?.tps}`);
|
||||
|
||||
// ---- unload, shutdown, nothing left
|
||||
const unload = await call(`/api/native/instances/${instance.id}/unload`, 'POST');
|
||||
await delay(3000);
|
||||
record('unload stops the engine', unload.status < 300 && engineProcs().length === 0, `status=${unload.status}; engines left=${engineProcs().length}`);
|
||||
}
|
||||
|
||||
try { await main(); } catch (e) { record('the flow', false, e?.stack ?? e); }
|
||||
host.kill();
|
||||
for (let i = 0; i < 100 && host.exitCode === null; i++) await delay(100);
|
||||
if (host.exitCode === null) host.kill('SIGKILL');
|
||||
await delay(2000);
|
||||
const left = spawnSync('ps', ['-eo', 'pid,args'], { encoding: 'utf8' }).stdout.split('\n').filter(l => (l.includes(dirs.install) || l.includes(join(app, 'host'))) && !l.includes('ps -eo'));
|
||||
record('nothing of this run is left running', left.length === 0, left.join(' | ').slice(0, 300));
|
||||
writeFileSync(join(root, 'e2e-result.json'), JSON.stringify({ steps, hostLogTail: hostLog.slice(-2000) }, null, 1));
|
||||
console.log(failed ? 'FAILED' : 'ALL OK');
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the Strata DMG without a Finder session.
|
||||
#
|
||||
# Tauri's default DMG step runs an AppleScript that asks Finder to arrange the window (icon
|
||||
# positions, background). Finder AppleEvents time out in a non-GUI or sandboxed context — observed
|
||||
# here as `Finder got an error: AppleEvent timed out. (-1712)` — so the default path cannot be
|
||||
# relied on for an automated build. The upstream `create-dmg` script already supports exactly this
|
||||
# case via `--skip-jenkins`, which produces a plain, correct DMG with the app and the Applications
|
||||
# drop link and no cosmetic arrangement.
|
||||
#
|
||||
# Usage: scripts/make-dmg.sh [output.dmg]
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DESKTOP="$(cd "$HERE/.." && pwd)"
|
||||
BUNDLE="$DESKTOP/src-tauri/target/release/bundle"
|
||||
APP="$BUNDLE/macos/Strata.app"
|
||||
VERSION="$(grep -m1 '^version = ' "$DESKTOP/src-tauri/Cargo.toml" | cut -d'"' -f2)" # the one place the app version is written
|
||||
OUT="${1:-$BUNDLE/dmg/Strata_${VERSION}_aarch64.dmg}"
|
||||
|
||||
if [ ! -d "$APP" ]; then
|
||||
echo "error: $APP does not exist; run 'cargo tauri build --bundles app' first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK="$(mktemp -d -t strata-dmg)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
# A staging folder is what create-dmg copies into the image: the app, and nothing else.
|
||||
#
|
||||
# The Applications drop link is created by `--app-drop-link` below. Adding one here as well makes
|
||||
# create-dmg fail with "File exists", because it links into the volume it is assembling.
|
||||
mkdir -p "$WORK/stage"
|
||||
cp -R "$APP" "$WORK/stage/"
|
||||
|
||||
# Reuse Tauri's vendored create-dmg if the bundler left it behind, otherwise take it from the
|
||||
# bundle cache. Building it yourself is not necessary and not expected.
|
||||
SCRIPT="$BUNDLE/dmg/bundle_dmg.sh"
|
||||
if [ ! -x "$SCRIPT" ]; then
|
||||
echo "error: $SCRIPT not found; run 'cargo tauri build --bundles dmg' once to vendor it" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$OUT")"
|
||||
rm -f "$OUT"
|
||||
|
||||
# --skip-jenkins: no Finder AppleScript. Everything else is the same code path Tauri uses.
|
||||
"$SCRIPT" \
|
||||
--volname "Strata $VERSION" \
|
||||
--volicon "$BUNDLE/macos/Strata.app/Contents/Resources/Strata.icns" \
|
||||
--app-drop-link 400 200 \
|
||||
--skip-jenkins \
|
||||
--no-internet-enable \
|
||||
"$OUT" "$WORK/stage"
|
||||
|
||||
echo
|
||||
echo "built: $OUT"
|
||||
ls -lh "$OUT" | awk '{print "size: " $5}'
|
||||
shasum -a 256 "$OUT"
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* The app and installer icon, refined from the existing Strata mark (apps/host/public/brand/strata-mark-*.svg and the current
|
||||
* icons/icon.png): the same three rounded bars on the same dark tile, in the same colours. Nothing is redrawn. What changes is
|
||||
* how it is cut for small sizes: at 16-48 px the bars are snapped to whole pixels and made a little bolder and wider so they
|
||||
* stay crisp instead of blurring; from 64 px up the geometry is the mark's own proportions.
|
||||
*
|
||||
* node apps/desktop/scripts/make-icons.mjs [--out <folder>] writes icon-<size>.png for every size and icon.ico
|
||||
*/
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]]] : acc), []));
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const host = resolve(here, '../../host');
|
||||
const out = resolve(args.out ?? join(here, '../src-tauri/icons'));
|
||||
mkdirSync(out, { recursive: true });
|
||||
const { chromium } = await import(pathToFileURL(join(host, 'node_modules/playwright-core/index.mjs')).href).catch(() => import('playwright-core'));
|
||||
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome'].find(existsSync);
|
||||
|
||||
const TILE = '#161a1d', LIGHT = '#eef1f3', GREEN = '#34d399'; // the colours of the current icon.png
|
||||
|
||||
/** Integer geometry for the small sizes: bars on whole pixels, centred. Returns [{x,y,w,h,fill}]. */
|
||||
function hinted(size) {
|
||||
const bar = Math.max(2, Math.round(size * 0.14)), gap = Math.max(2, Math.round(size * 0.1)), mid = bar + 1;
|
||||
const total = 2 * bar + mid + 2 * gap, top = Math.floor((size - total) / 2);
|
||||
const even = w => (w % 2 === size % 2 ? w : w + 1);
|
||||
const wide = even(Math.round(size * 0.5)), long = even(Math.round(size * 0.76));
|
||||
return [
|
||||
{ x: (size - wide) / 2, y: top, w: wide, h: bar, fill: LIGHT },
|
||||
{ x: (size - long) / 2, y: top + bar + gap, w: long, h: mid, fill: GREEN },
|
||||
{ x: (size - wide) / 2, y: top + bar + gap + mid + gap, w: wide, h: bar, fill: LIGHT },
|
||||
];
|
||||
}
|
||||
/** The mark's own proportions (from icon.png at 512): bars 34% / 50% / 34% wide, 9% high, on a tile with 21.5% corners. */
|
||||
function proportional(size) {
|
||||
const k = size / 512;
|
||||
return [
|
||||
{ x: 170 * k, y: 156 * k, w: 173 * k, h: 46 * k, fill: LIGHT },
|
||||
{ x: 128 * k, y: 232 * k, w: 256 * k, h: 48 * k, fill: GREEN },
|
||||
{ x: 170 * k, y: 309 * k, w: 173 * k, h: 46 * k, fill: LIGHT },
|
||||
];
|
||||
}
|
||||
function svg(size) {
|
||||
const small = size <= 48;
|
||||
const bars = (small ? hinted(size) : proportional(size));
|
||||
const radius = small ? Math.round(size * 0.22) : size * 0.215;
|
||||
const rects = bars.map(b => `<rect x="${b.x}" y="${b.y}" width="${b.w}" height="${b.h}" rx="${small ? Math.min(b.h / 2, 1.5 + size / 32) : b.h / 2}" fill="${b.fill}"${small ? ' shape-rendering="crispEdges"' : ''}/>`).join('');
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}"><rect width="${size}" height="${size}" rx="${radius}" fill="${TILE}"/>${rects}</svg>`;
|
||||
}
|
||||
|
||||
const SIZES = [16, 20, 24, 32, 40, 48, 64, 96, 128, 256, 512, 1024];
|
||||
const browser = await chromium.launch({ executablePath: CHROME, headless: true });
|
||||
const png = {};
|
||||
for (const size of SIZES) {
|
||||
const page = await browser.newPage({ viewport: { width: size, height: size }, deviceScaleFactor: 1 });
|
||||
await page.setContent(`<!doctype html><body style="margin:0;background:transparent">${svg(size)}</body>`);
|
||||
png[size] = await page.screenshot({ omitBackground: true, clip: { x: 0, y: 0, width: size, height: size } });
|
||||
await page.close();
|
||||
writeFileSync(join(out, `icon-${size}.png`), png[size]);
|
||||
}
|
||||
await browser.close();
|
||||
|
||||
// The .ico: one PNG image per size (16-256), the sizes Windows asks for at 100-250% scaling.
|
||||
const ICO = [16, 20, 24, 32, 40, 48, 64, 96, 128, 256];
|
||||
const head = Buffer.alloc(6); head.writeUInt16LE(0, 0); head.writeUInt16LE(1, 2); head.writeUInt16LE(ICO.length, 4);
|
||||
let offset = 6 + 16 * ICO.length;
|
||||
const entries = [], blobs = [];
|
||||
for (const size of ICO) {
|
||||
const data = png[size], e = Buffer.alloc(16);
|
||||
e.writeUInt8(size === 256 ? 0 : size, 0); e.writeUInt8(size === 256 ? 0 : size, 1); e.writeUInt8(0, 2); e.writeUInt8(0, 3);
|
||||
e.writeUInt16LE(1, 4); e.writeUInt16LE(32, 6); e.writeUInt32LE(data.length, 8); e.writeUInt32LE(offset, 12);
|
||||
offset += data.length; entries.push(e); blobs.push(data);
|
||||
}
|
||||
writeFileSync(join(out, 'icon.ico'), Buffer.concat([head, ...entries, ...blobs]));
|
||||
writeFileSync(join(out, 'icon.png'), png[512]);
|
||||
console.log('icons in', out, ICO.join(','));
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* The installer's pictures, drawn from the app's own design: the light surface and ink colours of the app (src/styles tokens), the
|
||||
* brand lockup and mark files, GT Walsheim Bold for the words (baked into the pictures, because NSIS cannot use a web font).
|
||||
*
|
||||
* node apps/desktop/scripts/make-installer-images.mjs [--out <folder>] [--preview <folder>]
|
||||
*
|
||||
* Writes sidebar.bmp (164x314, the welcome and finish pages) and header.bmp (150x57, the other pages) as 24-bit BMPs, and with
|
||||
* --preview the same as PNGs at 3x.
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]]] : acc), []));
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const host = resolve(here, '../../host');
|
||||
const out = resolve(args.out ?? join(here, '../src-tauri/nsis'));
|
||||
mkdirSync(out, { recursive: true });
|
||||
const { chromium } = await import(pathToFileURL(join(host, 'node_modules/playwright-core/index.mjs')).href).catch(() => import('playwright-core'));
|
||||
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome'].find(existsSync);
|
||||
const font = pathToFileURL(join(host, 'public/fonts/GTWalsheimPro-Bold.woff2')).href;
|
||||
const fontMedium = pathToFileURL(join(host, 'public/fonts/GTWalsheimPro-Medium.woff2')).href;
|
||||
const lockup = readFileSync(join(host, 'public/brand/strata-lockup-light.svg'), 'utf8');
|
||||
const mark = readFileSync(join(host, 'public/brand/strata-mark-light.svg'), 'utf8');
|
||||
|
||||
// the app's light tokens
|
||||
const SURFACE = '#f9fafb', INK = '#171b1e', MUTED = '#5b646b', ACCENT = '#10b981', LINE = '#e3e8ea';
|
||||
const css = `@font-face{font-family:GT;font-weight:700;src:url(${font})}@font-face{font-family:GT;font-weight:500;src:url(${fontMedium})}
|
||||
*{box-sizing:border-box}body{margin:0;font-family:GT,'Segoe UI',sans-serif;color:${INK};background:${SURFACE}}`;
|
||||
|
||||
const pages = {
|
||||
sidebar: { w: 164, h: 314, html: `<div style="width:164px;height:314px;padding:22px 18px;display:flex;flex-direction:column;background:linear-gradient(180deg,#f9fafb 0%,#eef6f2 100%);border-right:1px solid ${LINE}">
|
||||
<div style="height:26px">${lockup.replace('<svg', '<svg style="height:26px;width:auto;display:block"')}</div>
|
||||
<div style="margin-top:auto">
|
||||
<div style="font-weight:700;font-size:22px;line-height:1.12;letter-spacing:-.01em">State of<br>the art.<br>At home.</div>
|
||||
<div style="margin-top:10px;font-weight:500;font-size:11px;line-height:1.5;color:${MUTED}">AI on your own computer.<br>Private. Fast. Easy.</div>
|
||||
<div style="margin-top:16px;height:3px;width:34px;border-radius:2px;background:${ACCENT}"></div>
|
||||
</div></div>` },
|
||||
header: { w: 150, h: 57, html: `<div style="width:150px;height:57px;display:flex;align-items:center;justify-content:flex-end;padding-right:12px;background:${SURFACE}">${lockup.replace('<svg', '<svg style="height:22px;width:auto;display:block"')}</div>` },
|
||||
};
|
||||
|
||||
const browser = await chromium.launch({ executablePath: CHROME, headless: true });
|
||||
const work = join(tmpdir(), 'strata-installer-images'); mkdirSync(work, { recursive: true });
|
||||
for (const [name, page] of Object.entries(pages)) {
|
||||
for (const scale of [1, 3]) {
|
||||
const file = join(work, `${name}.html`);
|
||||
writeFileSync(file, `<!doctype html><meta charset="utf-8"><style>${css}</style><body>${page.html}</body>`);
|
||||
const tab = await browser.newPage({ viewport: { width: page.w, height: page.h }, deviceScaleFactor: scale });
|
||||
await tab.goto(pathToFileURL(file).href);
|
||||
await tab.evaluate(() => document.fonts.ready);
|
||||
await tab.waitForTimeout(200);
|
||||
const png = join(work, `${name}@${scale}.png`);
|
||||
await tab.screenshot({ path: png, clip: { x: 0, y: 0, width: page.w, height: page.h } });
|
||||
await tab.close();
|
||||
if (scale === 3 && args.preview) { mkdirSync(resolve(args.preview), { recursive: true }); writeFileSync(join(resolve(args.preview), `${name}.png`), readFileSync(png)); }
|
||||
}
|
||||
}
|
||||
await browser.close();
|
||||
// 24-bit BMP, the format NSIS wants: render at 3x, shrink with a good filter
|
||||
const py = `import sys\nfrom PIL import Image\nfor name in ('sidebar','header'):\n im = Image.open(sys.argv[1] + '/' + name + '@3.png').convert('RGB')\n w, h = im.width // 3, im.height // 3\n im.resize((w, h), Image.LANCZOS).save(sys.argv[2] + '/' + name + '.bmp', 'BMP')\n`;
|
||||
const done = spawnSync('python', ['-c', py, work, out], { encoding: 'utf8' });
|
||||
if (done.status !== 0) { console.error(done.stderr); process.exit(1); }
|
||||
console.log('wrote', join(out, 'sidebar.bmp'), join(out, 'header.bmp'));
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
# Write a tester's README with the checksums and sizes of the files it names, computed from the files themselves.
|
||||
#
|
||||
# apps/desktop/scripts/make-tester-readme.sh <template> <out.txt> --appimage <file name> --files-dir <dir> [--files-dir <dir> ...]
|
||||
#
|
||||
# Why: a README that carries a hand-pasted sha256 goes stale the moment the file is rebuilt (the Ubuntu 26.04 tester compared the file
|
||||
# with the old build's hash). The template holds placeholders instead, and this script fills them from the real files:
|
||||
# @APPIMAGE@ the AppImage's file name
|
||||
# @APP_VERSION@ the app version, read from apps/desktop/src-tauri/Cargo.toml
|
||||
# @SHA256:<name>@ sha256 of the file <name> found in one of the --files-dir folders
|
||||
# @SIZE_GB:<name>@ its size in GB with one decimal
|
||||
# @INCLUDE:<file>@ (alone on a line) the contents of <file> from the template's folder, with the same placeholders
|
||||
# It refuses to write the README when a named file is missing or a placeholder is left over, so a stale or empty hash cannot ship.
|
||||
# The same check is done by `--verify <readme> --files-dir <dir>`: every "<64 hex> <name>" or "sha256 <64 hex>" pair that names a file
|
||||
# present in the folders must match that file.
|
||||
set -euo pipefail
|
||||
|
||||
usage() { echo "usage: $0 <template> <out.txt> --appimage <name> --files-dir <dir> [--files-dir <dir> ...]" >&2; echo " $0 --verify <readme> --files-dir <dir> [...]" >&2; exit 2; }
|
||||
|
||||
VERIFY=""; TEMPLATE=""; OUT=""; APPIMAGE=""; DIRS=()
|
||||
if [ "${1:-}" = "--verify" ]; then VERIFY="${2:?readme}"; shift 2; else TEMPLATE="${1:?template}"; OUT="${2:?out}"; shift 2 || usage; fi
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--appimage) APPIMAGE="$2"; shift 2 ;;
|
||||
--files-dir) DIRS+=("$2"); shift 2 ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
[ ${#DIRS[@]} -gt 0 ] || usage
|
||||
|
||||
find_file() { local d; for d in "${DIRS[@]}"; do [ -f "$d/$1" ] && { echo "$d/$1"; return 0; }; done; return 1; }
|
||||
sha_of() { sha256sum "$1" | awk '{print $1}'; }
|
||||
size_gb() { awk -v b="$(stat -c %s "$1" 2>/dev/null || stat -f %z "$1")" 'BEGIN{printf "%.1f", b/1000000000}'; }
|
||||
|
||||
if [ -n "$VERIFY" ]; then
|
||||
# every checksum in the README must be the checksum of a file that is here, and a line that names a file and carries a checksum must carry that file's checksum
|
||||
hashes=""; bad=0; seen=0
|
||||
for d in "${DIRS[@]}"; do for f in "$d"/*; do [ -f "$f" ] && grep -qF "$(basename "$f")" "$VERIFY" && hashes="$hashes $(sha_of "$f")"; done; done # only files the README names are hashed (a folder may hold big unrelated files)
|
||||
readme_hashes="$(grep -Eo '[0-9a-f]{64}' "$VERIFY" | sort -u || true)"
|
||||
for h in $readme_hashes; do
|
||||
seen=$((seen + 1))
|
||||
case " $hashes " in *" $h "*) ;; *) echo "STALE: the README lists $h, which is the checksum of no file in: ${DIRS[*]}" >&2; bad=$((bad + 1)) ;; esac
|
||||
done
|
||||
for d in "${DIRS[@]}"; do for f in "$d"/*; do
|
||||
[ -f "$f" ] || continue
|
||||
grep -qF "$(basename "$f")" "$VERIFY" || continue
|
||||
# a line that names this file AND carries a checksum must carry this file's checksum (a sentence that merely mentions the file is fine)
|
||||
line_hash="$(grep -F "$(basename "$f")" "$VERIFY" | grep -Eo '[0-9a-f]{64}' | head -1 || true)"
|
||||
if [ -n "$line_hash" ] && [ "$line_hash" != "$(sha_of "$f")" ]; then
|
||||
echo "STALE: $(basename "$f") is $(sha_of "$f"), the README says $line_hash" >&2; bad=$((bad + 1))
|
||||
fi
|
||||
done; done
|
||||
echo "$seen checksum(s) in the README checked, $bad stale"
|
||||
[ "$bad" = 0 ] && [ "$seen" -gt 0 ]
|
||||
exit
|
||||
fi
|
||||
|
||||
[ -n "$APPIMAGE" ] || usage
|
||||
TDIR="$(cd "$(dirname "$TEMPLATE")" && pwd)"
|
||||
# the app version has one source (the same file Tauri and the shell use)
|
||||
APP_VERSION="$(grep -m1 '^version = ' "$TDIR/../src-tauri/Cargo.toml" | cut -d'"' -f2)"
|
||||
[ -n "$APP_VERSION" ] || { echo "make-tester-readme: no version in $TDIR/../src-tauri/Cargo.toml" >&2; exit 1; }
|
||||
TMP="$(mktemp)"; trap 'rm -f "$TMP" "$TMP.2"' EXIT
|
||||
# includes first (one level), then the placeholders
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
if [[ "$line" =~ ^@INCLUDE:([^@]+)@$ ]]; then tr -d '\r' < "$TDIR/${BASH_REMATCH[1]}"; else printf '%s\n' "$line"; fi
|
||||
done < <(tr -d '\r' < "$TEMPLATE") | sed "s|@APPIMAGE@|$APPIMAGE|g; s|@APP_VERSION@|$APP_VERSION|g" > "$TMP"
|
||||
|
||||
# @SHA256:name@ and @SIZE_GB:name@
|
||||
while IFS= read -r token; do
|
||||
kind="${token#@}"; kind="${kind%%:*}"; name="${token#@*:}"; name="${name%@}"
|
||||
f="$(find_file "$name")" || { echo "make-tester-readme: no file named $name in: ${DIRS[*]} (a README must not carry a hash it did not compute)" >&2; exit 1; }
|
||||
case "$kind" in
|
||||
SHA256) value="$(sha_of "$f")" ;;
|
||||
SIZE_GB) value="$(size_gb "$f")" ;;
|
||||
*) echo "make-tester-readme: unknown placeholder $token" >&2; exit 1 ;;
|
||||
esac
|
||||
# replace every occurrence of this exact token (plain text, no pattern characters)
|
||||
text="$(cat "$TMP"; printf x)"; text="${text%x}"
|
||||
printf '%s' "${text//"$token"/$value}" > "$TMP"
|
||||
done < <(grep -Eo '@(SHA256|SIZE_GB):[^@ ]+@' "$TMP" | sort -u)
|
||||
|
||||
if grep -Eq '@[A-Z0-9_]+[:@]' "$TMP"; then echo "make-tester-readme: placeholders left over:" >&2; grep -En '@[A-Z0-9_]+[:@]' "$TMP" >&2; exit 1; fi
|
||||
cp "$TMP" "$OUT"
|
||||
echo "wrote $OUT"
|
||||
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Attached pictures reaching a model, end to end on a test box, through the installed app layout (packaged host + bundled Control + an engine
|
||||
* component that carries strata-vision):
|
||||
* 1. the model catalog marks the picture-capable models (images: true) when the picture reader is installed;
|
||||
* 2. API models: a llama-server with a small picture model (--mmproj) is saved as a Custom model API; the host works out that it reads pictures,
|
||||
* and a real picture goes out as image_url and is answered. A server without a picture reader, a model set "not to read pictures", and an API
|
||||
* of another style each get a plain refusal (409) that says how to fix it; ticking "This model reads pictures" lets the picture through;
|
||||
* 3. a local model (unless --skip-local): Download and prepare with Read pictures ON, load, and a picture through the app's own chat.
|
||||
*
|
||||
* node pics-e2e.mjs --install <node/ host/ control/> --components <engine components> --work <scratch> --image shapes.png \
|
||||
* Also: --silent <url> (a server whose model list says nothing about pictures: ON by default), --agent (the same picture through the agent; needs a Harness)
|
||||
* --vlm http://127.0.0.1:18090/v1 --vlm-model smolvlm --text-only http://127.0.0.1:18091/v1 [--skip-local --family qwen --model IQ3_XXS --data <folder> --device cpu]
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
|
||||
import { createServer } from 'node:net';
|
||||
import { join, resolve } from 'node:path';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]?.startsWith('--') || all[i + 1] === undefined ? true : all[i + 1]]] : acc), []));
|
||||
for (const need of ['install', 'components', 'work', 'image']) if (!args[need]) { console.error('missing --' + need); process.exit(2); }
|
||||
const install = resolve(args.install), work = resolve(args.work), components = resolve(args.components);
|
||||
const want = String(args.match ?? args.model ?? 'IQ3_XXS').toLowerCase(), contextSize = Number(args.context ?? 32768), skipLocal = args['skip-local'] === true, device = String(args.device ?? 'cpu'), family = String(args.family ?? 'qwen'), modelId = String(args.model ?? 'IQ3_XXS');
|
||||
const node = join(install, 'node', 'node'), hostEntry = join(install, 'host', 'server', 'index.mjs');
|
||||
const dirs = { models: join(work, 'models'), profiles: join(work, 'profiles'), runtime: join(work, 'runtime'), home: join(work, 'home'), xdg: join(work, 'xdg'), appdata: join(work, 'appdata') };
|
||||
for (const d of [dirs.models, dirs.home, dirs.xdg, dirs.appdata]) mkdirSync(d, { recursive: true });
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
let failed = false;
|
||||
const record = (name, ok, detail = '') => { if (!ok) failed = true; console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ' ' + String(detail).slice(0, 600) : ''}`); };
|
||||
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
|
||||
const env = { ...process.env, STRATA_COMPONENTS_DIR: components, STRATA_PROFILE_DIR: dirs.profiles, STRATA_RUNTIME_DIR: dirs.runtime, STRATA_INSTALL_DIR: dirs.models,
|
||||
STRATA_HOME: dirs.home, STRATA_MODELS_DIR: join(work, 'models-unused'), APPDATA: dirs.appdata, XDG_CONFIG_HOME: dirs.xdg, PYTHONDONTWRITEBYTECODE: '1' };
|
||||
delete env.STRATA_API_KEY;
|
||||
const port = await freePort();
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
const host = spawn(node, [hostEntry], { cwd: join(install, 'host'), stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: { ...env, STRATA_PORT: String(port), STRATA_DIST: join(install, 'host', 'dist'), NODE_ENV: 'production', STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1', STRATA_NO_ACQUIRE: '1',
|
||||
STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()) } });
|
||||
let hostLog = '';
|
||||
host.stdout.on('data', c => { hostLog += c; }); host.stderr.on('data', c => { hostLog += c; });
|
||||
const call = async (path, method = 'GET', body) => {
|
||||
const response = await fetch(base + path, { method, headers: { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
|
||||
return { status: response.status, body: await response.json().catch(() => null) };
|
||||
};
|
||||
const png = 'data:image/png;base64,' + readFileSync(resolve(args.image)).toString('base64');
|
||||
const QUESTION = 'Name each shape in this picture with its colour, and read out any text.';
|
||||
|
||||
/** One chat turn through the app's own endpoint: the plain-text answer, or the refusal (status + message). */
|
||||
async function chat(extra, message = QUESTION) {
|
||||
const response = await fetch(base + '/api/chat/stream', { method: 'POST', headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ message, agent: false, images: [{ url: png, name: 'shapes.png' }], ...extra }) });
|
||||
if (response.status !== 200) { const b = await response.json().catch(() => ({})); return { status: response.status, message: b.error ?? b.message ?? '' }; }
|
||||
let text = '', error = '';
|
||||
for (const line of (await response.text()).split('\n')) {
|
||||
if (!line.trim()) continue;
|
||||
let f; try { f = JSON.parse(line); } catch { continue; }
|
||||
if (f.type === 'error') error = f.message ?? 'error';
|
||||
else if (typeof f.text === 'string' && f.type === 'delta') text += f.text;
|
||||
}
|
||||
return { status: 200, text, error };
|
||||
}
|
||||
const saveApi = async (name, baseUrl, models, protocol = 'openai-chat') => {
|
||||
const existing = (await call('/api/model-providers')).body.connections.find(c => c.name === name);
|
||||
return (await call('/api/model-providers', 'POST', { ...(existing ? { id: existing.id } : {}), kind: 'custom', provider: null, name, baseUrl, protocol, apiKey: '', models })).body;
|
||||
};
|
||||
async function readsAfter(connectionName, id, want, seconds = 20) {
|
||||
for (let i = 0; i < seconds * 2; i++) {
|
||||
const c = (await call('/api/model-providers')).body.connections.find(x => x.name === connectionName);
|
||||
const m = c?.models.find(x => x.id === id);
|
||||
if (m && m.reads === want) return { c, m };
|
||||
await delay(500);
|
||||
}
|
||||
const c = (await call('/api/model-providers')).body.connections.find(x => x.name === connectionName);
|
||||
return { c, m: c?.models.find(x => x.id === id) };
|
||||
}
|
||||
async function waitJob(id, seconds) {
|
||||
let last = '';
|
||||
for (let i = 0; i < seconds * 2; i++) {
|
||||
const state = (await call('/api/native')).body;
|
||||
const job = state.jobs.find(j => j.id === id);
|
||||
if (job && job.summary !== last) { last = job.summary; console.log(' ... ' + String(last).slice(0, 120)); }
|
||||
if (job && job.state !== 'running') return { job, state };
|
||||
await delay(500);
|
||||
}
|
||||
throw new Error('operation did not finish');
|
||||
}
|
||||
const names = low => /(red|blue|green|circle|square|triangle|strata|42)/.test(low);
|
||||
|
||||
async function main() {
|
||||
let up = false;
|
||||
for (let i = 0; i < 200 && !up; i++) { try { up = (await fetch(base + '/api/health')).ok; } catch { /* not yet */ } if (!up) await delay(150); }
|
||||
record('host healthy', up, up ? base : hostLog.slice(-500));
|
||||
if (!up) return;
|
||||
|
||||
// ---- 1. the catalog
|
||||
const packages = existsSync(join(components, 'engine')) ? readdirSync(join(components, 'engine')) : [];
|
||||
const reader = packages.some(p => existsSync(join(components, 'engine', p, 'strata-vision')));
|
||||
const native = (await call('/api/native?refresh=1')).body;
|
||||
record('the engine component carries the picture reader and the host knows it (visionReader)', reader && native.visionReader === true, `component=${reader} host=${native.visionReader}`);
|
||||
const rows = (native.catalog?.families ?? []).flatMap(f => (f.sizes ?? []).map(s => ({ family: f.title ?? f.family, size: s.id, images: s.images, capable: s.imagesCapable })));
|
||||
console.log(' catalog (family / size / images / imagesCapable):');
|
||||
for (const r of rows) console.log(` ${String(r.family).padEnd(26)} ${String(r.size).padEnd(30)} images=${r.images} capable=${r.capable}`);
|
||||
record('some models in the catalog read pictures', rows.some(r => r.images === true), `${rows.filter(r => r.images).length} of ${rows.length}`);
|
||||
record('every picture-capable model reads pictures when the reader is installed', rows.every(r => r.capable === r.images), '');
|
||||
const flash = rows.filter(r => /flash-next/i.test(`${r.family} ${r.size}`) && !/gyro|qad/i.test(`${r.family} ${r.size}`));
|
||||
record('the Qwen3.8-Flash-Next models are marked images: true', flash.length > 0 && flash.every(r => r.images === true), flash.map(r => r.size + '=' + r.images).join(' '));
|
||||
|
||||
// ---- 2. API models
|
||||
if (args.vlm) {
|
||||
const smol = args['vlm-model'] ?? 'smolvlm';
|
||||
await saveApi('Small picture model', args.vlm, [{ id: smol, name: 'SmolVLM' }]);
|
||||
const found = await readsAfter('Small picture model', smol, true);
|
||||
record('the host works out that the llama-server reads pictures (no tick needed)', found.m?.reads === true, JSON.stringify(found.m));
|
||||
const conn = found.c;
|
||||
const sent = await chat({ remoteModel: { connectionId: conn?.id, modelId: smol } });
|
||||
console.log(` answer from the API model: ${JSON.stringify((sent.text ?? sent.message ?? '').slice(0, 300))}`);
|
||||
record('the picture goes to the API model and is answered', sent.status === 200 && !sent.error && (sent.text ?? '').trim().length > 0 && names((sent.text ?? '').toLowerCase()), `status=${sent.status} err=${sent.error ?? ''}`);
|
||||
|
||||
if (args['text-only']) {
|
||||
await saveApi('Text only server', args['text-only'], [{ id: 'plain', name: 'Plain model' }]);
|
||||
const t = await readsAfter('Text only server', 'plain', false);
|
||||
record('a server without a picture reader is known not to read pictures', t.m?.reads === false, JSON.stringify(t.m));
|
||||
const refused = await chat({ remoteModel: { connectionId: t.c.id, modelId: 'plain' } });
|
||||
console.log(` refusal: ${JSON.stringify(refused.message)}`);
|
||||
record('a picture for it is refused in plain words that say how to fix it', refused.status === 409 && /Read pictures with this model/.test(refused.message ?? '') && /Settings/.test(refused.message ?? ''), `${refused.status}`);
|
||||
const ticked = await saveApi('Text only server', args['text-only'], [{ id: 'plain', name: 'Plain model', vision: true }]);
|
||||
const after = (await call('/api/model-providers')).body.connections.find(x => x.name === 'Text only server');
|
||||
record('ticking "Read pictures with this model" is saved and makes the model read pictures', after?.models[0]?.vision === true && after?.models[0]?.reads === true, JSON.stringify(after?.models[0]));
|
||||
const through = await chat({ remoteModel: { connectionId: after.id, modelId: 'plain' } });
|
||||
record('with the tick the picture is sent on (the server itself decides what to do with it)', through.status === 200 && (through.error ?? '') !== '' ? /turn off/.test(through.error) : through.status === 200, `${through.status} ${through.message ?? through.error ?? ''}`);
|
||||
}
|
||||
|
||||
await saveApi('Small picture model', args.vlm, [{ id: smol, name: 'SmolVLM', vision: false }]);
|
||||
const off = await readsAfter('Small picture model', smol, false);
|
||||
const notRead = await chat({ remoteModel: { connectionId: off.c.id, modelId: smol } });
|
||||
record('a model switched OFF refuses pictures and says where to turn them on again', notRead.status === 409 && /not to read pictures/.test(notRead.message ?? '') && /Settings/.test(notRead.message ?? ''), `${notRead.status} ${notRead.message ?? ''}`);
|
||||
await saveApi('Small picture model', args.vlm, [{ id: smol, name: 'SmolVLM', vision: null }]);
|
||||
|
||||
// unknown is ON by default: a server whose list and /props say nothing is sent the picture
|
||||
if (args.silent) {
|
||||
await saveApi('Silent server', args.silent, [{ id: 'quiet', name: 'Quiet model' }]);
|
||||
const q = await readsAfter('Silent server', 'quiet', true, 8);
|
||||
record('a server that says nothing about pictures is ON by default', q.m?.reads === true && q.m?.vision === null, JSON.stringify(q.m));
|
||||
const sentQuiet = await chat({ remoteModel: { connectionId: q.c.id, modelId: 'quiet' } });
|
||||
record('and the picture is sent to it (not refused)', sentQuiet.status === 200, `${sentQuiet.status} ${sentQuiet.message ?? sentQuiet.error ?? ''}`);
|
||||
}
|
||||
await saveApi('Other style', 'https://api.anthropic.com/v1', [{ id: 'claude-x', name: 'Claude' }], 'anthropic');
|
||||
const other = (await call('/api/model-providers')).body.connections.find(x => x.name === 'Other style');
|
||||
const style = await chat({ remoteModel: { connectionId: other.id, modelId: 'claude-x' } });
|
||||
console.log(` other style: ${JSON.stringify(style.message)}`);
|
||||
record('an API of another style says plainly why pictures stay in the preview', style.status === 409 && /OpenAI/.test(style.message ?? ''), `${style.status}`);
|
||||
|
||||
}
|
||||
if (skipLocal) return;
|
||||
|
||||
// ---- 3. a local model
|
||||
const values = { family, model: modelId, context: contextSize, vision: 'yes', loadVision: device, ...(args.data ? { data_dir: resolve(args.data) } : {}) };
|
||||
const started = await call('/api/native/operations', 'POST', { kind: 'install', args: values });
|
||||
record('the install operation starts', started.status === 200 || started.status === 202, JSON.stringify(started.body).slice(0, 200));
|
||||
const done = await waitJob(started.body.id, 3600);
|
||||
record('Download and prepare finishes', done.job.state === 'done', `${done.job.state} ${done.job.error ?? done.job.summary}`);
|
||||
const refreshed = (await call('/api/native?refresh=1')).body;
|
||||
const model = refreshed.installed?.models?.find(m => [m.id, m.model, m.tag].some(x => String(x ?? '').toLowerCase() === want));
|
||||
record('the model is prepared with its picture reader', !!model?.visionPrepared, JSON.stringify({ ready: model?.ready, visionPrepared: model?.visionPrepared }));
|
||||
const load = await call('/api/native/operations', 'POST', { kind: 'load', args: { model: model?.model ?? want } });
|
||||
const loaded = await waitJob(load.body.id, 900);
|
||||
const instance = loaded.state.instances[0];
|
||||
record('the model loads with the picture reader', loaded.job.state === 'done' && instance?.status === 'ready' && instance?.vision === true, `${loaded.job.state} ${loaded.job.error ?? ''} vision=${instance?.vision}`);
|
||||
record('the model loaded with pictures ON without being asked per load (default)', instance?.vision === true, '');
|
||||
const t0 = Date.now();
|
||||
let local = { status: 0 };
|
||||
for (let i = 0; i < 2 && !(local.text ?? '').trim(); i++) local = await chat({ nativeModelId: instance?.id, maxTokens: 4096 });
|
||||
console.log(` answer from the local model (${Math.round((Date.now() - t0) / 1000)} s): ${JSON.stringify((local.text ?? local.message ?? '').slice(0, 300))}`);
|
||||
const low = (local.text ?? '').toLowerCase();
|
||||
record('the local model answers about the picture (names the shapes)', local.status === 200 && /red|circle/.test(low) && /blue|square/.test(low) && /green|triangle/.test(low), `status=${local.status} err=${local.error ?? ''}`);
|
||||
record('the local model reads STRATA 42', low.includes('42'));
|
||||
// the same picture through the agent (the Harness route must declare image input: the "does not declare image input" error of the tester)
|
||||
if (args.agent) { // needs a Harness beside the host (the installed layout has one; a bare test box does not)
|
||||
const t1 = Date.now(); let a = { status: 0 };
|
||||
for (let i = 0; i < 2 && !/red|circle|square|triangle/.test((a.text ?? '').toLowerCase()); i++) a = await chat({ nativeModelId: instance?.id, agent: true, sessionId: 'pics-agent-' + i, mode: 'plan', maxTokens: 4096 });
|
||||
console.log(` answer through the agent (${Math.round((Date.now() - t1) / 1000)} s): ${JSON.stringify((a.text ?? a.message ?? '').slice(0, 300))}`);
|
||||
record('the agent route reads the picture too', a.status === 200 && /red|circle|square|triangle/.test((a.text ?? '').toLowerCase()) && !/does not declare image input/.test(a.text ?? ''), `status=${a.status} err=${a.error ?? ''}`);
|
||||
}
|
||||
// OFF: loading with Read pictures off refuses a picture in plain words and says what to do
|
||||
await call(`/api/native/instances/${instance?.id}/unload`, 'POST', {});
|
||||
const loadOff = await call('/api/native/operations', 'POST', { kind: 'load', args: { model: model?.model ?? want, vision: 'no' } });
|
||||
const loadedOff = await waitJob(loadOff.body.id, 900);
|
||||
const off2 = loadedOff.state.instances[0];
|
||||
record('loaded with Read pictures OFF', loadedOff.job.state === 'done' && off2?.vision === false, `${loadedOff.job.state} vision=${off2?.vision}`);
|
||||
const refusedLocal = await chat({ nativeModelId: off2?.id, maxTokens: 64 });
|
||||
console.log(` refusal: ${JSON.stringify(refusedLocal.message)}`);
|
||||
record('a picture for the OFF model is refused with a next step', refusedLocal.status === 409 && /Read pictures/.test(refusedLocal.message ?? '') && /Models/.test(refusedLocal.message ?? ''), `${refusedLocal.status}`);
|
||||
const unloaded = await call(`/api/native/instances/${off2?.id}/unload`, 'POST', {});
|
||||
record('unload', unloaded.status === 200, unloaded.status);
|
||||
}
|
||||
try { await main(); } catch (e) { record('run', false, e.stack ?? e); }
|
||||
host.kill();
|
||||
await delay(1500);
|
||||
console.log(failed ? 'PICS-E2E FAIL' : 'PICS-E2E PASS');
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
# Put the bundled payloads back to the bytes they were staged with, inside a finished AppImage.
|
||||
#
|
||||
# apps/desktop/scripts/repack-appimage.sh <in.AppImage> <out.AppImage> [--resources <src-tauri/resources>] [--check]
|
||||
#
|
||||
# Why: Tauri hands the AppDir to linuxdeploy, which treats every ELF file under usr/lib as a library to fix up: it rewrites the RPATH of
|
||||
# the engine (strata, libstrata_kq.so, libcudart), of the bundled Python and Node and of the Harness's native modules. The files then no
|
||||
# longer match the SHA-256 and size their manifests list (engine-component.json), so the host refuses the bundled engine, and the
|
||||
# installer's own integrity promise ("what we staged is what runs") is broken. This script unpacks the AppImage, copies the staged
|
||||
# folders (control, harness, node, engine-bundle) over the AppDir byte for byte, and packs it again with the same runtime.
|
||||
#
|
||||
# --check does not write anything: it lists every engine-bundle file whose bytes differ from the manifest in the AppImage.
|
||||
set -euo pipefail
|
||||
|
||||
IN="$(realpath "${1:?in.AppImage}")"; OUT="${2:-}"
|
||||
RES="$(cd "$(dirname "${BASH_SOURCE[0]}")/../src-tauri/resources" && pwd)"
|
||||
CHECK=0
|
||||
shift 2 || true
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--resources) RES="$(cd "$2" && pwd)"; shift 2 ;;
|
||||
--check) CHECK=1; shift ;;
|
||||
*) echo "unknown option $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
OFFSET="$("$IN" --appimage-offset)"
|
||||
WORK="$(mktemp -d -t strata-repack.XXXXXX)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
unsquashfs -no-progress -o "$OFFSET" -d "$WORK/AppDir" "$IN" >/dev/null
|
||||
# the product folder under usr/lib (Tauri names it after productName)
|
||||
LIB="$(find "$WORK/AppDir/usr/lib" -maxdepth 2 -type d -name engine-bundle -printf '%h\n' | head -1)"
|
||||
[ -n "$LIB" ] || LIB="$(find "$WORK/AppDir/usr/lib" -maxdepth 2 -type d -name control -printf '%h\n' | head -1)"
|
||||
[ -n "$LIB" ] || { echo "no Strata resource folder in the AppImage" >&2; exit 1; }
|
||||
|
||||
if [ "$CHECK" = 1 ]; then
|
||||
bad=0
|
||||
while IFS= read -r f; do
|
||||
rel="${f#$RES/}"
|
||||
if ! cmp -s "$f" "$LIB/$rel"; then echo "differs: $rel"; bad=$((bad + 1)); fi
|
||||
done < <(find "$RES/engine-bundle" "$RES/node" -type f ! -name .gitignore)
|
||||
echo "$bad file(s) differ from what was staged"
|
||||
[ "$bad" = 0 ]
|
||||
exit
|
||||
fi
|
||||
|
||||
[ -n "$OUT" ] || { echo "give out.AppImage" >&2; exit 2; }
|
||||
for d in control harness node engine-bundle; do
|
||||
[ -d "$RES/$d" ] || continue
|
||||
rsync -aL --checksum --delete --exclude .gitignore "$RES/$d/" "$LIB/$d/"
|
||||
done
|
||||
# The launcher records the environment the AppImage was started from, before its own hooks add the AppImage's libraries and GTK/GIO
|
||||
# settings (LD_LIBRARY_PATH, GTK_*, XDG_DATA_DIRS ...). The host gives it back to every program that is not Strata's own (zenity,
|
||||
# kdialog, xdg-open, the assistant's shell): apps/host/server/external-env.mjs. Without it a system program loads the AppImage's libssl
|
||||
# and dies ("OPENSSL_3.2.0 not found"). The line goes right after `this_dir=...`, ahead of every hook.
|
||||
if ! grep -q STRATA_ORIGINAL_ENV "$WORK/AppDir/AppRun"; then
|
||||
sed -i '/^this_dir=/a # Strata: the environment this AppImage was started from (given back to programs that are not Strata'"'"'s own)\nexport STRATA_ORIGINAL_ENV="$(env -0 | base64 -w0)"' "$WORK/AppDir/AppRun"
|
||||
grep -q STRATA_ORIGINAL_ENV "$WORK/AppDir/AppRun" || { echo "could not add the environment record to AppRun" >&2; exit 1; }
|
||||
fi
|
||||
mksquashfs "$WORK/AppDir" "$WORK/app.squashfs" -root-owned -noappend -comp zstd -b 131072 -no-progress >/dev/null
|
||||
head -c "$OFFSET" "$IN" > "$OUT"
|
||||
cat "$WORK/app.squashfs" >> "$OUT"
|
||||
chmod +x "$OUT"
|
||||
# The checksum is computed here, from the finished file, every time: a README or a note must take it from this file
|
||||
# (apps/desktop/scripts/make-tester-readme.sh) and never from an older build.
|
||||
( cd "$(dirname "$OUT")" && sha256sum "$(basename "$OUT")" > "$(basename "$OUT").sha256" )
|
||||
echo "repacked: $OUT ($(stat -c %s "$OUT") bytes)"
|
||||
cat "$OUT.sha256"
|
||||
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stage the Control component the installer ships: src-tauri/resources/control.
|
||||
|
||||
python apps/desktop/scripts/stage-control.py # build the terminal bundle, stage it
|
||||
python apps/desktop/scripts/stage-control.py --archive X.zip # stage an archive you already built
|
||||
python apps/desktop/scripts/stage-control.py --wheelhouse DIR # build with no network (wheels from DIR)
|
||||
python apps/desktop/scripts/stage-control.py --check # verify what is staged
|
||||
|
||||
What lands in `control/`: the pinned CPython 3.12.15 (python-build-standalone, digest-checked), the hash-locked wheels
|
||||
(`packaging/locks/runtime-<target>-cp312.txt`, installed with --require-hashes at build time), strata_control,
|
||||
strata_cli, serve, tools/strata_tokenizer.py + gguf_reader.py, and BUNDLE.json (version, interpreter and lock digests,
|
||||
the digest of the archive it came from). It is the same bundle as the Terminal archive (packaging/package_terminal.py):
|
||||
one Python runtime-core for both products. The app then never runs pip, npm, git or a compiler to start or serve.
|
||||
|
||||
This script is a BUILD step. It may download the interpreter and the wheels (verified); the app never does.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[2]
|
||||
OUT = HERE.parent / "src-tauri" / "resources" / "control"
|
||||
sys.path.insert(0, str(ROOT / "packaging"))
|
||||
import package_terminal as pt # noqa: E402
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1 << 20), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def build_archive(work: Path, wheelhouse: str | None) -> Path:
|
||||
command = [sys.executable, str(ROOT / "packaging" / "package_terminal.py"), "--out", str(work)]
|
||||
if wheelhouse:
|
||||
command += ["--wheelhouse", wheelhouse]
|
||||
subprocess.run(command, check=True, cwd=str(ROOT))
|
||||
archives = sorted(work.glob("strata-terminal-*.zip")) + sorted(work.glob("strata-terminal-*.tar.gz"))
|
||||
if len(archives) != 1:
|
||||
raise SystemExit(f"expected one archive in {work}, found {archives}")
|
||||
return archives[0]
|
||||
|
||||
|
||||
def extract(archive: Path, destination: Path) -> Path:
|
||||
"""Unpack the archive; returns the folder that holds python/ and strata.py."""
|
||||
scratch = destination.parent / (destination.name + ".incoming")
|
||||
shutil.rmtree(scratch, ignore_errors=True)
|
||||
scratch.mkdir(parents=True)
|
||||
if archive.name.endswith(".zip"):
|
||||
with zipfile.ZipFile(archive) as zf:
|
||||
for member in zf.namelist():
|
||||
target = (scratch / member).resolve()
|
||||
if scratch.resolve() not in target.parents and target != scratch.resolve():
|
||||
raise SystemExit(f"unsafe member in {archive.name}: {member}")
|
||||
zf.extractall(scratch)
|
||||
else:
|
||||
with tarfile.open(archive) as tf:
|
||||
tf.extractall(scratch, filter="data")
|
||||
(inner,) = [p for p in scratch.iterdir() if p.is_dir()]
|
||||
return inner
|
||||
|
||||
|
||||
#: What the app's installer needs on top of the Terminal bundle: the model catalog, GPU probe and install job. The list and the copy
|
||||
#: live in packaging/package_terminal.py (the Terminal archive carries the same files, for `strata launch`).
|
||||
INSTALLER_FILES, INSTALLER_TOOLS, INSTALLER_DATA = pt.INSTALLER_FILES, pt.INSTALLER_TOOLS, pt.INSTALLER_DATA
|
||||
local_imports = pt.local_imports
|
||||
|
||||
|
||||
def add_installer(folder: Path) -> dict:
|
||||
"""Copy the installer's files from the source tree into the staged Control (after the Terminal bundle is in place)."""
|
||||
return pt.add_installer(folder, ROOT)
|
||||
|
||||
|
||||
#: What the other names of the interpreter become: Tauri copies a symlink as a full copy of its target, so `python`, `python3` and
|
||||
#: `python3.12` in a bundle would be three real files (68 MB each on Linux). Only `python3.12` stays the real one.
|
||||
#: The launcher runs inside the AppImage's environment (LD_LIBRARY_PATH holds the bundled libraries), where an external program such as
|
||||
#: `dirname` can fail to start (Ubuntu 26.04's coreutils: "libsystemd.so.0: version LIBSYSTEMD_254 not found"), so it uses shell built-ins
|
||||
#: only. The first version called `dirname`; that left the folder empty and "Control stopped" on such a computer.
|
||||
LAUNCHER = """#!/bin/sh
|
||||
# Strata: this name only starts the real interpreter beside it (the bundle keeps one copy, not one per name). Built-ins only: no dirname.
|
||||
case "$0" in */*) dir=${0%%/*} ;; *) dir=. ;; esac
|
||||
here=$(cd "$dir" && pwd)
|
||||
exec "$here/%s" "$@"
|
||||
"""
|
||||
|
||||
|
||||
def dedupe_python(folder: Path) -> list[str]:
|
||||
"""Make every other name of the interpreter in `python/bin` a small launcher of the real binary (Linux and macOS; Windows has one
|
||||
python.exe). A symlink would not survive the bundling, and a plain copy is what costs the space. Returns the names changed."""
|
||||
bindir = folder / "python" / "bin"
|
||||
if not bindir.is_dir():
|
||||
return []
|
||||
real = bindir / f"python{pt.PYTHON_VERSION.rsplit('.', 1)[0]}"
|
||||
if not real.exists():
|
||||
return []
|
||||
if real.is_symlink(): # the real file is a link itself: make the target the real one
|
||||
target = real.resolve()
|
||||
real.unlink()
|
||||
shutil.move(str(target), str(real))
|
||||
changed = []
|
||||
for name in ("python", "python3"):
|
||||
path = bindir / name
|
||||
if not (path.is_symlink() or (path.is_file() and path.stat().st_size == real.stat().st_size)):
|
||||
continue
|
||||
path.unlink()
|
||||
path.write_bytes((LAUNCHER % real.name).encode("utf-8"))
|
||||
path.chmod(0o755)
|
||||
changed.append(name)
|
||||
return changed
|
||||
|
||||
|
||||
|
||||
def write_bundle_record(folder: Path, archive: Path) -> dict:
|
||||
manifest_path = archive.with_name(archive.name.rsplit(".", 2 if archive.name.endswith(".tar.gz") else 1)[0] + ".manifest.json")
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8")) if manifest_path.exists() else {}
|
||||
target = manifest.get("target") or pt.host_target()
|
||||
record = {
|
||||
"schema_version": 1, "component": "strata-control",
|
||||
"version": (folder / "VERSION").read_text(encoding="utf-8").strip(),
|
||||
"target": target,
|
||||
"python": {"version": pt.PYTHON_VERSION, "build": pt.PBS_RELEASE, "sha256": pt.PYTHON_PINS[target]["sha256"]},
|
||||
"lock": manifest.get("lock"),
|
||||
"installed": manifest.get("installed"),
|
||||
"archive": {"name": archive.name, "sha256": sha256(archive), "bytes": archive.stat().st_size},
|
||||
"acquires_at_runtime": False,
|
||||
"installer": add_installer(folder),
|
||||
}
|
||||
(folder / "BUNDLE.json").write_text(json.dumps(record, indent=1) + "\n", encoding="utf-8")
|
||||
return record
|
||||
|
||||
|
||||
def check(folder: Path) -> int:
|
||||
target = pt.host_target()
|
||||
python = folder / pt.PYTHON_PINS[target]["python"]
|
||||
problems = []
|
||||
for need in (python, folder / "strata.py", folder / "strata_control", folder / "serve" / "server.py", folder / "BUNDLE.json",
|
||||
folder / "setup.py", folder / "tools" / "strata_mcp.py", folder / "tools" / "iq_pack.py", folder / "data" / "draft_vocab.bin"):
|
||||
if not need.exists():
|
||||
problems.append(f"missing {need}")
|
||||
if problems:
|
||||
print("\n".join(problems), file=sys.stderr)
|
||||
return 1
|
||||
env = {"PYTHONNOUSERSITE": "1", "PYTHONDONTWRITEBYTECODE": "1", "STRATA_NO_ACQUIRE": "1", "SYSTEMROOT": __import__("os").environ.get("SYSTEMROOT", "")}
|
||||
done = subprocess.run([str(python), str(folder / "strata.py"), "--version"], capture_output=True, text=True, env=env,
|
||||
cwd=str(folder), timeout=60)
|
||||
print(done.stdout.strip() or done.stderr.strip())
|
||||
if done.returncode:
|
||||
return done.returncode
|
||||
# the installer: its catalog, a GPU probe and its imports work on the bundled Python alone (nothing is installed or fetched)
|
||||
probe = ("import sys; sys.path.insert(0, %r); import importlib.util as u; s = u.spec_from_file_location('s', %r); m = u.module_from_spec(s); "
|
||||
"s.loader.exec_module(m); import iq_pack, strata_pack, pack_index, mtp_fetch; "
|
||||
"print('installer ok: %%d models, %%d families' %% (len(m.MODELS), len(m.FAMILIES)))") % (str(folder / "tools"), str(folder / "setup.py"))
|
||||
env["STRATA_CONTROL_INSTALL"] = "1"
|
||||
again = subprocess.run([str(python), "-c", probe], capture_output=True, text=True, env=env, cwd=str(folder), timeout=120)
|
||||
print(again.stdout.strip() or again.stderr.strip()[-600:])
|
||||
return again.returncode
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--archive", type=Path, help="stage this terminal archive instead of building one")
|
||||
ap.add_argument("--wheelhouse", help="build with the wheels in this folder (no network)")
|
||||
ap.add_argument("--out", type=Path, default=OUT)
|
||||
ap.add_argument("--check", action="store_true", help="verify the staged component and exit")
|
||||
a = ap.parse_args()
|
||||
if a.check:
|
||||
return check(a.out)
|
||||
with tempfile.TemporaryDirectory(prefix="strata-stage-control-") as work:
|
||||
archive = a.archive.resolve() if a.archive else build_archive(Path(work), a.wheelhouse)
|
||||
keep = [p for p in a.out.iterdir() if p.name == ".gitignore"] if a.out.exists() else []
|
||||
if a.out.exists():
|
||||
for child in a.out.iterdir():
|
||||
if child.name != ".gitignore":
|
||||
shutil.rmtree(child, ignore_errors=True) if child.is_dir() else child.unlink()
|
||||
a.out.mkdir(parents=True, exist_ok=True)
|
||||
inner = extract(archive, a.out)
|
||||
record = write_bundle_record(inner, archive)
|
||||
for child in inner.iterdir():
|
||||
shutil.move(str(child), str(a.out / child.name))
|
||||
shutil.rmtree(inner.parent, ignore_errors=True)
|
||||
launched = dedupe_python(a.out)
|
||||
if launched:
|
||||
print(f"python: {', '.join(launched)} are launchers of the one real interpreter")
|
||||
print(f"staged {record['component']} {record['version']} ({record['target']}) into {a.out}")
|
||||
return check(a.out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Stage the DeepSeek Harness (pinned, MIT) as the installer's `harness/` folder: a pruned PRODUCTION install, built at
|
||||
* build time, so nothing is fetched or compiled on the person's PC.
|
||||
*
|
||||
* node apps/desktop/scripts/stage-harness.mjs --src <built Harness checkout> [--out apps/desktop/src-tauri/resources/harness]
|
||||
*
|
||||
* <built Harness checkout> is a clone of the pinned commit after `pnpm install --frozen-lockfile` and `pnpm run build:artifacts`
|
||||
* (see phaseA-tests/notes-split-s4a.md, step 1). The script never touches it: it runs `pnpm deploy --legacy --prod` into
|
||||
* the output folder, removes what a running Harness does not read (sources, tests, maps, docs, other operating systems'
|
||||
* prebuilt binaries, the LibreOffice document converter and the speech engine, which the app does not use), keeps the
|
||||
* MIT licence and third-party notices, writes BUNDLE.json, and finally boots the staged copy once as proof.
|
||||
*
|
||||
* The host finds the result at `<install>/harness` (server/agent/harness-runtime.mjs), or in the component folder.
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
||||
import { join, relative, resolve, sep } from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { flattenPnpm } from './flatten-pnpm.mjs';
|
||||
import { isOtherOsBinaryFolder, missingHarnessImports } from './harness-prune.mjs';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const opt = (name, fallback) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : fallback; };
|
||||
const src = resolve(opt('src', process.env.STRATA_HARNESS_SRC ?? ''));
|
||||
const out = resolve(opt('out', join(import.meta.dirname, '..', 'src-tauri', 'resources', 'harness')));
|
||||
if (!opt('src', process.env.STRATA_HARNESS_SRC) || !existsSync(join(src, 'apps', 'cli', 'lib', 'bin.js'))) {
|
||||
console.error('Give --src <a built Harness checkout> (apps/cli/lib/bin.js must exist).');
|
||||
process.exit(2);
|
||||
}
|
||||
const platform = process.platform;
|
||||
const run = (cmd, argv, cwd, extra = {}) => {
|
||||
const r = spawnSync(cmd, argv, { cwd, stdio: 'inherit', shell: false, ...extra });
|
||||
if (r.status !== 0) { console.error(`${cmd} ${argv.join(' ')} failed (${r.status ?? r.error})`); process.exit(1); }
|
||||
};
|
||||
|
||||
// The output folder in the repo carries a .gitignore that keeps the staged files out of git: keep it across the clean-up.
|
||||
const keepIgnore = existsSync(join(out, '.gitignore')) ? readFileSync(join(out, '.gitignore')) : null;
|
||||
rmSync(out, { recursive: true, force: true });
|
||||
mkdirSync(out, { recursive: true });
|
||||
const pnpm = platform === 'win32' ? ['corepack.cmd', ['pnpm']] : ['corepack', ['pnpm']];
|
||||
run(pnpm[0], [...pnpm[1], 'deploy', '--legacy', '--prod', '--filter', '@deepseek-ai/dsh', out], src, { shell: platform === 'win32' });
|
||||
|
||||
// --- peers -------------------------------------------------------------------------------------------------------------
|
||||
// `pnpm deploy --prod` leaves out peer dependencies, but the Harness loads some of them by name at run time (for example the
|
||||
// plugin-group helper that app-boot imports). Walk the installed packages; every dependency or peer that cannot be
|
||||
// resolved from the output is copied from the source checkout's own node_modules, and its own requirements after it.
|
||||
function packageJsonsUnder(dir, out2 = []) {
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = join(dir, e.name);
|
||||
if (e.isSymbolicLink()) continue;
|
||||
if (e.isDirectory()) packageJsonsUnder(p, out2);
|
||||
else if (e.name === 'package.json' && p.includes(`${sep}node_modules${sep}`)) out2.push(p);
|
||||
}
|
||||
return out2;
|
||||
}
|
||||
function requirementsOf(file) {
|
||||
try { const j = JSON.parse(readFileSync(file, 'utf8')); return [...Object.keys(j.dependencies ?? {}), ...Object.keys(j.peerDependencies ?? {}), ...Object.keys(j.optionalDependencies ?? {})]; } catch { return []; }
|
||||
}
|
||||
/** The checkout's own workspace packages (packages/**, vendor/*, apps/*) by name: where a peer that is one of ours comes from. */
|
||||
const workspace = new Map();
|
||||
function indexWorkspace(dir, depth = 0) {
|
||||
if (depth > 4) return;
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
if (!e.isDirectory() || e.isSymbolicLink() || e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
||||
const p = join(dir, e.name);
|
||||
const manifest = join(p, 'package.json');
|
||||
if (existsSync(manifest)) { try { workspace.set(JSON.parse(readFileSync(manifest, 'utf8')).name, p); } catch { /* not a package */ } }
|
||||
indexWorkspace(p, depth + 1);
|
||||
}
|
||||
}
|
||||
for (const top of ['packages', 'vendor', 'apps']) if (existsSync(join(src, top))) indexWorkspace(join(src, top));
|
||||
const resolvable = name => existsSync(join(out, 'node_modules', ...name.split('/')));
|
||||
const queue = [...new Set(packageJsonsUnder(join(out, 'node_modules')).flatMap(requirementsOf))].filter(n => !resolvable(n));
|
||||
const added = [];
|
||||
while (queue.length) {
|
||||
const name = queue.shift();
|
||||
if (resolvable(name)) continue;
|
||||
const from = workspace.get(name) ?? join(src, 'node_modules', ...name.split('/'));
|
||||
if (!existsSync(from)) continue; // an optional package for another platform, or not installed in the source: nothing to copy
|
||||
const to = join(out, 'node_modules', ...name.split('/'));
|
||||
mkdirSync(join(to, '..'), { recursive: true });
|
||||
const realFrom = realpathSync(from);
|
||||
cpSync(realFrom, to, { recursive: true, dereference: true, filter: f => !relative(realFrom, f).split(sep).includes('node_modules') });
|
||||
added.push(name);
|
||||
queue.push(...requirementsOf(join(to, 'package.json')).filter(n => !resolvable(n)));
|
||||
}
|
||||
if (added.length) console.log(`peer packages added: ${added.join(', ')}`);
|
||||
|
||||
// --- the browser tool server ---------------------------------------------------------------------------------------------
|
||||
// Settings -> Tools can switch on a browser the assistant drives (Playwright's MCP server, Apache-2.0) on the person's own Chrome or Edge,
|
||||
// so no browser is downloaded. The server and the two Playwright libraries it needs are not part of the Harness's production closure;
|
||||
// they are copied from the source checkout's package store (~20 MB) and the host starts it with Node (server/agent/browser-probe.mjs).
|
||||
const pnpmStore = join(src, 'node_modules', '.pnpm');
|
||||
function storePackage(name, version) {
|
||||
if (!existsSync(pnpmStore)) return null;
|
||||
const prefix = `${name.replace('/', '+')}@`;
|
||||
const hit = readdirSync(pnpmStore).filter(n => n.startsWith(prefix) && (!version || n === `${prefix}${version}` || n.startsWith(`${prefix}${version}_`))).sort().at(-1);
|
||||
const dir = hit ? join(pnpmStore, hit, 'node_modules', ...name.split('/')) : null;
|
||||
return dir && existsSync(dir) ? dir : null;
|
||||
}
|
||||
const browserDir = storePackage('@playwright/mcp');
|
||||
if (browserDir) {
|
||||
const wanted = JSON.parse(readFileSync(join(browserDir, 'package.json'), 'utf8')).dependencies ?? {};
|
||||
const parts = [['@playwright/mcp', browserDir], ...['playwright', 'playwright-core'].map(n => [n, storePackage(n, wanted[n])])];
|
||||
if (parts.every(([, dir]) => dir)) {
|
||||
for (const [name, dir] of parts) {
|
||||
const to = join(out, 'node_modules', ...name.split('/'));
|
||||
mkdirSync(join(to, '..'), { recursive: true });
|
||||
cpSync(realpathSync(dir), to, { recursive: true, dereference: true, filter: f => !relative(dir, f).split(sep).includes('node_modules') });
|
||||
}
|
||||
console.log(`browser tool server added: ${parts.map(([n]) => n).join(', ')}`);
|
||||
} else console.log('browser tool server NOT added: Playwright libraries missing from the package store');
|
||||
} else console.log('browser tool server NOT added: @playwright/mcp is not in the source checkout');
|
||||
|
||||
// --- one real folder per package, no links -------------------------------------------------------------------------------
|
||||
// The installer copies files; pnpm's layout is links. See flatten-pnpm.mjs. The two packages the app does not use are never copied.
|
||||
const flat = flattenPnpm(out, { skip: /libreoffice-kit|sherpa-onnx/i });
|
||||
console.log(`flattened: ${flat.packages} placed (${flat.nested} nested for a second version)`);
|
||||
|
||||
// --- prune ------------------------------------------------------------------------------------------------------------
|
||||
/** Whole packages the app never loads: office-document conversion (hundreds of MB) and speech input. */
|
||||
const DROP_PACKAGES = [/libreoffice-kit/i, /sherpa-onnx/i];
|
||||
/** Only when sitting directly in a package's root (a library may have a runtime folder called `doc`, e.g. yaml/dist/doc). */
|
||||
const DROP_DIRS = new Set(['test', 'tests', '__tests__', 'docs', 'website', 'benchmarks', 'examples', '.github']);
|
||||
const DROP_FILES = /\.(map|md|markdown|pdb|tsbuildinfo)$/i;
|
||||
/** Licences and notices travel with the files they cover. */
|
||||
const KEEP_FILES = /^(LICENSE|LICENCE|NOTICE|THIRD_PARTY)/i;
|
||||
let removed = 0;
|
||||
function prune(dir) {
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
const p = join(dir, e.name);
|
||||
if (e.isSymbolicLink()) continue;
|
||||
if (e.isDirectory()) {
|
||||
const inLib = p.split(sep).includes('lib');
|
||||
if ((DROP_DIRS.has(e.name) && !inLib && existsSync(join(dir, 'package.json'))) || DROP_PACKAGES.some(r => r.test(e.name)) || isOtherOsBinaryFolder(platform, e.name, p)) {
|
||||
rmSync(p, { recursive: true, force: true }); removed++; continue;
|
||||
}
|
||||
prune(p);
|
||||
} else if (DROP_FILES.test(e.name) && !KEEP_FILES.test(e.name)) {
|
||||
rmSync(p, { force: true }); removed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
prune(out);
|
||||
|
||||
// --- licence: the MIT text and the third-party notices travel with the files ---------------------------------------------
|
||||
for (const name of ['LICENSE', 'THIRD_PARTY_NOTICES.md']) {
|
||||
if (existsSync(join(src, name))) cpSync(join(src, name), join(out, name));
|
||||
}
|
||||
const cli = JSON.parse(readFileSync(join(out, 'package.json'), 'utf8'));
|
||||
let commit = process.env.STRATA_HARNESS_COMMIT ?? null;
|
||||
if (!commit) { const r = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: src, encoding: 'utf8' }); if (r.status === 0) commit = r.stdout.trim(); }
|
||||
function size(dir) { let t = 0, n = 0; for (const e of readdirSync(dir, { withFileTypes: true })) { const p = join(dir, e.name); if (e.isDirectory()) { const s = size(p); t += s.bytes; n += s.files; } else if (e.isFile()) { t += statSync(p).size; n++; } } return { bytes: t, files: n }; }
|
||||
const total = size(out);
|
||||
writeFileSync(join(out, 'BUNDLE.json'), JSON.stringify({
|
||||
component: 'deepseek-harness', version: cli.version, commit, license: 'MIT', platform, node: process.version,
|
||||
bytes: total.bytes, files: total.files, entry: 'apps/cli/lib/bin.js', builtAt: new Date().toISOString(),
|
||||
note: 'Pruned production install (pnpm deploy --prod). No package manager runs on the user\'s PC.',
|
||||
}, null, 2));
|
||||
|
||||
if (keepIgnore) writeFileSync(join(out, '.gitignore'), keepIgnore);
|
||||
|
||||
// --- proof 1: nothing the Harness imports by name was pruned ------------------------------------------------------------
|
||||
// (2026-10-17: the other-OS rule once removed @deepseek-ai/dsh-win32-process, which every platform imports; Linux/Mac agent chats died.)
|
||||
const lost = missingHarnessImports(out);
|
||||
if (lost.length) { console.error(['staged Harness is missing imported packages:', ...lost.map(l => ` ${l.from} -> ${l.missing}`)].join(String.fromCharCode(10))); process.exit(1); }
|
||||
console.log('import check: every @deepseek-ai package the Harness imports is staged');
|
||||
|
||||
// --- proof 2: the staged copy answers --version and boots the web profile once ----------------------------------------
|
||||
const bin = join(out, 'apps', 'cli', 'lib', 'bin.js');
|
||||
const entry = existsSync(bin) ? bin : join(out, 'lib', 'bin.js');
|
||||
const v = spawnSync(process.execPath, [entry, '--version'], { encoding: 'utf8' });
|
||||
if (v.status !== 0) { console.error('staged Harness does not start:', v.stderr); process.exit(1); }
|
||||
console.log(`staged ${cli.version} (${commit?.slice(0, 8) ?? 'no commit'}) in ${out}: ${(total.bytes / 1048576).toFixed(0)} MiB, ${total.files} files, ${removed} pruned items; --version -> ${v.stdout.trim()}`);
|
||||
console.log(`sha256(BUNDLE.json)=${createHash('sha256').update(readFileSync(join(out, 'BUNDLE.json'))).digest('hex')}`);
|
||||
@@ -0,0 +1,721 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Headless UI flows of the installed app against a packaged host with a FAKE installer and a FAKE engine (nothing is
|
||||
* downloaded, no real engine runs; the real Control starts the fake engine through the owner runtime):
|
||||
*
|
||||
* node apps/desktop/scripts/ui-flows.mjs [--control <staged control>] [--node <node.exe>] [--shots <dir>] [--only download,models,tune,fail,theme]
|
||||
*
|
||||
* download the onboarding download: ONE combined bar over both files, an ETA in minutes left of Stop, Finish later closes the
|
||||
* pop-up with the download still running, the Models screen shows the same progress, Stop stops it
|
||||
* models an installed model is the one Models selects and loads, without a download pop-up
|
||||
* tune the tuning step offers no picture reading, tunes, writes a RAM budget for "leave 20%", opens chat; reading size
|
||||
* fail an engine that stops while loading: the error is in words and the pop-up does not scroll
|
||||
* theme the app follows the system light/dark setting
|
||||
* Per-user folders are all under a scratch folder. Exit 1 if a check fails.
|
||||
*/
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, rmdirSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||
import { createServer } from 'node:net';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]?.startsWith('--') ? true : all[i + 1]]] : acc), []));
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const repoHost = resolve(here, '../../host');
|
||||
const staged = resolve(here, '../src-tauri/resources');
|
||||
const stagedControl = resolve(args.control ?? join(staged, 'control'));
|
||||
const nodeExe = resolve(args.node ?? join(staged, 'node', process.platform === 'win32' ? 'node.exe' : 'node'));
|
||||
const work = mkdtempSync(join(tmpdir(), 'strata-ui-flows-'));
|
||||
const shots = resolve(args.shots ?? join(work, 'shots'));
|
||||
mkdirSync(shots, { recursive: true });
|
||||
const only = new Set(String(args.only ?? 'download,models,profile,ui2,ui2d,tune,fail,delete,theme,restart,perf,polish,stop,latest').split(','));
|
||||
const isWin = process.platform === 'win32';
|
||||
const { chromium } = await import(pathToFileURL(join(repoHost, 'node_modules/playwright-core/index.mjs')).href).catch(() => import('playwright-core'));
|
||||
const CHROME = process.env.CHROME ?? ['C:/Program Files/Google/Chrome/Application/chrome.exe', '/usr/bin/google-chrome'].find(existsSync);
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
const sha = buf => createHash('sha256').update(buf).digest('hex');
|
||||
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
|
||||
let failed = false;
|
||||
const check = (ok, label, detail = '') => { console.log(`${ok ? 'ok ' : 'FAIL'} ${label}${detail ? ' ' + detail : ''}`); if (!ok) failed = true; };
|
||||
|
||||
// ------------------------------------------------------------------ the app folder: the real Control with a fake installer
|
||||
const control = join(work, 'control');
|
||||
mkdirSync(control, { recursive: true });
|
||||
for (const name of ['python', 'strata_control', 'strata_cli', 'serve']) symlinkSync(join(stagedControl, name), join(control, name), 'junction');
|
||||
for (const name of ['strata.py', 'VERSION', 'BUNDLE.json', 'requirements.txt']) copyFileSync(join(stagedControl, name), join(control, name));
|
||||
cpSync(join(stagedControl, 'tools'), join(control, 'tools'), { recursive: true });
|
||||
const python = isWin ? join(control, 'python', 'python.exe') : join(control, 'python', 'bin', 'python3');
|
||||
|
||||
const FAKE_INSTALLER = String.raw`
|
||||
import json, os, sys, time
|
||||
from pathlib import Path
|
||||
ROOT = Path(sys.argv[sys.argv.index('--root') + 1]) if '--root' in sys.argv else Path('.')
|
||||
STATE = Path(os.environ['FAKE_STATE']); STATE.mkdir(parents=True, exist_ok=True)
|
||||
RATE = float(os.environ.get('FAKE_RATE_GB_S', '0.5'))
|
||||
def size(model, gb, ram, mid):
|
||||
return {'model': model, 'about': model, 'download_gb': gb, 'ram_needed_gb': ram, 'experts_gb': ram - 8, 'on_this_pc': 'fits', 'installed': False, 'images': True, 'experimental': False, 'id': mid}
|
||||
CATALOG = {'summary': 'fake', 'families': [{'family': 'qwen', 'title': 'Qwen3.8-Flash-Next', 'about': 'x', 'experimental': False, 'images': True, 'sizes': [
|
||||
size('Q2_0', 66.4, 48, 'q2_0'), size('IQ3_XXS', 75.8, 60, 'iq3_xxs'), size('IQ3_S', 83.6, 62, 'iq3_s')]}],
|
||||
'contexts': [8192, 32768, 65536], 'recommendation': {'family': 'qwen', 'model': 'IQ3_XXS', 'title': 'Qwen3.8-Flash-Next IQ3_XXS', 'context': 32768}, 'context_note': ''}
|
||||
HARDWARE = {'ram_gb': 63.1, 'cpu': {'name': 'cpu', 'cores': 12}, 'gpus': [{'index': 0, 'name': 'NVIDIA GeForce RTX 5070', 'vram_gb': 11.9, 'usable': True}]}
|
||||
TOTAL = 83.6
|
||||
def job():
|
||||
f = STATE / 'job.json'
|
||||
if not f.exists(): return None
|
||||
j = json.loads(f.read_text())
|
||||
if j.get('cancelled'):
|
||||
return {'running': False, 'exit_code': 1, 'error': 'stopped', 'result': 'failed'}
|
||||
done = min(TOTAL, RATE * (time.time() - j['start']))
|
||||
return {'running': True, 'step': '5/7: downloading Qwen3.8-Flash-Next IQ3_S', 'model': 'IQ3_S',
|
||||
'download': {'file': 'model files', 'done_gb': round(done, 3), 'total_gb': TOTAL, 'remaining_gb': round(TOTAL - done, 3), 'percent': int(100 * done / TOTAL)}}
|
||||
def call(name, a):
|
||||
if name == 'strata_models': return CATALOG
|
||||
if name == 'strata_remove':
|
||||
cfg = ROOT / ('strata-%s.json' % a['model'])
|
||||
if a.get('confirm') and cfg.exists(): cfg.unlink()
|
||||
return {'summary': 'Deleted', 'plan': {'gb': 83.6}, 'failed': []}
|
||||
if name == 'strata_status': return {'summary': 'fake', 'installed': {'models': [{'model': 'iq3_s', 'disk_gb': 83.6, 'disk_bytes': 83600000000}]}, 'hardware': HARDWARE, 'recommendation': CATALOG['recommendation'], 'install': job(), 'running': []}
|
||||
if name == 'strata_install':
|
||||
if a.get('cancel'):
|
||||
f = STATE / 'job.json'
|
||||
if f.exists(): j = json.loads(f.read_text()); j['cancelled'] = True; f.write_text(json.dumps(j))
|
||||
return {'summary': 'stopped', 'install': job()}
|
||||
if a.get('confirm'):
|
||||
(STATE / 'job.json').write_text(json.dumps({'start': time.time()}))
|
||||
return {'summary': 'started', 'plan': {'download_gb': TOTAL}, 'install': job()}
|
||||
if not a: return {'summary': 'progress', 'install': job()}
|
||||
return {'summary': 'Plan', 'plan': {'title': 'Qwen3.8-Flash-Next IQ3_S', 'download_gb': TOTAL, 'disk_short': None}}
|
||||
raise Exception('unknown tool ' + name)
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if not line: continue
|
||||
m = json.loads(line)
|
||||
if 'id' not in m: continue
|
||||
method = m.get('method')
|
||||
if method == 'initialize': res = {'protocolVersion': '2024-11-05'}
|
||||
elif method == 'tools/list': res = {'tools': [{'name': n} for n in ('strata_models', 'strata_status', 'strata_install', 'strata_remove')]}
|
||||
else:
|
||||
p = m['params']
|
||||
res = {'structuredContent': call(p['name'], p.get('arguments') or {})}
|
||||
sys.stdout.write(json.dumps({'jsonrpc': '2.0', 'id': m['id'], 'result': res}) + '\n'); sys.stdout.flush()
|
||||
`;
|
||||
writeFileSync(join(control, 'tools', 'strata_mcp.py'), FAKE_INSTALLER);
|
||||
|
||||
const FAKE_ENGINE = `import os, sys, json, time
|
||||
from pathlib import Path
|
||||
if os.environ.get('FAKE_ENGINE_FAIL'):
|
||||
print('memory guard: 5.1 GiB of RAM free is under the 6 GiB limit (limit 63.1 GiB)', flush=True)
|
||||
sys.exit(3)
|
||||
ctx = '4096'
|
||||
if '--max-context' in sys.argv: ctx = sys.argv[sys.argv.index('--max-context') + 1]
|
||||
print('READY %s stop' % ctx, flush=True)
|
||||
for line in sys.stdin:
|
||||
if line.strip() == 'QUIT': break
|
||||
if line.startswith(('GEN ', 'BGEN ')):
|
||||
slot = int(line.split()[1]) if line.startswith('BGEN ') else None
|
||||
t0 = time.time(); n = 0
|
||||
for ch in os.environ.get('FAKE_REPLY', 'a'):
|
||||
print('T %d' % ord(ch), flush=True); n += 1
|
||||
if os.environ.get('FAKE_TOKEN_DELAY'): time.sleep(float(os.environ['FAKE_TOKEN_DELAY']))
|
||||
print('DONE %d 12 300.0 %.1f length 0 0 0' % (n, max(1.0, (time.time() - t0) * 1000)), flush=True)
|
||||
if slot is not None: print(f'BADM {slot} 0', flush=True)
|
||||
`;
|
||||
|
||||
// one scenario = one fresh host with its own per-user folders
|
||||
async function scenario(name, { installed = false, env = {} } = {}, body) {
|
||||
if (!only.has(name)) return;
|
||||
console.log(`--- ${name}`);
|
||||
const dir = join(work, name);
|
||||
const dirs = { home: join(dir, 'home'), appdata: join(dir, 'appdata'), local: join(dir, 'local'), components: join(dir, 'local', 'Strata', 'components'), install: join(dir, 'local', 'Strata', 'install'), state: join(dir, 'fake-state'), profiles: join(dir, 'profiles'), runtime: join(dir, 'runtime') };
|
||||
for (const d of [dirs.home, dirs.appdata, dirs.local, dirs.components, dirs.install, dirs.state, join(dir, 'models')]) mkdirSync(d, { recursive: true });
|
||||
const baseEnv = { ...process.env, STRATA_COMPONENTS_DIR: dirs.components, STRATA_PROFILE_DIR: dirs.profiles, STRATA_RUNTIME_DIR: dirs.runtime, STRATA_HOME: dirs.home, STRATA_MODELS_DIR: join(dir, 'models'), APPDATA: dirs.appdata, LOCALAPPDATA: dirs.local, USERPROFILE: dirs.home, XDG_CONFIG_HOME: join(dir, 'xdg'), XDG_DATA_HOME: join(dir, 'xdgdata'), PYTHONDONTWRITEBYTECODE: '1', STRATA_ENGINE_BACKEND: 'cuda', FAKE_STATE: dirs.state, ...env };
|
||||
delete baseEnv.STRATA_API_KEY; delete baseEnv.STRATA_INSTALL_DIR;
|
||||
if (installed) {
|
||||
const platform = isWin ? 'windows-x86_64' : 'linux-x86_64';
|
||||
const folder = join(dirs.components, 'engine', `0.1.45-${platform}-cuda`);
|
||||
mkdirSync(folder, { recursive: true });
|
||||
writeFileSync(join(folder, 'engine_main.py'), FAKE_ENGINE);
|
||||
const entry = isWin ? 'engine.cmd' : 'engine';
|
||||
writeFileSync(join(folder, entry), isWin ? `@echo off\r\n"${python}" "%~dp0engine_main.py" %*\r\n` : `#!/bin/sh\nexec "${python}" "$(dirname "$0")/engine_main.py" "$@"\n`);
|
||||
const files = [entry, 'engine_main.py'].map(p => { const b = readFileSync(join(folder, p)); return { path: p, sha256: sha(b), bytes: b.length }; });
|
||||
writeFileSync(join(folder, 'engine-component.json'), JSON.stringify({ schema_version: 1, id: 'strata-engine', version: '0.1.45', platform, backend: 'cuda', entrypoint: entry, files }));
|
||||
const tok = join(dirs.install, 'tokenizer');
|
||||
mkdirSync(tok, { recursive: true });
|
||||
const script = `import json, sys\nfrom tools.strata_tokenizer import BYTE_TO_UNICODE\ntok = sys.argv[1]\ntokens = [BYTE_TO_UNICODE[i] for i in range(256)] + ['ab', '<|im_start|>', '<|im_end|>', '<|endoftext|>']\njson.dump({t: i for i, t in enumerate(tokens)}, open(tok + '/vocab.json', 'w'))\nopen(tok + '/merges.txt', 'w').write('a b')\njson.dump([0] * 257 + [3, 3, 3], open(tok + '/token_type.json', 'w'))\n`;
|
||||
const made = spawnSync(python, ['-c', script, tok], { cwd: control, env: { ...baseEnv, PYTHONNOUSERSITE: '1' }, encoding: 'utf8' });
|
||||
if (made.status !== 0) throw new Error('tokenizer fixture: ' + made.stderr);
|
||||
writeFileSync(join(dirs.install, 'strata-iq3_s.json'), JSON.stringify({ exe: 'component:strata-engine', backend: 'cuda', tokenizer: tok, args: ['--max-context', '32768', '--pool-workers', '12'], cwd: dir, model_name: 'stub', host: '127.0.0.1', port: 0, kv: 'int8' }));
|
||||
}
|
||||
const port = env.FIXED_PORT ? Number(env.FIXED_PORT) : await freePort(), base = `http://127.0.0.1:${port}`;
|
||||
const host = spawn(nodeExe, [join(repoHost, 'server', 'index.mjs')], { cwd: repoHost, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
|
||||
env: { ...baseEnv, STRATA_PORT: String(port), STRATA_DIST: join(repoHost, 'dist'), STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1', STRATA_NO_ACQUIRE: '1', STRATA_CONTROL_DIR: control, STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()) } });
|
||||
let log = ''; host.stdout.on('data', c => { log += c; }); host.stderr.on('data', c => { log += c; });
|
||||
const api = async (path, init) => { const r = await fetch(base + path, init); return { status: r.status, body: await r.json().catch(() => null) }; };
|
||||
const browser = await chromium.launch({ executablePath: CHROME, headless: true });
|
||||
try {
|
||||
let up = false;
|
||||
for (let i = 0; i < 200 && !up; i++) { try { up = (await api('/api/health')).status === 200; } catch { await delay(150); } }
|
||||
check(up, `${name}: the packaged host starts`);
|
||||
await api('/api/native?refresh=1');
|
||||
await body({ base, api, browser, dirs, shot: (page, n) => page.screenshot({ path: join(shots, `${name}-${n}.png`) }), log: () => log });
|
||||
} catch (error) {
|
||||
check(false, `${name}: the flow`, String(error?.stack ?? error).slice(0, 700) + '\n' + log.slice(-600));
|
||||
} finally {
|
||||
await browser.close().catch(() => {});
|
||||
host.kill(); await new Promise(r => host.once('exit', r));
|
||||
}
|
||||
}
|
||||
const newPage = async (browser, base, options = {}) => {
|
||||
const context = await browser.newContext({ viewport: { width: 1400, height: 950 }, ...(process.env.SCHEME ? { colorScheme: process.env.SCHEME } : {}), ...options });
|
||||
const page = await context.newPage();
|
||||
const problems = [];
|
||||
page.on('pageerror', e => problems.push(e.message));
|
||||
page.problems = problems;
|
||||
await page.goto(base, { waitUntil: 'domcontentloaded' });
|
||||
return page;
|
||||
};
|
||||
const clickText = async (page, re, timeout = 30000) => { const b = page.getByRole('button', { name: re }).first(); await b.waitFor({ timeout }); await b.click(); };
|
||||
const noScroll = async page => page.evaluate(() => {
|
||||
const root = document.querySelector('.guided-onboarding');
|
||||
const bad = [];
|
||||
for (let el = document.querySelector('.onboard-tune-stage, .onboard-prepare-stage, .onboard-model-stage') ?? root; el && el !== document.body; el = el.parentElement) {
|
||||
const style = getComputedStyle(el);
|
||||
if (/(auto|scroll)/.test(style.overflowY) && el.scrollHeight > el.clientHeight + 1) bad.push(`${el.className || el.tagName}: ${el.scrollHeight} > ${el.clientHeight}`);
|
||||
}
|
||||
return bad;
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ download
|
||||
await scenario('download', {}, async ({ base, api, browser, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /get started/i);
|
||||
await page.getByText(/Choose a version/i).waitFor({ timeout: 30000 });
|
||||
await page.locator('[role=radio]').filter({ hasText: 'IQ3_S' }).first().click();
|
||||
await clickText(page, /^Continue/);
|
||||
await clickText(page, /Download model/i, 60000);
|
||||
await page.locator('.onboard-progress progress').waitFor({ timeout: 30000 });
|
||||
// ONE bar: sample the percent while it runs, it only goes up
|
||||
const samples = [];
|
||||
let eta = '';
|
||||
for (let i = 0; i < 40 && !eta; i++) {
|
||||
const state = (await api('/api/native')).body;
|
||||
const d = state.jobs.at(-1)?.result?.install?.download;
|
||||
if (d) samples.push(d.done_gb);
|
||||
eta = (await page.locator('.onboard-progress-actions span').first().textContent({ timeout: 1000 }).catch(() => '')) ?? '';
|
||||
await delay(1000);
|
||||
}
|
||||
await shot(page, '1-wizard-progress');
|
||||
check(/About \d+ (min left|h)/.test(eta), 'the wizard shows about how many minutes are left', eta.trim());
|
||||
const order = await page.evaluate(() => { const a = document.querySelector('.onboard-progress-actions'); const kids = a ? [...a.children].map(k => k.tagName + ':' + Math.round(k.getBoundingClientRect().left)) : []; return kids; });
|
||||
check(order.length === 2 && order[0].startsWith('SPAN') && order[1].startsWith('BUTTON') && Number(order[0].split(':')[1]) < Number(order[1].split(':')[1]), 'the time left sits left of Stop', order.join(' '));
|
||||
check(samples.length >= 3 && samples.every((v, i) => i === 0 || v >= samples[i - 1]), 'one combined count that only goes up', samples.map(v => v.toFixed(1)).join(', '));
|
||||
const meta = await page.locator('.onboard-progress-meta span').first().textContent();
|
||||
check(/GB/.test(meta ?? '') && /83\.6/.test(meta ?? ''), 'the bar says done / total over the whole model', meta);
|
||||
// close the pop-up while it downloads
|
||||
await clickText(page, /Finish later/i);
|
||||
await page.waitForURL(/#\/models/, { timeout: 15000 }).catch(() => {});
|
||||
await page.getByText(/Preparing your model/i).waitFor({ timeout: 15000 }).catch(() => {});
|
||||
await delay(1500);
|
||||
await shot(page, '2-models-progress');
|
||||
await page.locator('.native-progress').first().screenshot({ path: join(shots, 'download-3-models-progress-card.png') }).catch(() => {});
|
||||
check(await page.locator('.guided-onboarding').count() === 0, 'the setup pop-up closed');
|
||||
check(await page.getByText(/Preparing your model/i).count() > 0, 'the Models screen shows the running download');
|
||||
const still = (await api('/api/native')).body.jobs.at(-1);
|
||||
check(still?.state === 'running', 'the download keeps running in the background', still?.state);
|
||||
const modelsEta = await page.locator('.native-progress .onboard-progress-actions span').first().textContent({ timeout: 15000 }).catch(() => '');
|
||||
check(/About \d+ (min left|h)/.test(modelsEta ?? ''), 'the Models screen shows the time left too', (modelsEta ?? '').trim());
|
||||
await clickText(page, /^Stop$/);
|
||||
await delay(2500);
|
||||
check((await api('/api/native')).body.jobs.at(-1)?.state !== 'running', 'Stop stops it');
|
||||
check(page.problems.length === 0, 'no page error', page.problems.join('; '));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ models
|
||||
await scenario('models', { installed: true }, async ({ base, api, browser, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /Go straight to the app/i);
|
||||
await page.waitForURL(/#\/models/, { timeout: 15000 }).catch(() => {});
|
||||
await page.getByText(/Conversation context/i).first().waitFor({ timeout: 30000 });
|
||||
await delay(1500);
|
||||
await shot(page, '1-models');
|
||||
await page.locator('.version-trigger').first().waitFor({ timeout: 20000 }).catch(async () => { await shot(page, '0-models-missing-summary'); console.log('info page text:', JSON.stringify((await page.locator('body').innerText()).slice(0, 500))); });
|
||||
const summary = await page.locator('.version-trigger').first().textContent({ timeout: 5000 }).catch(() => '');
|
||||
// the Models tab fits the first view: nothing to scroll while no list is open and Expert mode is closed
|
||||
if (await page.getByRole('button', { name: /^Close expert mode$/ }).count()) { await page.getByRole('button', { name: /^Close expert mode$/ }).click(); await delay(500); }
|
||||
await shot(page, '1b-models-one-view');
|
||||
const overflow = await page.evaluate(() => { const el = document.querySelector('.main__body'); return el ? el.scrollHeight - el.clientHeight : 0; });
|
||||
check(overflow <= 2, 'the Models tab fits the first view without scrolling', String(overflow) + ' px over');
|
||||
check(await page.locator('.family-switch').count() === 1, 'the model family switch is in the navbar');
|
||||
check(await page.locator('h1').filter({ hasText: /^Model$/ }).count() === 0, 'and the navbar does not say "Model"');
|
||||
check(/IQ3_S|High quality/.test(summary ?? ''), 'Models selects the model that is on this PC, not the recommendation', (summary ?? '').replace(/\s+/g, ' ').trim());
|
||||
check(!/IQ3_XXS|Balanced/.test(summary ?? ''), 'the recommendation (IQ3_XXS) is not what is selected');
|
||||
const load = page.getByRole('button', { name: /^Load model$/ }).first();
|
||||
await load.click();
|
||||
await delay(1200);
|
||||
check(await page.getByRole('dialog').filter({ hasText: /Download and prepare/i }).count() === 0, 'loading an installed model opens no download pop-up');
|
||||
let ready = false;
|
||||
for (let i = 0; i < 90 && !ready; i++) { const s = (await api('/api/native')).body; ready = s.instances.some(x => x.status === 'ready'); const err = s.instances.find(x => x.status === 'error'); if (err) throw new Error('load failed: ' + err.error); await delay(1000); }
|
||||
if (!ready) { const st = (await api('/api/native')).body; console.log('info jobs', JSON.stringify(st.jobs.map(j => [j.kind, j.state, j.error ?? j.summary])), 'instances', JSON.stringify(st.instances.map(i => [i.status, i.error]))); await shot(page, '2-models-after-click'); }
|
||||
check(ready, 'the model loads (a fake engine under the real Control)');
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.getByRole('button', { name: /^Unload$/ }).first().waitFor({ timeout: 30000 }).catch(() => {});
|
||||
await shot(page, '3-models-loaded');
|
||||
check(await page.getByRole('button', { name: /^Unload$/ }).count() > 0, 'after a reload the Models screen shows the model as loaded, with Unload');
|
||||
check(await page.getByRole('button', { name: /^Load model$/ }).count() === 0, 'and no longer offers Load model');
|
||||
await page.getByRole('button', { name: /^Unload$/ }).first().click();
|
||||
let gone = false;
|
||||
for (let i = 0; i < 40 && !gone; i++) { gone = (await api('/api/native')).body.instances.length === 0; await delay(500); }
|
||||
check(gone, 'Unload stops the engine');
|
||||
await page.getByRole('button', { name: /^Load model$/ }).first().waitFor({ timeout: 15000 }).catch(() => {});
|
||||
check(await page.getByRole('button', { name: /^Load model$/ }).count() > 0, 'and Load model is back');
|
||||
check(page.problems.length === 0, 'no page error', page.problems.join('; '));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ the profile used, the popover, the pop-up
|
||||
await scenario('profile', { installed: true }, async ({ base, api, browser, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /Go straight to the app/i);
|
||||
await page.getByText(/Conversation context/i).first().waitFor({ timeout: 30000 });
|
||||
await delay(1500);
|
||||
const work = () => page.getByRole('button', { name: /Leave 20% for my work/ }).first();
|
||||
const full = () => page.getByRole('button', { name: /Full performance/ }).first();
|
||||
await work().click();
|
||||
check(await work().getAttribute('aria-pressed') === 'true', 'Leave 20% is chosen before loading');
|
||||
await page.getByRole('button', { name: /^Load model$/ }).first().click();
|
||||
let ready = false;
|
||||
for (let i = 0; i < 90 && !ready; i++) { const s = (await api('/api/native')).body; ready = s.instances.some(x => x.status === 'ready'); await delay(500); }
|
||||
check(ready, 'the model loads');
|
||||
const inst = (await api('/api/native')).body.instances.find(x => x.status === 'ready');
|
||||
check(inst?.resourceProfile === 'work', 'the running model reports the profile it was started with', String(inst?.resourceProfile));
|
||||
await delay(1500);
|
||||
check(await work().getAttribute('aria-pressed') === 'true' && await full().getAttribute('aria-pressed') === 'false', 'after the load the page still shows Leave 20%, not Full performance');
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.getByRole('button', { name: /^Unload$/ }).first().waitFor({ timeout: 30000 });
|
||||
await delay(1000);
|
||||
check(await work().getAttribute('aria-pressed') === 'true', 'and after a reload it is still the profile in use');
|
||||
await shot(page, '1-profile-used');
|
||||
// the reasoning popover names the loaded model
|
||||
await page.evaluate(() => { location.hash = '#/chat'; });
|
||||
const effort = page.getByRole('button', { name: /^Reasoning effort/ }).first();
|
||||
await effort.waitFor({ timeout: 30000 });
|
||||
await effort.click();
|
||||
const header = await page.locator('.reasoning-menu-model strong').first().textContent({ timeout: 10000 });
|
||||
check(header && !/No model loaded/i.test(header), 'the Reasoning popover names the loaded model, not "No model loaded"', String(header));
|
||||
await shot(page, '2-reasoning-popover');
|
||||
await page.keyboard.press('Escape');
|
||||
// an installed model asked for with other settings: the pop-up never speaks of downloading
|
||||
await page.evaluate(() => { location.hash = '#/models'; });
|
||||
await page.getByRole('button', { name: /^Unload$/ }).first().waitFor({ timeout: 30000 });
|
||||
await page.getByRole('button', { name: /^Unload$/ }).first().click();
|
||||
await page.getByRole('button', { name: /^Load model$/ }).first().waitFor({ timeout: 30000 });
|
||||
await page.getByRole('combobox', { name: 'Conversation context' }).first().click();
|
||||
const options = page.getByRole('option');
|
||||
const pick = (await options.allTextContents()).findIndex(t => /^8K$/.test(t.trim()));
|
||||
if (pick >= 0) await options.nth(pick).click();
|
||||
await page.getByRole('button', { name: /^Load model$|^Checking/ }).first().click();
|
||||
const dialog = page.getByRole('dialog').first();
|
||||
await dialog.waitFor({ timeout: 15000 }).catch(() => {});
|
||||
const text = (await dialog.textContent().catch(() => '')) ?? '';
|
||||
await shot(page, '3-apply-dialog');
|
||||
if (text) {
|
||||
check(!/Download and prepare|Download . load|approximately|[0-9] GB/i.test(text), 'for a downloaded model the pop-up has no download wording or size', text.replace(/\s+/g, ' ').slice(0, 600));
|
||||
check(/Apply/.test(text), 'it says what happens: apply settings and load');
|
||||
} else console.log('info the settings were the saved ones, so loading needed no pop-up at all');
|
||||
check(page.problems.length === 0, 'no page error', page.problems.join('; '));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ version list, This computer, Tune, a task
|
||||
for (const scheme of ['light', 'dark']) await scenario(scheme === 'light' ? 'ui2' : 'ui2d', { installed: true, env: { FAKE_TOKEN_DELAY: '0.02', FAKE_REPLY: 'I am thinking about it. </think> ' + 'Here is the answer, word by word. '.repeat(12), STRATA_TUNE_NO_QUIET_CHECK: '1' } }, async ({ base, api, browser, shot }) => {
|
||||
const page = await newPage(browser, base, { colorScheme: scheme });
|
||||
await clickText(page, /Go straight to the app/i);
|
||||
await page.getByText(/Conversation context/i).first().waitFor({ timeout: 30000 });
|
||||
await delay(1500);
|
||||
// the version list: Recommended (the fixed four), then folded groups, one line per model
|
||||
console.log('info triggers', await page.locator('.version-trigger').count(), await page.getByRole('button', { name: 'Model version' }).count());
|
||||
await page.locator('.version-trigger').first().click();
|
||||
await delay(300);
|
||||
console.log('info expanded after click', await page.locator('.version-trigger').first().getAttribute('aria-expanded'));
|
||||
await delay(800); await shot(page, `0-after-click-${scheme}`);
|
||||
console.log('info menu', await page.evaluate(() => { const m = document.querySelector('.version-menu'); const t = document.querySelector('.version-trigger'); return JSON.stringify({ menu: m ? m.getBoundingClientRect() : null, parent: m ? [m.parentElement.className, getComputedStyle(m.parentElement).display, getComputedStyle(m.parentElement).visibility, m.parentElement.style.cssText.slice(0, 120)] : null, expanded: t?.getAttribute('aria-expanded'), roles: [...document.querySelectorAll('[role=menu]')].length }); }));
|
||||
await page.locator('.version-menu').waitFor({ timeout: 10000 });
|
||||
const heads = await page.locator('.version-head span:first-child, .version-group__head').allTextContents();
|
||||
const names = await page.locator('.version-menu > .version-row .version-row__name strong').allTextContents();
|
||||
console.log('info version list', JSON.stringify(heads.map(t => t.replace(/\s+/g, ' ').trim())), JSON.stringify(names));
|
||||
check(heads[0]?.trim() === 'Recommended', `${scheme}: the list starts with Recommended`);
|
||||
check(names.length >= 1 && names.length <= 4, `${scheme}: Recommended shows only the picks this catalog has (at most the fixed four)`, names.join(' | '));
|
||||
check(heads.some(t => /Swift models|GSQ-RCO models|Gyro models|NVFP4 models|Unsloth models/.test(t)), `${scheme}: the other models are in folded groups`);
|
||||
check(heads.at(-1)?.includes('Use your own GGUF'), `${scheme}: the last row is Use your own GGUF`);
|
||||
const tall = await page.locator('.version-row').evaluateAll(els => els.filter(e => e.getBoundingClientRect().height > 44).length);
|
||||
check(tall === 0, `${scheme}: every model is one line`, String(tall));
|
||||
await shot(page, `1-versions-${scheme}`);
|
||||
await page.keyboard.press('Escape');
|
||||
// This computer
|
||||
const pc = page.locator('.pc-card');
|
||||
await pc.waitFor({ timeout: 10000 });
|
||||
const pcText = ((await pc.textContent()) ?? '').replace(/\s+/g, ' ');
|
||||
check(/This computer/.test(pcText) && /(Fits fully in memory|read from the SSD)/.test(pcText), `${scheme}: This computer says in one line whether it fits`, pcText.slice(0, 200));
|
||||
check(await pc.locator('.pc-meter').count() >= 1, `${scheme}: it shows the memory bars`);
|
||||
check(await pc.locator('.chip, .st-pill, [class*="badge"]').count() === 0, `${scheme}: no chips or badges in it`);
|
||||
await shot(page, `2-computer-${scheme}`);
|
||||
// Tune: five steps, a live speed, a result card
|
||||
await page.getByRole('button', { name: /^Tune model$/ }).first().click({ timeout: 10000 });
|
||||
await delay(1500); await shot(page, `2b-tune-dialog-${scheme}`);
|
||||
// The fake engine has no other setting worth trying, so its tune passes through "trying" too fast to be seen. The page is shown the progress a
|
||||
// real tune reports (Control's status file: stage, speeds) for the running view; the start, the result and every other call are real.
|
||||
let fakePhase = 'run';
|
||||
await page.route('**/api/native*', async route => {
|
||||
const response = await route.fetch(); let json = null;
|
||||
try { json = await response.json(); } catch { return route.fulfill({ response }); }
|
||||
for (const job of json?.jobs ?? []) if (job.kind === 'autotune' && fakePhase === 'done') { job.state = 'done'; job.progress = null; job.result = { outcome: 'improved', gain_pct: 18, base: { decode_tok_s: 41.2 }, chosen: { decode_tok_s: 48.6 }, preset: { id: 'fast', name: 'Fast · 64K' }, answers: { class: 'exact', fields: [] } }; } else if (job.kind === 'autotune' && job.state === 'running') job.progress = { stage: 'rounds', text: null, minutesLeft: 9, runsDone: 3, runsPlanned: 6, elapsedS: 300, step: 'Trying: more experts kept in memory (round 1 of 2)', speeds: [{ label: 'base', tok_s: 41.2 }, { label: 'Fewer helper threads', tok_s: 40.1 }, { label: 'More experts kept in memory', tok_s: 46.8 }] };
|
||||
return route.fulfill({ response, json });
|
||||
});
|
||||
await page.getByRole('button', { name: /Start tuning|Tune again/ }).first().click({ timeout: 15000 });
|
||||
await page.locator('.tune-step').first().waitFor({ timeout: 60000 });
|
||||
check(await page.locator('.tune-step').count() === 5, `${scheme}: the tuner shows its five steps`);
|
||||
let sawRun = false, sawSpeed = false;
|
||||
for (let i = 0; i < 400; i++) {
|
||||
const done = await page.locator('.tune-result, .tune-progress').count();
|
||||
if (i % 4 === 0) { const jobs = (await api('/api/native')).body?.jobs ?? []; const j = jobs.find(x => x.kind === 'autotune'); if (j) console.log('info tune job', j.state, JSON.stringify(j.progress ?? null).slice(0, 300)); }
|
||||
if (await page.locator('.tune-step[data-s=run]').count()) sawRun = true;
|
||||
const big = await page.locator('.tune-live__big').first().textContent().catch(() => '');
|
||||
if (/[0-9]/.test(big ?? '')) sawSpeed = true;
|
||||
if (sawRun && sawSpeed) { await delay(700); await shot(page, `3-tune-running-${scheme}`); break; }
|
||||
if (done) break;
|
||||
await delay(600);
|
||||
}
|
||||
check(sawRun, `${scheme}: a step is shown as running`);
|
||||
check(sawSpeed, `${scheme}: the live speed shows a number while it runs`);
|
||||
if (sawSpeed) { fakePhase = 'done'; await page.locator('.tune-outcome').waitFor({ timeout: 15000 }).catch(() => {}); check(/18% faster/.test((await page.locator('.tune-outcome__body h3').textContent().catch(() => '')) ?? ''), `${scheme}: the result card shows the gain as its headline`); check(/Fast/.test((await page.locator('.tune-outcome__facts').textContent().catch(() => '')) ?? ''), `${scheme}: and what it was saved as`); }
|
||||
await shot(page, `4-tune-result-${scheme}`);
|
||||
await page.getByRole('button', { name: /^Close$/ }).first().click({ timeout: 10000 }).catch(() => {});
|
||||
// a task: the opened task is skimmable, with pause / delete / Edit / Run now
|
||||
const made = await api('/api/tasks', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ title: 'Open Downloads folder', prompt: 'Open my Downloads folder and tell me what is new.', frequency: 'weekdays', at: '08:00', weekday: 1, timeZone: 'Europe/Ljubljana', mode: 'manual', enabled: true }) });
|
||||
const id = made.body?.id ?? made.body?.task?.id;
|
||||
check(!!id, `${scheme}: a task is created`, JSON.stringify(made.body).slice(0, 120));
|
||||
await page.evaluate(i => { location.hash = '#/tasks/' + i; }, id);
|
||||
await page.getByRole('button', { name: /^Edit$/ }).first().waitFor({ timeout: 15000 });
|
||||
console.log('info header buttons', await page.getByRole('button', { name: /^Pause / }).count(), await page.getByRole('button', { name: /^Delete / }).count(), await page.getByRole('button', { name: /Run now/ }).count(), JSON.stringify(await page.locator('.task-header-actions button').evaluateAll(b => b.map(x => (x.getAttribute('aria-label') ?? x.textContent ?? '').trim()))));
|
||||
check(await page.getByRole('button', { name: /^Pause / }).count() === 1 && await page.getByRole('button', { name: /^Delete / }).count() >= 1 && await page.getByRole('button', { name: /Run now/ }).count() >= 1, `${scheme}: pause, delete, Edit and Run now are in the header`);
|
||||
check(await page.locator('.main__back').count() === 0 && await page.locator('.main__title-line .task-state').count() === 0, `${scheme}: nothing sits on the left of an opened task's navbar`);
|
||||
const factsText = ((await page.locator('.task-facts').textContent()) ?? '').replace(/\s+/g, ' ');
|
||||
check(/Weekdays/.test(factsText) && /Schedule/i.test(factsText) && /Last result/i.test(factsText), `${scheme}: schedule, what it may do and the last result are at the top`, factsText.slice(0, 160));
|
||||
await shot(page, `5-task-${scheme}`);
|
||||
await page.getByRole('button', { name: /^Pause / }).first().click();
|
||||
await page.getByRole('button', { name: /^Resume / }).first().waitFor({ timeout: 10000 });
|
||||
check(true, `${scheme}: Pause becomes Resume`);
|
||||
check(page.problems.length === 0, `${scheme}: no page error`, page.problems.join('; '));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ tune + reading size
|
||||
await scenario('tune', { installed: true, env: { FAKE_TOKEN_DELAY: '0.02', FAKE_REPLY: ['I am thinking about it.', '</think>', '', 'Here is a list:', '', '- one', '- two', '', '## Heading', '', 'Some `code` and **bold** text.', '', '| a | b |', '|---|---|', '| 1 | 2 |', '', '```', 'code line', '```', ''].join(String.fromCharCode(10)) } }, async ({ base, api, browser, dirs, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /get started/i);
|
||||
await page.getByText(/Choose a version/i).waitFor({ timeout: 30000 });
|
||||
await page.locator('[role=radio]').filter({ hasText: 'IQ3_S' }).first().click();
|
||||
await clickText(page, /^Continue/);
|
||||
await page.getByText(/Leave 20% for my work/i).waitFor({ timeout: 30000 });
|
||||
await delay(800);
|
||||
await shot(page, '1-tune-step');
|
||||
check(await page.getByText(/Read pictures when this model loads/i).count() === 0, 'the tuning step offers no picture reading for an app install');
|
||||
await clickText(page, /Tune & start chatting/i);
|
||||
let done = false, job = null;
|
||||
for (let i = 0; i < 120 && !done; i++) { job = (await api('/api/native')).body.jobs.findLast?.(j => j.kind === 'tune') ?? null; done = !!job && job.state !== 'running'; await delay(1000); }
|
||||
check(job?.state === 'done', 'tuning finishes without a vision error', job?.error ?? job?.state);
|
||||
const cfg = JSON.parse(readFileSync(join(dirs.install, 'strata-iq3_s.json'), 'utf8'));
|
||||
check(cfg.args.includes('--resident-budget-gib'), '"leave 20%" wrote a RAM budget for the experts', cfg.args.join(' '));
|
||||
const gib = Number(cfg.args[cfg.args.indexOf('--resident-budget-gib') + 1]);
|
||||
check(gib + 10 <= 63.1 * 0.8, 'the budget keeps the model within 80% of the PC RAM', `${gib} GiB`);
|
||||
// chat + reading size
|
||||
await page.getByRole('textbox', { name: /Message Strata/i }).waitFor({ timeout: 60000 });
|
||||
await page.getByRole('textbox', { name: /Message Strata/i }).fill('hello');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.locator('.st-msg--assistant .st-bubble').first().waitFor({ timeout: 60000 });
|
||||
await page.locator('.st-msg--assistant .st-bubble p').first().waitFor({ timeout: 60000 }).catch(async () => console.log('info reply so far:', JSON.stringify(await page.locator('.st-msg--assistant').first().innerText()).slice(0, 300)));
|
||||
await page.waitForFunction(() => !document.querySelector('.st-msg--assistant .caret'), null, { timeout: 90000 }).catch(() => {});
|
||||
// the chat's telemetry chips: speed and context
|
||||
const chips = page.locator('.telemetry__chip');
|
||||
const chipTexts = await chips.allInnerTexts();
|
||||
console.log('info chips', JSON.stringify(chipTexts));
|
||||
let speedText = '', contextText = '';
|
||||
for (let i = 0; i < await chips.count(); i++) {
|
||||
await chips.nth(i).click();
|
||||
await delay(400);
|
||||
const pop = await page.locator('.telepop').last().innerText().catch(() => '');
|
||||
if (/tok\/s|speed/i.test(pop) && !speedText) speedText = pop;
|
||||
if (/context/i.test(pop) && !contextText) contextText = pop;
|
||||
await page.keyboard.press('Escape');
|
||||
}
|
||||
const tps = Number(speedText.match(/([\d.]+)\s*tok(?:ens)?\/s/)?.[1] ?? NaN);
|
||||
check(tps > 15 && tps < 120, 'the speed shown is the decode speed (the engine paced at ~50 tokens a second)', speedText.replace(/\s+/g, ' ').slice(0, 160));
|
||||
check(/32\.8K|32K|32,768|32768/.test(contextText) && !/128K/.test(contextText), 'the context shown is the one the model runs with (32K), not 128K', contextText.replace(/\s+/g, ' ').slice(0, 160));
|
||||
const sizes = [];
|
||||
for (const px of [12, 14, 16, 22]) {
|
||||
await page.evaluate(v => { const k = 'strata.prefs.v2'; const cur = JSON.parse(localStorage.getItem(k) || '{}'); localStorage.setItem(k, JSON.stringify({ ...cur, contentFontSize: v })); }, px);
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.locator('.st-msg--assistant .st-bubble').first().waitFor({ timeout: 30000 });
|
||||
await page.screenshot({ path: join(shots, `tune-3-chat-${px}px.png`) });
|
||||
sizes.push(await page.evaluate(() => { const b = document.querySelector('.st-msg--assistant .st-bubble'); const t = b.querySelector('p, .md-li, span') ?? b; const u = document.querySelector('.st-msg--user .st-bubble'); const all = {}; for (const el of b.querySelectorAll('*')) { if (!el.childNodes.length || ![...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim())) continue; all[el.tagName.toLowerCase() + (el.className ? '.' + String(el.className).split(' ')[0] : '')] = parseFloat(getComputedStyle(el).fontSize); } const extra = {}; for (const [k, q] of Object.entries({ reasoning: '.st-msg--assistant .st-collapse__body', codeblock: '.st-msg--assistant .st-code pre', codehead: '.st-msg--assistant .st-code__head' })) { const e = document.querySelector(q); if (e) extra[k] = parseFloat(getComputedStyle(e).fontSize); } Object.assign(all, extra); const chain = []; for (let e = b.querySelector('p'); e && e !== b; e = e.parentElement) chain.push(e.tagName + '.' + String(e.className).split(' ')[0] + '=' + getComputedStyle(e).fontSize); all.chain = chain; globalThis.__all = all; return [parseFloat(getComputedStyle(b).fontSize), parseFloat(getComputedStyle(t).fontSize), parseFloat(getComputedStyle(u).fontSize), all]; }));
|
||||
}
|
||||
console.log('info chain', JSON.stringify(await page.evaluate(() => { const el = document.querySelector('.st-msg--assistant .st-bubble p'); const out = []; for (let e = el; e && !e.classList.contains('st-msg'); e = e.parentElement) out.push(e.tagName + '.' + String(e.className).split(' ')[0] + '=' + getComputedStyle(e).fontSize); return out; })));
|
||||
// which visible text does NOT follow the reading size? (same elements at 12 and 22)
|
||||
const survey = async px => { await page.evaluate(v => { const k = 'strata.prefs.v2'; const cur = JSON.parse(localStorage.getItem(k) || '{}'); localStorage.setItem(k, JSON.stringify({ ...cur, contentFontSize: v })); }, px); await page.reload({ waitUntil: 'domcontentloaded' }); await page.locator('.st-msg--assistant .st-bubble').first().waitFor({ timeout: 30000 }); await delay(600);
|
||||
return page.evaluate(() => { const out = {}; for (const el of document.querySelectorAll('main *, .chat *, textarea')) { if (![...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim()) && el.tagName !== 'TEXTAREA') continue; const r = el.getBoundingClientRect(); if (!r.width || !r.height) continue; const key = el.tagName.toLowerCase() + '.' + String(el.className).split(' ')[0] + ' <' + (el.parentElement?.className ?? '').toString().split(' ')[0] + '>'; out[key] = parseFloat(getComputedStyle(el).fontSize); } return out; }); };
|
||||
const a = await survey(12), b = await survey(22);
|
||||
// a fresh app (nothing saved) uses 14 px
|
||||
await page.evaluate(() => localStorage.removeItem('strata.prefs.v2'));
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.locator('.st-msg--assistant .st-bubble').first().waitFor({ timeout: 30000 });
|
||||
check(await page.evaluate(() => getComputedStyle(document.querySelector('.st-msg--assistant .st-bubble')).fontSize) === '14px', 'the default reading size is 14 px');
|
||||
const fixed = Object.keys(a).filter(k => k in b && a[k] === b[k]);
|
||||
console.log('info text that does not follow the reading size:', JSON.stringify(fixed.map(k => k + '=' + a[k])));
|
||||
await shot(page, '2-chat-22px');
|
||||
console.log('info sizes', JSON.stringify(sizes), JSON.stringify(await page.locator('.st-msg--assistant .st-bubble').first().innerHTML()).slice(0, 400));
|
||||
check(sizes[0][1] === 12 && sizes[1][1] === 14 && sizes[2][1] === 16 && sizes[3][1] === 22 && sizes[0][2] === 12 && sizes[3][2] === 22, 'the reading size changes the text size of the conversation', JSON.stringify(sizes));
|
||||
// the slider itself
|
||||
await page.evaluate(() => { location.hash = '#/chat?settings=1'; });
|
||||
const slider = page.getByLabel('Reading text size');
|
||||
if (await slider.count()) {
|
||||
await slider.first().focus();
|
||||
for (let i = 0; i < 4; i++) await page.keyboard.press('ArrowRight');
|
||||
await delay(500);
|
||||
const now = await page.evaluate(() => getComputedStyle(document.querySelector('.st-msg--assistant .st-bubble') ?? document.body).fontSize);
|
||||
check(parseFloat(now) === 18, 'moving the slider changes the conversation text at once', now);
|
||||
const box = await slider.first().boundingBox();
|
||||
await page.mouse.click(box.x + box.width * 0.9, box.y + box.height / 2);
|
||||
await delay(400);
|
||||
const dragged = await page.evaluate(() => [getComputedStyle(document.querySelector('.st-msg--assistant .st-bubble') ?? document.body).fontSize, getComputedStyle(document.documentElement).getPropertyValue('--st-content-fs'), JSON.parse(localStorage.getItem('strata.prefs.v2') || '{}').contentFontSize]);
|
||||
check(parseFloat(dragged[0]) >= 20 && parseFloat(dragged[2]) >= 20, 'clicking the slider with the mouse changes it too, and it is saved', dragged.join(' '));
|
||||
} else console.log('info the settings slider was not reachable by hash here; the stored size was checked above');
|
||||
check(page.problems.length === 0, 'no page error', page.problems.join('; '));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ delete
|
||||
await scenario('delete', { installed: true }, async ({ base, api, browser, dirs, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /Go straight to the app/i);
|
||||
await page.waitForURL(/#\/models/, { timeout: 15000 }).catch(() => {});
|
||||
await page.getByRole('button', { name: /^Load model$/ }).first().click();
|
||||
let ready = false;
|
||||
for (let i = 0; i < 90 && !ready; i++) { ready = (await api('/api/native')).body.instances.some(x => x.status === 'ready'); await delay(1000); }
|
||||
check(ready, 'a model is loaded');
|
||||
await clickText(page, /Manage models/i);
|
||||
await page.getByRole('button', { name: /^Delete$/ }).first().click();
|
||||
await delay(600);
|
||||
await shot(page, '1-delete-confirm');
|
||||
const dialog = await page.getByRole('dialog').last().innerText();
|
||||
check(/Delete this version\?/.test(dialog) && /Remove 83\.6 GB of downloaded files\. You can download this version again\./.test(dialog), 'the confirm dialog uses the existing words and the real size', dialog.replace(/\s+/g, ' ').slice(0, 600));
|
||||
check(/running model will be unloaded/i.test(dialog), 'and says the loaded model is unloaded first');
|
||||
await page.getByRole('button', { name: /^Delete version$/ }).click();
|
||||
let state = null;
|
||||
for (let i = 0; i < 40; i++) { state = (await api('/api/native')).body; if (state.instances.length === 0 && !state.installed?.models?.some(m => m.model === 'iq3_s')) break; await delay(500); }
|
||||
check(state.instances.length === 0, 'the loaded model was unloaded');
|
||||
check(!existsSync(join(dirs.install, 'strata-iq3_s.json')), 'the installer removed the run config of the model');
|
||||
check(!state.installed?.models?.some(m => m.model === 'iq3_s'), 'the model is no longer listed as downloaded');
|
||||
await shot(page, '2-after-delete');
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ perf
|
||||
await scenario('perf', { installed: true }, async ({ base, api, browser, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /Go straight to the app/i);
|
||||
await page.waitForURL(/#\/models/, { timeout: 15000 }).catch(() => {});
|
||||
await page.getByRole('button', { name: /^Load model$/ }).first().click();
|
||||
let ready = false;
|
||||
for (let i = 0; i < 90 && !ready; i++) { ready = (await api('/api/native')).body.instances.some(x => x.status === 'ready'); await delay(1000); }
|
||||
check(ready, 'a model is loaded');
|
||||
await delay(2500);
|
||||
const requests = [];
|
||||
const started = new Map();
|
||||
page.on('request', r => started.set(r, Date.now()));
|
||||
page.on('requestfinished', r => requests.push([r.url().replace(base, '').slice(0, 60), Date.now() - (started.get(r) ?? Date.now())]));
|
||||
await page.evaluate(() => { window.__long = []; new PerformanceObserver(l => { for (const e of l.getEntries()) window.__long.push(Math.round(e.duration)); }).observe({ entryTypes: ['longtask'] }); });
|
||||
const t0 = Date.now();
|
||||
await page.evaluate(() => { location.hash = '#/chat'; });
|
||||
await page.getByRole('textbox', { name: /Message Strata/i }).waitFor({ timeout: 30000 });
|
||||
const open = Date.now() - t0;
|
||||
await delay(1500);
|
||||
const long = await page.evaluate(() => window.__long);
|
||||
console.log('info chat opened in', open, 'ms; long tasks', JSON.stringify(long), 'requests', JSON.stringify(requests.filter(r => r[1] > 100)), 'all', requests.length);
|
||||
check(open < 1500 && Math.max(0, ...long) < 300, 'Models to Chat opens at once, with no long block', open + ' ms, longest task ' + Math.max(0, ...long) + ' ms');
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ polish
|
||||
await scenario('polish', { installed: true }, async ({ base, api, browser, dirs, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /Go straight to the app/i);
|
||||
await page.waitForURL(/#\/models/, { timeout: 15000 }).catch(() => {});
|
||||
await page.getByText(/Conversation context/i).first().waitFor({ timeout: 30000 });
|
||||
check(await page.getByRole('button', { name: /^Tune model$/ }).count() === 1, 'the Models tab has the Tune model button (the real tuner, not the old short benchmark)');
|
||||
await clickText(page, /Manage models/i);
|
||||
await delay(500);
|
||||
const rows = await page.locator('.native-manage-row').count();
|
||||
check(rows === 1, 'Manage models lists only the downloaded model, not the catalog', rows + ' row(s)');
|
||||
await shot(page, '1-manage-models');
|
||||
await page.getByRole('button', { name: /^Done$/ }).click();
|
||||
await page.getByRole('button', { name: /Other local models/i }).click().catch(async () => { await clickText(page, /Manage models/i); await clickText(page, /Use a local GGUF/i); });
|
||||
await page.getByText(/Folder with the GGUF files/i).first().waitFor({ timeout: 15000 }).catch(() => {});
|
||||
await shot(page, '2-local-gguf');
|
||||
check(await page.getByText(/Folder with the GGUF files/i).count() > 0, 'the local GGUF page opens in the same design as Models');
|
||||
await page.getByPlaceholder(/Full path to the folder/i).fill(join(dirs.install, 'does-not-exist'));
|
||||
await page.getByRole('button', { name: /^Check files$/ }).click();
|
||||
await delay(3000);
|
||||
check(await page.locator('.error-notice').count() > 0, 'a folder that cannot be used says so in words');
|
||||
await shot(page, '2b-local-gguf-problem');
|
||||
await page.evaluate(() => { location.hash = '#/chat?settings=1'; });
|
||||
await delay(1500);
|
||||
const text = await page.locator('body').innerText();
|
||||
check(!/Contrast/.test(text), 'Settings has no Contrast option');
|
||||
check(!/View request log/.test(text), 'and no "View request log" button');
|
||||
await page.getByText(/^Engine$/).first().click().catch(() => {});
|
||||
await delay(1500);
|
||||
await shot(page, '3-settings-engine');
|
||||
const engine = await page.locator('body').innerText();
|
||||
check(/Update component/.test(engine) && /Diagnostics/.test(engine), 'Settings > Engine has Update component and Diagnostics');
|
||||
check(!/Performance|Model settings|Calibrate|Engine installation/.test(engine), 'and nothing else');
|
||||
await page.getByRole('button', { name: /^View logs$/ }).click(); // the logs open in a pop-up that holds Save logs
|
||||
await page.getByRole('button', { name: /^Save logs$/ }).click();
|
||||
await page.getByText(/Saved to/).first().waitFor({ timeout: 15000 }).catch(() => {});
|
||||
const savedText = await page.getByText(/Saved to/).first().innerText().catch(() => '');
|
||||
const savedPath = savedText.replace(/^Saved to\s*/, '').trim();
|
||||
check(!!savedPath && existsSync(savedPath), 'Save logs writes a file to Downloads and says where', savedPath);
|
||||
await shot(page, '4-logs-saved');
|
||||
await page.evaluate(() => { location.hash = '#/monitor'; });
|
||||
await delay(6000);
|
||||
await shot(page, '5-monitor');
|
||||
const mon = await page.locator('body').innerText();
|
||||
check(/°C/.test(mon), 'the Monitor shows GPU data with no model loaded', (mon.match(/.{0,30}°C.{0,20}/)?.[0] ?? 'no temperature text').replace(/\s+/g, ' '));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ stop
|
||||
await scenario('stop', { installed: true, env: { FAKE_TOKEN_DELAY: '0.1', FAKE_REPLY: 'thinking '.repeat(30) } }, async ({ base, api, browser, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /Go straight to the app/i);
|
||||
await page.waitForURL(/#\/models/, { timeout: 15000 }).catch(() => {});
|
||||
await page.getByRole('button', { name: /^Load model$/ }).first().click();
|
||||
let ready = false;
|
||||
for (let i = 0; i < 90 && !ready; i++) { ready = (await api('/api/native')).body.instances.some(x => x.status === 'ready'); await delay(1000); }
|
||||
if (!ready) { const st = (await api('/api/native')).body; console.log('info jobs', JSON.stringify(st.jobs.map(j => [j.kind, j.state, j.error ?? j.summary])), JSON.stringify(st.instances.map(i => [i.status, i.error]))); await shot(page, '0-stop-notloaded'); }
|
||||
check(ready, 'a model is loaded');
|
||||
await page.evaluate(() => { location.hash = '#/chat'; });
|
||||
const box = page.getByRole('textbox', { name: /Message Strata/i });
|
||||
await box.waitFor({ timeout: 30000 });
|
||||
await box.fill('hello');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.locator('.typing').first().waitFor({ timeout: 30000 });
|
||||
await delay(1500);
|
||||
await page.getByRole('button', { name: /^Stop/i }).first().click().catch(async () => { await page.keyboard.press('Escape'); });
|
||||
await delay(4000);
|
||||
const dots = await page.locator('.typing').count();
|
||||
await shot(page, '1-after-stop');
|
||||
check(dots === 0, 'after Stop in the middle of thinking the three dots are gone', dots + ' left');
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ latest
|
||||
await scenario('latest', {}, async ({ base, browser, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
const now = Date.now();
|
||||
await page.evaluate(now => {
|
||||
const messages = [];
|
||||
for (let i = 0; i < 40; i++) { messages.push({ id: 'u' + i, role: 'user', content: 'question ' + i, at: now }, { id: 'a' + i, role: 'assistant', content: 'answer number ' + i + ' with some text to fill the line and more of it.', at: now, tokens: 20, inputTokens: 5 }); }
|
||||
localStorage.setItem('strata.sessions.v1', JSON.stringify([{ id: 's_latest', title: 'long', updatedAt: now, createdAt: now, messages }]));
|
||||
localStorage.setItem('strata.active-session.v1', 's_latest');
|
||||
}, now);
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await delay(1500);
|
||||
if (await page.getByRole('button', { name: /Go straight to the app/i }).count()) { await clickText(page, /Go straight to the app/i); await delay(1500); }
|
||||
await page.goto(base + '/#/chat', { waitUntil: 'domcontentloaded' });
|
||||
await page.getByRole('textbox', { name: /Message Strata/i }).waitFor({ timeout: 30000 });
|
||||
await delay(800);
|
||||
await page.locator('.main__body.chat').first().waitFor({ timeout: 15000 }).catch(async () => { await shot(page, '0-latest-missing'); console.log('info body', JSON.stringify((await page.locator('body').innerText()).slice(0, 300))); });
|
||||
await page.locator('.main__body.chat').first().evaluate(el => { el.scrollTop = 3000; });
|
||||
await delay(600);
|
||||
await page.mouse.wheel(0, -400);
|
||||
await delay(600);
|
||||
await page.locator('.main__body.chat').first().evaluate(el => { el.scrollTop = 0; });
|
||||
await delay(800);
|
||||
const gaps = await page.evaluate(() => { const b = document.querySelector('.jumplatest'), c = document.querySelector('.main__body.chat'); if (!b || !c) return null; const r = b.getBoundingClientRect(), cr = c.getBoundingClientRect(); return { top: Math.round(r.top - cr.top), bottom: Math.round(cr.bottom - r.bottom) }; });
|
||||
console.log('info chat dom', JSON.stringify(await page.evaluate(() => { const c = document.querySelector('.main__body.chat'); return { msgs: document.querySelectorAll('.st-msg').length, scrollH: c?.scrollHeight, clientH: c?.clientHeight, top: c?.scrollTop, jump: document.querySelectorAll('.jumplatest').length }; })));
|
||||
await shot(page, '1-latest-button');
|
||||
check(!!gaps && gaps.top >= 12 && gaps.bottom >= 12, 'the Latest button keeps a gap from the top navigation and from the bottom', JSON.stringify(gaps));
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ fail
|
||||
await scenario('fail', { installed: true, env: { FAKE_ENGINE_FAIL: '1' } }, async ({ base, api, browser, shot }) => {
|
||||
const page = await newPage(browser, base);
|
||||
await clickText(page, /get started/i);
|
||||
await page.getByText(/Choose a version/i).waitFor({ timeout: 30000 });
|
||||
await page.locator('[role=radio]').filter({ hasText: 'IQ3_S' }).first().click();
|
||||
await clickText(page, /^Continue/);
|
||||
await clickText(page, /Tune & start chatting/i, 30000);
|
||||
await page.getByText(/ran low on memory/i).first().waitFor({ timeout: 120000 });
|
||||
await delay(1000);
|
||||
await shot(page, '1-tune-error');
|
||||
const text = await page.locator('.guided-onboarding').innerText();
|
||||
check(/ran low on memory/.test(text), 'the error says in words that the PC ran low on memory');
|
||||
check(/Leave 20% for my work/.test(text) && /low-RAM/.test(text), 'and what to try');
|
||||
check(!/server_exited|memory guard/.test(text), 'no code or log line in the main text');
|
||||
check(!/vision files/i.test(text), 'no picture-reading error');
|
||||
const bad = await noScroll(page);
|
||||
check(bad.length === 0, 'the tuning pop-up does not scroll when an error shows', bad.join('; '));
|
||||
await page.locator('.error-notice summary').first().click();
|
||||
await delay(500);
|
||||
await shot(page, '2-tune-error-details');
|
||||
const details = await page.locator('.error-notice pre').first().innerText();
|
||||
check(/server_exited/.test(details) && /exited before/.test(details), 'Details holds the code and the reason the server logged', details.replace(/\s+/g, ' ').slice(0, 120));
|
||||
const badOpen = await noScroll(page);
|
||||
check(badOpen.length === 0, 'and the pop-up still does not scroll with Details open', badOpen.join('; '));
|
||||
await page.setViewportSize({ width: 1280, height: 700 });
|
||||
await delay(600);
|
||||
const small = await noScroll(page);
|
||||
check(small.length === 0, 'nor in a small window (1280x700), Details open', small.join('; '));
|
||||
await shot(page, '3-tune-error-small-window');
|
||||
void api;
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ restart
|
||||
// The app keeps its theme, text size, language and chat list in the browser's storage, which belongs to the window's origin
|
||||
// (127.0.0.1:<port>). The shell now uses the same port every launch: stop the host, start it again on that port, reopen the
|
||||
// window with the same browser profile, and everything is still there.
|
||||
if (only.has('restart')) {
|
||||
console.log('--- restart');
|
||||
const fixed = await freePort();
|
||||
const profile = join(work, 'browser-profile');
|
||||
const run = async (body) => scenario('restart', { env: { FIXED_PORT: String(fixed) } }, async ctx => {
|
||||
const context = await chromium.launchPersistentContext(profile, { executablePath: CHROME, headless: true, viewport: { width: 1200, height: 800 } });
|
||||
try { await body(context, ctx); } finally { await context.close(); }
|
||||
});
|
||||
only.add('restart');
|
||||
await run(async (context, { base }) => {
|
||||
const page = context.pages()[0] ?? await context.newPage();
|
||||
await page.goto(base, { waitUntil: 'domcontentloaded' });
|
||||
await page.evaluate(() => { localStorage.setItem('strata.prefs.v2', JSON.stringify({ theme: 'dark', contentFontSize: 19, language: 'de' })); localStorage.setItem('strata.sessions.v1', JSON.stringify([{ id: 'chat-1', title: 'My chat' }])); });
|
||||
});
|
||||
await run(async (context, { base }) => {
|
||||
const page = context.pages()[0] ?? await context.newPage();
|
||||
await page.goto(base, { waitUntil: 'domcontentloaded' });
|
||||
const saved = await page.evaluate(() => ({ prefs: JSON.parse(localStorage.getItem('strata.prefs.v2') || '{}'), chats: localStorage.getItem('strata.sessions.v1'), theme: document.documentElement.dataset.theme }));
|
||||
check(saved.prefs.contentFontSize === 19 && saved.prefs.language === 'de', 'text size and language survive a restart on the same port', JSON.stringify(saved.prefs));
|
||||
check(saved.theme === 'dark', 'the theme survives too', saved.theme);
|
||||
check(/My chat/.test(saved.chats ?? ''), 'and the chat list', saved.chats);
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ theme
|
||||
await scenario('theme', {}, async ({ base, browser, shot }) => {
|
||||
for (const scheme of ['dark', 'light']) {
|
||||
const page = await newPage(browser, base, { colorScheme: scheme });
|
||||
await delay(1500);
|
||||
const theme = await page.evaluate(() => document.documentElement.dataset.theme);
|
||||
check(theme === scheme, `a fresh app follows the system (${scheme})`, theme);
|
||||
await shot(page, `1-${scheme}`);
|
||||
await page.context().close();
|
||||
}
|
||||
});
|
||||
|
||||
// the junctions into the staged Control go first (rmdir on a junction removes only the link)
|
||||
for (const name of ['python', 'strata_control', 'strata_cli', 'serve']) { try { rmdirSync(join(control, name)); } catch { /* already gone */ } }
|
||||
try { rmSync(work, { recursive: true, force: true, maxRetries: 3 }); } catch (e) { console.log('info scratch folder left behind:', work); }
|
||||
console.log(`shots in ${shots}`);
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Picture reading in the APP, end to end on a test box, through the installed app layout (packaged host + bundled Control + a real engine component
|
||||
* that carries strata-vision):
|
||||
* Download and prepare with "Read pictures" ON (the picture-reader file is fetched, the vision settings are written), load the model with the picture
|
||||
* reader, send a real picture through the host's own /v1 endpoint and check that the answer names what is in it, unload, shut down.
|
||||
*
|
||||
* node vision-e2e.mjs --install <folder with node/ host/ control/> --components <folder holding engine/<package with strata-vision>> \
|
||||
* --work <scratch> --family qwen --model IQ3_XXS --data <data folder that has (or will get) the model files> --image shapes.png [--device cpu|gpu]
|
||||
*
|
||||
* The picture has a red circle, a blue square, a green triangle and the text "STRATA 42". The run exits 1 if any step fails.
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { createServer } from 'node:net';
|
||||
import { join, resolve } from 'node:path';
|
||||
|
||||
const args = Object.fromEntries(process.argv.slice(2).reduce((acc, a, i, all) => (a.startsWith('--') ? [...acc, [a.slice(2), all[i + 1]?.startsWith('--') || all[i + 1] === undefined ? true : all[i + 1]]] : acc), []));
|
||||
for (const need of ['install', 'components', 'work', 'model', 'image']) if (!args[need]) { console.error('missing --' + need); process.exit(2); }
|
||||
const install = resolve(args.install), work = resolve(args.work), components = resolve(args.components), family = String(args.family ?? 'qwen'), modelId = String(args.model);
|
||||
const device = String(args.device ?? 'cpu');
|
||||
const node = join(install, 'node', 'node'), hostEntry = join(install, 'host', 'server', 'index.mjs'), controlDir = join(install, 'control');
|
||||
const dirs = { models: join(work, 'models'), profiles: join(work, 'profiles'), runtime: join(work, 'runtime'), home: join(work, 'home'), xdg: join(work, 'xdg'), appdata: join(work, 'appdata') };
|
||||
for (const d of [dirs.models, dirs.home, dirs.xdg, dirs.appdata]) mkdirSync(d, { recursive: true });
|
||||
const delay = ms => new Promise(r => setTimeout(r, ms));
|
||||
let failed = false;
|
||||
const record = (name, ok, detail = '') => { if (!ok) failed = true; console.log(`${ok ? 'ok ' : 'FAIL'} ${name}${detail ? ' ' + String(detail).slice(0, 600) : ''}`); };
|
||||
const freePort = async () => { const s = createServer(); await new Promise(r => s.listen(0, '127.0.0.1', r)); const p = s.address().port; await new Promise(r => s.close(r)); return p; };
|
||||
const env = { ...process.env, STRATA_COMPONENTS_DIR: components, STRATA_PROFILE_DIR: dirs.profiles, STRATA_RUNTIME_DIR: dirs.runtime, STRATA_INSTALL_DIR: dirs.models,
|
||||
STRATA_HOME: dirs.home, STRATA_MODELS_DIR: join(work, 'models-unused'), APPDATA: dirs.appdata, XDG_CONFIG_HOME: dirs.xdg, PYTHONDONTWRITEBYTECODE: '1' };
|
||||
delete env.STRATA_API_KEY;
|
||||
const port = await freePort();
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
const host = spawn(node, [hostEntry], { cwd: join(install, 'host'), stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: { ...env, STRATA_PORT: String(port), STRATA_DIST: join(install, 'host', 'dist'), NODE_ENV: 'production', STRATA_PACKAGED: '1', STRATA_REAL_ENGINE: '1', STRATA_NO_ACQUIRE: '1',
|
||||
STRATA_SHARE_PORT: String(await freePort()), STRATA_TUNNEL_PORT: String(await freePort()) } });
|
||||
let hostLog = '';
|
||||
host.stdout.on('data', c => { hostLog += c; }); host.stderr.on('data', c => { hostLog += c; });
|
||||
const call = async (path, method = 'GET', body) => {
|
||||
const response = await fetch(base + path, { method, headers: { 'content-type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body) });
|
||||
return { status: response.status, body: await response.json().catch(() => null) };
|
||||
};
|
||||
async function waitJob(id, seconds) {
|
||||
let last = '';
|
||||
for (let i = 0; i < seconds * 2; i++) {
|
||||
const state = (await call('/api/native')).body;
|
||||
const job = state.jobs.find(j => j.id === id);
|
||||
if (job && job.summary !== last) { last = job.summary; console.log(' ... ' + String(last).slice(0, 120)); }
|
||||
if (job && job.state !== 'running') return { job, state };
|
||||
await delay(500);
|
||||
}
|
||||
throw new Error('operation did not finish');
|
||||
}
|
||||
async function main() {
|
||||
let up = false;
|
||||
for (let i = 0; i < 200 && !up; i++) { try { up = (await fetch(base + '/api/health')).ok; } catch { /* not yet */ } if (!up) await delay(150); }
|
||||
record('host healthy', up, up ? base : hostLog.slice(-500));
|
||||
if (!up) return;
|
||||
const packages = existsSync(join(components, 'engine')) ? readdirSync(join(components, 'engine')) : [];
|
||||
const reader = packages.some(p => existsSync(join(components, 'engine', p, 'strata-vision')));
|
||||
record('the engine component carries the picture reader (strata-vision)', reader, packages.join(', '));
|
||||
const status = (await call('/api/control/status')).body;
|
||||
record('Control: engine usable', status.engine?.usable === true, `engine=${status.engine?.engine?.version}`);
|
||||
|
||||
// ---- Download and prepare, "Read pictures" on (the app sends vision "yes" for the download)
|
||||
const values = { family, model: modelId, context: 32768, vision: 'yes', loadVision: device, ...(args.data ? { data_dir: resolve(args.data) } : {}) };
|
||||
const plan = await call('/api/native/plan', 'POST', values);
|
||||
record('the plan for the download is made', plan.status === 200, JSON.stringify(plan.body).slice(0, 300));
|
||||
const started = await call('/api/native/operations', 'POST', { kind: 'install', args: values });
|
||||
record('the install operation starts', started.status === 200 || started.status === 202, JSON.stringify(started.body).slice(0, 200));
|
||||
const done = await waitJob(started.body.id, 3600);
|
||||
record('Download and prepare finishes', done.job.state === 'done', `${done.job.state} ${done.job.error ?? done.job.summary}`);
|
||||
const native = (await call('/api/native?refresh=1')).body;
|
||||
console.log(' installed models: ' + JSON.stringify((native.installed?.models ?? []).map(m => ({ id: m.id, model: m.model, ready: m.ready, visionPrepared: m.visionPrepared, images: m.images, config: m.config }))));
|
||||
const want = modelId.toLowerCase();
|
||||
const model = native.installed?.models?.find(m => [m.id, m.model, m.tag].some(x => String(x ?? '').toLowerCase() === want));
|
||||
record('the model is installed with the picture reader prepared (visionPrepared)', !!model?.visionPrepared, JSON.stringify({ ready: model?.ready, visionPrepared: model?.visionPrepared, images: model?.images }));
|
||||
const configFile = model?.configPath ?? (model?.config ? join(dirs.models, model.config) : null);
|
||||
let cfg = {};
|
||||
try { cfg = JSON.parse(readFileSync(configFile, 'utf8')); } catch { /* reported below */ }
|
||||
const mm = cfg.vision?.mmproj;
|
||||
record('the run settings carry the vision entry and --vision, and the mmproj file is on disk',
|
||||
!!cfg.vision && (cfg.args ?? []).includes('--vision') && !!mm && existsSync(mm) && statSync(mm).size > 800e6,
|
||||
`vision=${cfg.vision ? 'yes' : 'no'} --vision=${(cfg.args ?? []).includes('--vision')} mmproj=${mm ? Math.round(statSync(mm).size / 1048576) + ' MB' : 'none'}`);
|
||||
|
||||
// ---- load with the picture reader
|
||||
const load = await call('/api/native/operations', 'POST', { kind: 'load', args: { model: model?.model ?? modelId.toLowerCase(), vision: device } });
|
||||
const loaded = await waitJob(load.body.id, 900);
|
||||
const instance = loaded.state.instances[0];
|
||||
record('load starts the engine with the picture reader', loaded.job.state === 'done' && instance?.status === 'ready', `${loaded.job.state} ${loaded.job.error ?? ''}; instance=${instance?.status}; vision=${instance?.vision}`);
|
||||
record('the host says the loaded model reads pictures', !!instance?.vision, `vision=${instance?.vision}`);
|
||||
|
||||
// ---- a real picture through the host's own /v1 endpoint
|
||||
const png = readFileSync(resolve(args.image)).toString('base64');
|
||||
const t = Date.now();
|
||||
let answer = '', finish = '';
|
||||
for (const budget of [1536, 4096]) {
|
||||
const chat = await call('/v1/chat/completions', 'POST', { model: instance?.apiModelId ?? modelId, max_tokens: budget, stream: false, messages: [{ role: 'user', content: [
|
||||
{ type: 'text', text: 'Describe this picture: name each shape with its colour and read out any text.' },
|
||||
{ type: 'image_url', image_url: { url: 'data:image/png;base64,' + png } }] }] });
|
||||
const choice = chat.body?.choices?.[0] ?? {};
|
||||
answer = choice.message?.content ?? '';
|
||||
finish = choice.finish_reason ?? String(chat.status);
|
||||
if (answer.trim() || finish !== 'length') break;
|
||||
}
|
||||
console.log(` answer (${Math.round((Date.now() - t) / 1000)} s, finish=${finish}): ${JSON.stringify(answer.slice(0, 300))}`);
|
||||
const low = answer.toLowerCase();
|
||||
record('the answer names the red circle', /red|circle/.test(low));
|
||||
record('the answer names the blue square', /blue|square/.test(low));
|
||||
record('the answer names the green triangle', /green|triangle/.test(low));
|
||||
record('the answer reads STRATA 42', low.includes('42'));
|
||||
|
||||
// ---- unload
|
||||
const unloaded = await call(`/api/native/instances/${instance?.id}/unload`, 'POST', {});
|
||||
record('unload', unloaded.status === 200, unloaded.status);
|
||||
}
|
||||
try { await main(); } catch (e) { record('run', false, e.stack ?? e); }
|
||||
host.kill();
|
||||
await delay(1500);
|
||||
console.log(failed ? 'VISION-E2E FAIL' : 'VISION-E2E PASS');
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,2 @@
|
||||
# Rust build output. Regenerated by `cargo tauri build`; never committed.
|
||||
/target/
|
||||
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "strata-desktop"
|
||||
version = "0.2.1"
|
||||
description = "Strata Desktop — local AI, tuned to one model."
|
||||
edition = "2021"
|
||||
rust-version = "1.77"
|
||||
license = "MIT"
|
||||
|
||||
# Linux: the drop of a file on the window is read straight from the GTK drag signals (see hook_linux_drops in main.rs); the versions are the ones Tauri itself uses.
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
gtk = "0.18"
|
||||
webkit2gtk = "2.0"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-single-instance = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = "s"
|
||||
panic = "abort"
|
||||
strip = true
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Tauri uses the system WKWebView, so this bundle needs no JIT entitlement of its own;
|
||||
the web content process gets what it needs from the OS. The App Host runs as an ordinary
|
||||
child process and needs no special rights. Kept minimal on purpose. -->
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1 @@
|
||||
{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-exit":{"identifier":"allow-exit","description":"Enables the exit command without any pre-configured scope.","commands":{"allow":["exit"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-exit":{"identifier":"deny-exit","description":"Denies the exit command without any pre-configured scope.","commands":{"allow":[],"deny":["exit"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"DenLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 433 B |
|
After Width: | Height: | Size: 1004 B |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 771 B |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 949 B |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 504 B |
|
After Width: | Height: | Size: 1019 B |