mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 14:21:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
011c9e600a | ||
|
|
76f90e5843 | ||
|
|
328a3f5bd2 | ||
|
|
1df3a3108b | ||
|
|
daa654bd35 | ||
|
|
68864c564e | ||
|
|
2c27afec08 | ||
|
|
1c9bfd2a6e | ||
|
|
be4e24d421 | ||
|
|
3a7a6955cf | ||
|
|
4f864bcd2d | ||
|
|
2c8eb88196 | ||
|
|
e0cfb80f53 | ||
|
|
86d10ed8f9 | ||
|
|
01f8c32559 | ||
|
|
894f133b7d | ||
|
|
8a4106a47e | ||
|
|
4c3edf4765 | ||
|
|
f05725c332 | ||
|
|
8de1ec4165 | ||
|
|
3fbcea2f79 | ||
|
|
dc16e3a7cb | ||
|
|
5cdb59cfa9 | ||
|
|
0cf83f610a | ||
|
|
18b4affb87 | ||
|
|
aae92ceb26 | ||
|
|
926e7a38b8 | ||
|
|
366c8dbaae | ||
|
|
70f4240728 | ||
|
|
30a67051d0 | ||
|
|
b8b381a3b9 | ||
|
|
25d025fef8 | ||
|
|
8895949ef7 | ||
|
|
0da82e740c | ||
|
|
95cfbd4cc4 | ||
|
|
c8b1436022 | ||
|
|
9de04cd44e | ||
|
|
31ca248fbe | ||
|
|
63214a5655 | ||
|
|
86620837b2 | ||
|
|
75d0d0b966 | ||
|
|
6515aebac2 | ||
|
|
f801953992 | ||
|
|
53752401f2 | ||
|
|
ab61f892d8 | ||
|
|
034422c4d3 | ||
|
|
aece46db89 | ||
|
|
1e7bcb8b6e | ||
|
|
c5eb2dd923 | ||
|
|
88942998c5 | ||
|
|
0255581677 | ||
|
|
9b5b758358 | ||
|
|
29c811217f | ||
|
|
5f81c5dc47 | ||
|
|
cbf7f1d9f9 | ||
|
|
4e83ef04d5 | ||
|
|
34d7b2a3ba | ||
|
|
31fa54fb68 | ||
|
|
bc3f293861 | ||
|
|
21b0fdb0d4 | ||
|
|
564d0abebe | ||
|
|
413544b6d9 | ||
|
|
634e5826a5 | ||
|
|
c4f1e05dbd | ||
|
|
e2b4464917 | ||
|
|
10edc50f05 | ||
|
|
f353087104 | ||
|
|
79946b9c4f | ||
|
|
4cb96acd81 | ||
|
|
ca40c88ef4 | ||
|
|
e28889d120 | ||
|
|
890fee81b0 | ||
|
|
5e633b4ff7 | ||
|
|
7c11a196d5 | ||
|
|
8bf77c0755 | ||
|
|
8c41d3611c | ||
|
|
2aa1ec2d56 | ||
|
|
7a55995818 | ||
|
|
82a97251ea | ||
|
|
2e58e320b5 | ||
|
|
301ab7c7b4 | ||
|
|
0fce78c1c3 | ||
|
|
095e524fca | ||
|
|
88278af79a | ||
|
|
c864d804b6 | ||
|
|
51f418f15a | ||
|
|
9f38ee338c | ||
|
|
b2f2149431 | ||
|
|
ed61ad2a9c | ||
|
|
06e614017d | ||
|
|
2411e7cd3b | ||
|
|
8cfe44b71d | ||
|
|
1b47c1d076 | ||
|
|
b1594fbbe5 | ||
|
|
4dd62738df | ||
|
|
f6b4101127 | ||
|
|
d581286885 | ||
|
|
0b736f7702 | ||
|
|
b6825a493c | ||
|
|
c0b0869b03 | ||
|
|
2219cc99bf | ||
|
|
4681762e3c | ||
|
|
6b93b42877 | ||
|
|
d3cb2f58f1 | ||
|
|
9bb1dd2044 | ||
|
|
c3472caf8e | ||
|
|
f1bfcc2253 | ||
|
|
a64f1dd99b | ||
|
|
02791a6d18 | ||
|
|
672fd99ccc | ||
|
|
1f727f4b52 | ||
|
|
ebe4db78d4 | ||
|
|
b486d6f2b4 | ||
|
|
43127e3982 | ||
|
|
6c688faf22 | ||
|
|
0a35538b74 | ||
|
|
72f66767ca | ||
|
|
031adf99bf | ||
|
|
e3040e9e25 | ||
|
|
6f1eae68ff | ||
|
|
085474b8dc | ||
|
|
1d6a040129 | ||
|
|
a0e09803ac | ||
|
|
2fcc25aab6 | ||
|
|
67bb56bd1d | ||
|
|
d511c5102e | ||
|
|
d408fa2ff1 | ||
|
|
970c75c8f6 | ||
|
|
ee8e6404b0 | ||
|
|
17cb817594 | ||
|
|
594e379291 | ||
|
|
fd4f7d445c | ||
|
|
226b5f5492 | ||
|
|
9dc1c96377 | ||
|
|
4e1e6bad4e | ||
|
|
3de97a8892 | ||
|
|
c23a4a5827 | ||
|
|
4aabf3b8f5 | ||
|
|
6ce6413606 | ||
|
|
4fd5b0646d | ||
|
|
703daada09 | ||
|
|
d69c9c4b4f | ||
|
|
b22206c7bc | ||
|
|
a4f7975324 | ||
|
|
bcf88b7383 | ||
|
|
6a55bcb5e9 | ||
|
|
c7938c2d3e | ||
|
|
69a6db1904 | ||
|
|
1b6f782faf | ||
|
|
215aa14f25 | ||
|
|
4fc2e140bd | ||
|
|
740acc8934 | ||
|
|
72fc52251b | ||
|
|
0c79d7db94 | ||
|
|
d298999c3c | ||
|
|
632b4ed283 | ||
|
|
b111d1e830 | ||
|
|
56dee6b938 | ||
|
|
d101771758 | ||
|
|
a5b1c2cefa | ||
|
|
ca1d469e9a | ||
|
|
47d9d6c75a | ||
|
|
f2709679db | ||
|
|
c330caf4da | ||
|
|
7adced3db5 | ||
|
|
a0b6d1cd11 | ||
|
|
774096d16b | ||
|
|
76186bb595 | ||
|
|
e346198ff3 | ||
|
|
98faf8139e | ||
|
|
17b93dcb7c | ||
|
|
52ecf67ef7 | ||
|
|
f74ba2ea53 | ||
|
|
4fb0dde729 | ||
|
|
f94a34c330 | ||
|
|
c7c0e37bc4 | ||
|
|
2f4e42b9cf | ||
|
|
d61a443eb8 | ||
|
|
52cc6b4af0 | ||
|
|
31361a98bf | ||
|
|
87b7bb2ccb | ||
|
|
48c34f735a | ||
|
|
537f8fef8e | ||
|
|
83ae413176 | ||
|
|
80f7679b61 | ||
|
|
b8488e0d2b | ||
|
|
d0afabb6ab | ||
|
|
656478c68d | ||
|
|
fccfd343d7 | ||
|
|
5a9b4f4fc4 | ||
|
|
4509f2f997 | ||
|
|
f9e1fff983 | ||
|
|
a6ffc04d6d | ||
|
|
a5c7abe074 | ||
|
|
2793e3bd33 | ||
|
|
1b9b768939 | ||
|
|
642425bf2e | ||
|
|
cc500be1c9 | ||
|
|
13806c323e | ||
|
|
2f361a3757 | ||
|
|
2dfb254f7b | ||
|
|
28b150f854 | ||
|
|
676b57bff2 | ||
|
|
9424830516 | ||
|
|
00e8d10994 | ||
|
|
bf69ec2da4 | ||
|
|
7924bf65d2 | ||
|
|
867d7477ff | ||
|
|
f5410a6feb | ||
|
|
bb7e399c30 | ||
|
|
4bd5912695 | ||
|
|
8c07c0c36a | ||
|
|
149abc896b | ||
|
|
069384b4f6 | ||
|
|
c644312d15 | ||
|
|
ac71d2b32e | ||
|
|
7274c10e9a | ||
|
|
f0afedc50c | ||
|
|
b5d14279aa | ||
|
|
2e12958759 | ||
|
|
e4d22e7ef1 | ||
|
|
e59e64ec5d | ||
|
|
393eec80f9 | ||
|
|
06425cf932 | ||
|
|
afbd64bc4f | ||
|
|
2614b82c9a | ||
|
|
5b8cdaf2d9 | ||
|
|
ea6b385df8 | ||
|
|
8ca9093d7d | ||
|
|
ec841ca1e9 | ||
|
|
c4442fed95 | ||
|
|
2496795bd9 | ||
|
|
8d29804204 | ||
|
|
1523c592a6 | ||
|
|
f8c4410a38 | ||
|
|
df4706577e | ||
|
|
afd1f61cc5 | ||
|
|
31d857f817 | ||
|
|
4126250bf1 | ||
|
|
709f8dc63d | ||
|
|
3985c50777 | ||
|
|
f68ccf30c7 |
No files matched your search
@@ -66,7 +66,7 @@ body:
|
||||
attributes:
|
||||
label: Strata version
|
||||
description: "In the app: Settings > About. In the Terminal: `strata --version`. From source: the About tab of the web page, or `engine` in `GET /v1/status`, or the git commit."
|
||||
placeholder: "Strata 0.2.1 · engine 0.1.46"
|
||||
placeholder: "Strata 0.2.2 · engine 0.1.47"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
||||
@@ -13,7 +13,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
description: 'Release tag whose engine packages go into the images (for example v0.2.1)'
|
||||
description: 'Release tag whose engine packages go into the images (for example v0.2.2)'
|
||||
required: true
|
||||
type: string
|
||||
flavors:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Strata runs the Qwen3.8-Flash-Next mixture-of-experts model (and its Coder, Swift 1.5, Unsloth, Gyro and NVFP4
|
||||
variants, and Qwen3.6-35B-A3B) on a normal PC: one NVIDIA or AMD graphics card plus system RAM on Windows or Linux,
|
||||
or an Apple Silicon Mac. Version 0.2.1, engine 0.1.46. It has three parts:
|
||||
or an Apple Silicon Mac. Version 0.2.2, engine 0.1.47. 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.
|
||||
|
||||
@@ -11,7 +11,7 @@ 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 Quick speed check. For results other people can compare with (the standard benchmark, a few minutes), run: .venv\Scripts\python.exe strata.py bench run
|
||||
echo.
|
||||
".venv\Scripts\python.exe" tools\strata_bench.py %*
|
||||
set "RC=%ERRORLEVEL%"
|
||||
|
||||
+24
-3
@@ -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.46.2 LANGUAGES CXX)
|
||||
project(strata VERSION 0.1.47 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)
|
||||
@@ -992,6 +992,12 @@ 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)
|
||||
# the Gyro trellis decode kernels (TQ2_T / TQK6 / TQK7) on synthetic blocks: decode-once vs per-column bitwise, output
|
||||
# checksums to compare two builds byte for byte, --bench for timings (GPU, no model)
|
||||
add_executable(tq_pascal_bench src/kernels/tq_pascal_bench.cpp)
|
||||
target_include_directories(tq_pascal_bench PRIVATE ${_strata_gpu_include_directories})
|
||||
target_link_libraries(tq_pascal_bench PRIVATE strata_kernels ${_strata_gpu_runtime_target})
|
||||
add_test(NAME tq_pascal_bench COMMAND tq_pascal_bench)
|
||||
set_tests_properties(iq_multi_parity PROPERTIES ENVIRONMENT "STRATA_KQ_SPLIT=1") # the Q4_K / Q5_K / Q8_0 Split path against the old kernels
|
||||
# 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)
|
||||
@@ -1407,6 +1413,9 @@ if(STRATA_BUILD_TESTS)
|
||||
add_executable(arch_defaults_test src/core/arch_defaults_test.cpp src/core/arch_defaults.cpp)
|
||||
target_include_directories(arch_defaults_test PRIVATE include)
|
||||
add_test(NAME arch_defaults_test COMMAND arch_defaults_test)
|
||||
add_executable(uma_cache_test src/core/uma_cache_test.cpp)
|
||||
target_include_directories(uma_cache_test PRIVATE include)
|
||||
add_test(NAME uma_cache_test COMMAND uma_cache_test)
|
||||
add_executable(suffix_drafter_test src/spec/suffix_drafter_test.cpp)
|
||||
target_link_libraries(suffix_drafter_test PRIVATE strata_spec)
|
||||
add_test(NAME suffix_drafter_test COMMAND suffix_drafter_test)
|
||||
@@ -1416,6 +1425,9 @@ if(STRATA_BUILD_TESTS)
|
||||
add_executable(draft_policy_test src/spec/draft_policy_test.cpp)
|
||||
target_link_libraries(draft_policy_test PRIVATE strata_spec)
|
||||
add_test(NAME draft_policy_test COMMAND draft_policy_test)
|
||||
add_executable(spec_start_test src/spec/spec_start_test.cpp)
|
||||
target_include_directories(spec_start_test PRIVATE include)
|
||||
add_test(NAME spec_start_test COMMAND spec_start_test)
|
||||
endif()
|
||||
if(STRATA_BUILD_TESTS OR STRATA_BUILD_CONVERSATION_TESTS)
|
||||
add_executable(message_boundary_test src/program/message_boundary_test.cpp)
|
||||
@@ -2095,14 +2107,14 @@ if(STRATA_BUILD_TESTS)
|
||||
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)
|
||||
set_tests_properties(native_hadamard_test PROPERTIES TIMEOUT 300 SKIP_RETURN_CODE 77)
|
||||
# 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)
|
||||
set_tests_properties(tq_cpu_parity tq_cpu_parity_avx2 PROPERTIES TIMEOUT 300 SKIP_RETURN_CODE 77)
|
||||
endif()
|
||||
|
||||
add_executable(expert_parity src/kernels/cpu/expert_parity.cpp)
|
||||
@@ -2115,10 +2127,19 @@ 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})
|
||||
# where the workers are placed (one per physical core by default) and --pool-cores (real pools, no model)
|
||||
add_executable(pool_cores_test src/kernels/cpu/pool_cores_test.cpp)
|
||||
target_link_libraries(pool_cores_test PRIVATE strata_kernels_cpu)
|
||||
add_test(NAME pool_cores_test COMMAND pool_cores_test)
|
||||
# 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)
|
||||
# 0.2.2: when the pool splits per NUMA node by itself (a fake sysfs node folder, mock cores; STRATA_NUMA_POOL=0, one node = unchanged)
|
||||
add_executable(numa_pool_test src/kernels/cpu/numa_pool_test.cpp)
|
||||
target_include_directories(numa_pool_test PRIVATE include)
|
||||
target_compile_features(numa_pool_test PRIVATE cxx_std_17)
|
||||
add_test(NAME numa_pool_test COMMAND numa_pool_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)
|
||||
|
||||
+9
-3
@@ -5,6 +5,8 @@
|
||||
#
|
||||
# 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 build -t strata:src-cuda12 --build-arg CUDA_IMAGE=nvidia/cuda:12.9.1-devel-ubuntu24.04 \
|
||||
# --build-arg CUDA_ARCHITECTURES="60;61;70;75;80;86;89" --build-arg BUILD_VISION=0 . # GTX 10 / P100 / V100 (driver 575+)
|
||||
# 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
|
||||
#
|
||||
@@ -35,7 +37,9 @@ 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}'); \
|
||||
sm60=OFF; for a in $(echo "$CUDA_ARCHITECTURES" | tr ';,' ' '); do if [ "$a" -lt 75 ]; then sm60=ON; fi; done; \
|
||||
/venv/bin/cmake -G Ninja -S . -B build -DCMAKE_BUILD_TYPE=Release -DSTRATA_ENABLE_CUDA=ON -DSTRATA_BUILD_TESTS=OFF \
|
||||
-DSTRATA_EXPERIMENTAL_SM60=$sm60 \
|
||||
-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)"; \
|
||||
@@ -50,19 +54,21 @@ RUN set -e; \
|
||||
[ -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; \
|
||||
cudafull=$(nvcc --version | sed -n 's/.*release \([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' | head -1); cudamajor=${cudafull%%.*}; \
|
||||
if [ "$cudamajor" = 13 ]; then mindriver=580.65; else mindriver=575.51; fi; \
|
||||
echo "{\"version\": \"$ver\", \"source\": \"docker build\", \"archs\": \"$CUDA_ARCHITECTURES\", \"cuda\": \"$cudafull\", \"portable\": true}" > /stage/BUILD.json; \
|
||||
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*' \
|
||||
--min-driver nvidia=$mindriver --variant cuda$cudamajor --platform linux-x86_64 --build-id "docker-source-${ver}-cuda${cudafull}" --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
|
||||
|
||||
# ---- 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/*
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends python3 ca-certificates git && rm -rf /var/lib/apt/lists/*
|
||||
COPY . /src/
|
||||
RUN python3 /src/packaging/package_terminal.py --out /out --cache /cache \
|
||||
&& mkdir -p /opt/strata \
|
||||
|
||||
+14
-14
@@ -33,7 +33,7 @@ Bilder und arbeitet mit deinen Apps und Coding-Agents zusammen. Deine Chats und
|
||||
- **Kostenlos.** MIT-Lizenz, kein Konto, keine Cloud.
|
||||
- **Privat.** Keine Telemetrie. Was Strata herunterlädt, wann es das Internet nutzt und wie es Downloads prüft, steht in [docs/SAFETY.md](docs/SAFETY.md).
|
||||
|
||||
Version 0.2.1 · Engine 0.1.46
|
||||
Version 0.2.2 · Engine 0.1.47
|
||||
|
||||
## Was du brauchst
|
||||
|
||||
@@ -73,16 +73,16 @@ 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) |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, Treiber 580 oder neuer | [`Strata-0.2.2-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda13.exe) |
|
||||
| | NVIDIA, ältere Karte oder Treiber älter als 580 | [`Strata-0.2.2-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda12.exe) |
|
||||
| | AMD Radeon RX 9000 Serie oder Radeon AI PRO R9700 | [`Strata-0.2.2-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip.exe) |
|
||||
| | AMD Radeon RX 7000 Serie oder Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, Treiber 580 oder neuer | [`Strata-0.2.2-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, ältere Karte oder Treiber älter als 580 | [`Strata-0.2.2-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon RX 9000 Serie oder Radeon AI PRO R9700 | [`Strata-0.2.2-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon RX 7000 Serie | [`Strata-0.2.2-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 oder neuer) | eingebaut | [`Strata-0.2.2-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-mac.dmg) |
|
||||
|
||||
**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
|
||||
@@ -162,9 +162,9 @@ Größen, mehr oder weniger stark komprimiert. Kleinere Größen sind schneller.
|
||||
| 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. |
|
||||
| **Gyro-M** | 35 GB | 92,0 GB | Ungefähr die Qualität von IQ3_XXS bei etwa 20 % weniger GPU-Speicher. Es schreibt Antworten langsamer als Q2_0: 16 % auf einer RTX 3060 mit 12 GB, 21 % auf einer R9700. |
|
||||
|
||||
Gyro läuft auf NVIDIA- und AMD-Karten (bei AMD experimentell), nicht auf Intel Arc und nicht auf dem Mac. Details und Messwerte:
|
||||
Gyro läuft auf NVIDIA- und AMD-Karten (bei AMD experimentell) auf Intel-Arc-Karten unter Linux (getestet auf einer Arc A750) und ab 0.2.2 auf Macs mit Apple Silicon (Gyro-S auf einem Mac mini mit 32 GB: 9 Tokens/s, die Experten werden von der SSD gelesen). Details und Messwerte:
|
||||
[docs/GYRO.md](docs/GYRO.md). Starte es mit `strata launch --model gyro-s`.
|
||||
|
||||
### Die anderen Größen
|
||||
@@ -172,7 +172,7 @@ Gyro läuft auf NVIDIA- und AMD-Karten (bei AMD experimentell), nicht auf Intel
|
||||
| Dein Speicher (RAM + VRAM) | Nimm | Hinweise |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Gyro-S**, oder den **[Coder](docs/MODELS.md#coder)** für Code | Der Coder braucht 32 GB RAM und behält die Hälfte der Experten, deshalb ist er außerhalb von Code schwächer |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), oder **Gyro-M** (35 GB) | Swift denkt kürzer nach, bevor es antwortet. Gyro-M kommt der Qualität von IQ3_XXS nahe, schreibt Antworten aber 17-21 % langsamer als Q2_0 |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), oder **Gyro-M** (35 GB) | Swift denkt kürzer nach, bevor es antwortet. Gyro-M kommt der Qualität von IQ3_XXS nahe, schreibt Antworten aber 16-21 % langsamer als Q2_0 |
|
||||
| **48 GB** | **IQ3_XXS** (47,0 GB) | Langsamer, besser |
|
||||
| **64 GB** | **IQ3_S** (54,8 GB), das Original oder Swift 1.5 | Am langsamsten, am besten. Es erreicht in den veröffentlichten Tests das volle Modell |
|
||||
| **96 GB oder mehr** | **UD-IQ4_XS** von Unsloth (etwa 72 GB RAM) bis **Q8_0** (etwa 136 GB), 4 bis 8 Bit | Näher am vollen Modell. Bei weniger RAM liest Strata einen Teil von der SSD, das ist viel langsamer |
|
||||
|
||||
+14
-14
@@ -33,7 +33,7 @@ con tus aplicaciones y tus agentes de programación. Tus chats y tus archivos se
|
||||
- **Gratis.** Licencia MIT, sin cuenta, sin nube.
|
||||
- **Privado.** Sin telemetría. Lo que Strata descarga, cuándo usa internet y cómo comprueba las descargas está en [docs/SAFETY.md](docs/SAFETY.md).
|
||||
|
||||
Versión 0.2.1 · motor 0.1.46
|
||||
Versión 0.2.2 · motor 0.1.47
|
||||
|
||||
## Lo que necesitas
|
||||
|
||||
@@ -73,16 +73,16 @@ 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) |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, controlador 580 o posterior | [`Strata-0.2.2-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda13.exe) |
|
||||
| | NVIDIA, tarjeta más antigua o controlador anterior a 580 | [`Strata-0.2.2-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda12.exe) |
|
||||
| | AMD Radeon serie RX 9000 o Radeon AI PRO R9700 | [`Strata-0.2.2-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip.exe) |
|
||||
| | AMD Radeon serie RX 7000 o Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, controlador 580 o posterior | [`Strata-0.2.2-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, tarjeta más antigua o controlador anterior a 580 | [`Strata-0.2.2-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon serie RX 9000 o Radeon AI PRO R9700 | [`Strata-0.2.2-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon serie RX 7000 | [`Strata-0.2.2-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 o posterior) | integrada | [`Strata-0.2.2-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-mac.dmg) |
|
||||
|
||||
**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
|
||||
@@ -162,9 +162,9 @@ comprimido. Los tamaños pequeños son más rápidos. Los grandes son algo más
|
||||
| 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. |
|
||||
| **Gyro-M** | 35 GB | 92,0 GB | Calidad parecida a IQ3_XXS con un 20 % menos de memoria de GPU. Escribe las respuestas un 16 % más despacio que Q2_0 en una RTX 3060 de 12 GB y un 21 % más despacio en una R9700. |
|
||||
|
||||
Gyro funciona en tarjetas NVIDIA y AMD (en AMD es experimental), no en Intel Arc ni en el Mac. Detalles y mediciones:
|
||||
Gyro funciona en tarjetas NVIDIA y AMD (en AMD es experimental) en tarjetas Intel Arc con Linux (probado en una Arc A750) y, desde 0.2.2, en Mac con Apple Silicon (Gyro-S en un Mac mini de 32 GB: 9 tokens/s, con los expertos leídos del SSD). Detalles y mediciones:
|
||||
[docs/GYRO.md](docs/GYRO.md). Arráncalo con `strata launch --model gyro-s`.
|
||||
|
||||
### Los demás tamaños
|
||||
@@ -172,7 +172,7 @@ Gyro funciona en tarjetas NVIDIA y AMD (en AMD es experimental), no en Intel Arc
|
||||
| Tu memoria (RAM + VRAM) | Elige | Notas |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Gyro-S**, o el **[Coder](docs/MODELS.md#coder)** para código | El Coder necesita 32 GB de RAM y conserva la mitad de los expertos, así que es más flojo fuera del código |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), o **Gyro-M** (35 GB) | Swift piensa menos tiempo antes de responder. Gyro-M se acerca a la calidad de IQ3_XXS, pero escribe las respuestas un 17-21 % más despacio que Q2_0 |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), o **Gyro-M** (35 GB) | Swift piensa menos tiempo antes de responder. Gyro-M se acerca a la calidad de IQ3_XXS, pero escribe las respuestas un 16-21 % más despacio que Q2_0 |
|
||||
| **48 GB** | **IQ3_XXS** (47,0 GB) | Más lento, mejor |
|
||||
| **64 GB** | **IQ3_S** (54,8 GB), el original o Swift 1.5 | El más lento, el mejor. Iguala al modelo completo en las pruebas publicadas |
|
||||
| **96 GB o más** | **UD-IQ4_XS** de Unsloth (unos 72 GB de RAM) hasta **Q8_0** (unos 136 GB), de 4 a 8 bits | Más cerca del modelo completo. Con menos RAM, Strata lee una parte desde el SSD, que es mucho más lento |
|
||||
|
||||
+14
-14
@@ -33,7 +33,7 @@ fonctionne avec vos applications et vos agents de code. Vos conversations et vos
|
||||
- **Gratuit.** Licence MIT, pas de compte, pas de cloud.
|
||||
- **Privé.** Aucune télémétrie. Ce que Strata télécharge, quand il utilise internet et comment il vérifie les téléchargements est expliqué dans [docs/SAFETY.md](docs/SAFETY.md).
|
||||
|
||||
Version 0.2.1 · moteur 0.1.46
|
||||
Version 0.2.2 · moteur 0.1.47
|
||||
|
||||
## Ce qu'il vous faut
|
||||
|
||||
@@ -73,16 +73,16 @@ 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) |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, pilote 580 ou plus récent | [`Strata-0.2.2-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda13.exe) |
|
||||
| | NVIDIA, carte plus ancienne ou pilote antérieur à 580 | [`Strata-0.2.2-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda12.exe) |
|
||||
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.2-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip.exe) |
|
||||
| | AMD Radeon série RX 7000 ou Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, pilote 580 ou plus récent | [`Strata-0.2.2-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, carte plus ancienne ou pilote antérieur à 580 | [`Strata-0.2.2-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.2-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon série RX 7000 | [`Strata-0.2.2-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 ou plus récent) | intégrée | [`Strata-0.2.2-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-mac.dmg) |
|
||||
|
||||
**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
|
||||
@@ -163,9 +163,9 @@ plus ou moins compressé. Les petites tailles sont plus rapides. Les grandes son
|
||||
| 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. |
|
||||
| **Gyro-M** | 35 Go | 92,0 Go | À peu près la qualité d'IQ3_XXS avec environ 20 % de mémoire GPU en moins. Il écrit les réponses 16 % plus lentement que Q2_0 sur une RTX 3060 de 12 Go et 21 % plus lentement sur une R9700. |
|
||||
|
||||
Gyro fonctionne sur les cartes NVIDIA et AMD (expérimental sur AMD), pas sur Intel Arc ni sur Mac. Détails et mesures :
|
||||
Gyro fonctionne sur les cartes NVIDIA et AMD (expérimental sur AMD) sur les cartes Intel Arc sous Linux (testé sur une Arc A750) et, depuis la 0.2.2, sur les Mac Apple Silicon (Gyro-S sur un Mac mini de 32 Go : 9 tokens/s, experts lus sur le SSD). Détails et mesures :
|
||||
[docs/GYRO.md](docs/GYRO.md). Lancez-le avec `strata launch --model gyro-s`.
|
||||
|
||||
### Les autres tailles
|
||||
@@ -173,7 +173,7 @@ Gyro fonctionne sur les cartes NVIDIA et AMD (expérimental sur AMD), pas sur In
|
||||
| Votre mémoire (RAM + VRAM) | Prenez | Remarques |
|
||||
| --- | --- | --- |
|
||||
| **32 Go** | **Gyro-S**, ou le **[Coder](docs/MODELS.md#coder)** pour le code | Le Coder demande 32 Go de RAM et garde la moitié des experts, il est donc moins bon hors du code |
|
||||
| **40 Go** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 Go), ou **Gyro-M** (35 Go) | Swift réfléchit moins longtemps avant de répondre. Gyro-M approche la qualité d'IQ3_XXS, mais il écrit les réponses 17 à 21 % plus lentement que Q2_0 |
|
||||
| **40 Go** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 Go), ou **Gyro-M** (35 Go) | Swift réfléchit moins longtemps avant de répondre. Gyro-M approche la qualité d'IQ3_XXS, mais il écrit les réponses 16 à 21 % plus lentement que Q2_0 |
|
||||
| **48 Go** | **IQ3_XXS** (47,0 Go) | Plus lent, meilleur |
|
||||
| **64 Go** | **IQ3_S** (54,8 Go), l'original ou Swift 1.5 | Le plus lent, le meilleur. Il égale le modèle complet dans les tests publiés |
|
||||
| **96 Go ou plus** | **UD-IQ4_XS** d'Unsloth (environ 72 Go de RAM) jusqu'à **Q8_0** (environ 136 Go), 4 à 8 bits | Plus proche du modèle complet. Avec moins de RAM, Strata lit une partie depuis le SSD, ce qui est beaucoup plus lent |
|
||||
|
||||
+14
-14
@@ -33,7 +33,7 @@ Strata は **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next
|
||||
- **無料です。** MIT ライセンス。アカウントもクラウドも不要です。
|
||||
- **プライバシーを守ります。** テレメトリはありません。Strata が何をダウンロードするか、いつインターネットを使うか、ダウンロードをどう検証するかは [docs/SAFETY.md](docs/SAFETY.md) に書いてあります。
|
||||
|
||||
バージョン 0.2.1 · エンジン 0.1.46
|
||||
バージョン 0.2.2 · エンジン 0.1.47
|
||||
|
||||
## 必要な環境
|
||||
|
||||
@@ -72,16 +72,16 @@ Strata は **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-Next
|
||||
|
||||
| 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) |
|
||||
| **Windows 10 / 11** | NVIDIA RTX、ドライバー 580 以降 | [`Strata-0.2.2-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda13.exe) |
|
||||
| | NVIDIA、古いカード、またはドライバーが 580 より古い場合 | [`Strata-0.2.2-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda12.exe) |
|
||||
| | AMD Radeon RX 9000 シリーズまたは Radeon AI PRO R9700 | [`Strata-0.2.2-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip.exe) |
|
||||
| | AMD Radeon RX 7000 シリーズまたは Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX、ドライバー 580 以降 | [`Strata-0.2.2-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA、古いカード、またはドライバーが 580 より古い場合 | [`Strata-0.2.2-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon RX 9000 シリーズまたは Radeon AI PRO R9700 | [`Strata-0.2.2-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon RX 7000 シリーズ | [`Strata-0.2.2-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac**(Apple Silicon、macOS 15 以降) | 内蔵 | [`Strata-0.2.2-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-mac.dmg) |
|
||||
|
||||
**迷ったときは:** NVIDIA の場合、ドライバーがバージョン 580 以降なら `cuda13`、それより古ければ `cuda12` を選びます。`cuda12` は古いカードと古いドライバー向けです。
|
||||
`hip` と `amd` のファイルは AMD のカード用です。それ以外の AMD カード(RX 6000)には、まだ用意されたアプリがありません。
|
||||
@@ -156,9 +156,9 @@ AI ツールは [MCP サーバー](docs/MCP_SERVER.md)を通じて、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%遅くなります。 |
|
||||
| **Gyro-M** | 35 GB | 92.0 GB | 品質は IQ3_XXS 程度で、GPUメモリの使用量は約20%少なくなります。回答の書き出しは Q2_0 より、12 GBの RTX 3060 で16%、R9700 で21%遅くなります。 |
|
||||
|
||||
Gyro は NVIDIA と AMD のカードで動きます(AMD は実験的)。Intel Arc と Mac では動きません。詳細と測定結果は
|
||||
Gyro は NVIDIA と AMD のカード(AMD は実験的)、および Linux 上の Intel Arc カード(Arc A750 で確認)で動きます。0.2.2 からは Apple Silicon の Mac でも動きます(32 GB の Mac mini で Gyro-S は 9 トークン/秒、エキスパートは SSD から読み込み)。詳細と測定結果は
|
||||
[docs/GYRO.md](docs/GYRO.md) にあります。`strata launch --model gyro-s` で起動します。
|
||||
|
||||
### ほかのサイズ
|
||||
@@ -166,7 +166,7 @@ Gyro は NVIDIA と AMD のカードで動きます(AMD は実験的)。Inte
|
||||
| メモリ(RAM + VRAM) | 選ぶモデル | 備考 |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Gyro-S**、またはコード向けの **[Coder](docs/MODELS.md#coder)** | Coder はRAM 32 GBが必要で、エキスパートを半分だけ保持するため、コード以外は弱くなります |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS(39.2 GB)、または **Gyro-M**(35 GB) | Swift は答える前に考える時間が短くなります。Gyro-M は品質が IQ3_XXS に近いものの、回答の書き出しは Q2_0 より17〜21%遅くなります |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS(39.2 GB)、または **Gyro-M**(35 GB) | Swift は答える前に考える時間が短くなります。Gyro-M は品質が IQ3_XXS に近いものの、回答の書き出しは Q2_0 より16〜21%遅くなります |
|
||||
| **48 GB** | **IQ3_XXS**(47.0 GB) | 遅くなりますが、品質は上がります |
|
||||
| **64 GB** | **IQ3_S**(54.8 GB)、オリジナルまたは Swift 1.5 | いちばん遅く、いちばん高品質です。公開されているテストでは完全版と同等です |
|
||||
| **96 GB 以上** | Unsloth の **UD-IQ4_XS**(RAM 約72 GB)から **Q8_0**(約136 GB)まで、4〜8ビット | 完全版に近づきます。RAM が足りないと、Strata は一部を SSD から読むため、ずっと遅くなります |
|
||||
|
||||
@@ -33,7 +33,7 @@ agents. Your chats and files stay on your PC.
|
||||
- **Free.** MIT license, no account, no cloud.
|
||||
- **Private.** No telemetry. What Strata downloads, when it uses the internet and how it checks downloads is in [docs/SAFETY.md](docs/SAFETY.md).
|
||||
|
||||
Version 0.2.1 · engine 0.1.46
|
||||
Version 0.2.2 · engine 0.1.47
|
||||
|
||||
## What you need
|
||||
|
||||
@@ -73,20 +73,21 @@ 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) |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, driver 580 or newer | [`Strata-0.2.2-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda13.exe) |
|
||||
| | NVIDIA, older card or driver older than 580 | [`Strata-0.2.2-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda12.exe) |
|
||||
| | AMD Radeon RX 9000 series or Radeon AI PRO R9700 | [`Strata-0.2.2-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip.exe) |
|
||||
| | AMD Radeon RX 7000 series or Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, driver 580 or newer | [`Strata-0.2.2-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, older card or driver older than 580 | [`Strata-0.2.2-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon RX 9000 series or Radeon AI PRO R9700 | [`Strata-0.2.2-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon RX 7000 series | [`Strata-0.2.2-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 or newer) | built in | [`Strata-0.2.2-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-mac.dmg) |
|
||||
|
||||
**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).
|
||||
for older cards and older drivers. The `hip` and `amd` files are for AMD cards. RX 6000 cards have no installer of
|
||||
their own: install `hip-rdna3` (Windows) or `amd-rx7900` (Linux), and on a PC with an RX 6000 card it fetches the RX 6000 engine by itself
|
||||
(also `strata update`). That engine is built but not yet tested on a real RX 6000 card: see [AMD cards](docs/AMD_HIP.md).
|
||||
|
||||
Then:
|
||||
|
||||
@@ -157,17 +158,19 @@ Smaller sizes are faster. Larger sizes are a bit smarter.
|
||||
| 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. |
|
||||
| **Gyro-M** | 35 GB | 92.0 GB | About IQ3_XXS quality with about 20% less GPU memory. It writes answers 16% (12 GB RTX 3060) to 21% (R9700) slower than Q2_0. |
|
||||
|
||||
Gyro runs on NVIDIA and AMD cards (AMD is experimental), not on Intel Arc or the Mac. Details and measurements:
|
||||
Gyro runs on NVIDIA and AMD cards (AMD is experimental) on Intel Arc cards on Linux (tested on an Arc A750) and, from 0.2.2, on Apple Silicon Macs (Gyro-S on a 32 GB Mac mini: 9 tok/s, streaming from the SSD; [docs/MAC.md](docs/MAC.md#gyro-s-and-gyro-m-agentionais-trellis-experts)). Details and measurements:
|
||||
[docs/GYRO.md](docs/GYRO.md). Start it with `strata launch --model gyro-s`.
|
||||
|
||||
### The other sizes
|
||||
|
||||
The tiers count your RAM plus your VRAM, less 5 GB that Windows and the system keep for themselves. Every other size stays a choice.
|
||||
|
||||
| Your memory (RAM + VRAM) | Take | Notes |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Gyro-S**, or the **[Coder](docs/MODELS.md#coder)** for code | The Coder needs 32 GB of RAM and keeps half of the experts, so it is weaker outside code |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39.2 GB), or **Gyro-M** (35 GB) | Swift thinks for a shorter time before it answers. Gyro-M is close to IQ3_XXS in quality, but it writes answers 17-21% slower than Q2_0 |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39.2 GB), or **Gyro-M** (35 GB) | Swift thinks for a shorter time before it answers. Gyro-M is close to IQ3_XXS in quality, but it writes answers 16-21% slower than Q2_0 |
|
||||
| **48 GB** | **IQ3_XXS** (47.0 GB) | Slower, better |
|
||||
| **64 GB** | **IQ3_S** (54.8 GB), the original or Swift 1.5 | The slowest, the best: it matches the full model on the published tests |
|
||||
| **96 GB or more** | Unsloth's **UD-IQ4_XS** (about 72 GB of RAM) to **Q8_0** (about 136 GB), 4 to 8 bit | Closer to the full model. With less RAM, Strata reads part of them from the SSD, which is much slower |
|
||||
|
||||
+14
-14
@@ -33,7 +33,7 @@ agentes de programação. Suas conversas e arquivos ficam no seu PC.
|
||||
- **Gratuito.** Licença MIT, sem conta, sem nuvem.
|
||||
- **Privado.** Sem telemetria. O que o Strata baixa, quando usa a internet e como confere os downloads está em [docs/SAFETY.md](docs/SAFETY.md).
|
||||
|
||||
Versão 0.2.1 · mecanismo 0.1.46
|
||||
Versão 0.2.2 · mecanismo 0.1.47
|
||||
|
||||
## O que você precisa
|
||||
|
||||
@@ -73,16 +73,16 @@ 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) |
|
||||
| **Windows 10 / 11** | NVIDIA RTX, driver 580 ou mais novo | [`Strata-0.2.2-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda13.exe) |
|
||||
| | NVIDIA, placa mais antiga ou driver anterior ao 580 | [`Strata-0.2.2-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda12.exe) |
|
||||
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.2-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip.exe) |
|
||||
| | AMD Radeon série RX 7000 ou Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX, driver 580 ou mais novo | [`Strata-0.2.2-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA, placa mais antiga ou driver anterior ao 580 | [`Strata-0.2.2-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon série RX 9000 ou Radeon AI PRO R9700 | [`Strata-0.2.2-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon série RX 7000 | [`Strata-0.2.2-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac** (Apple Silicon, macOS 15 ou mais novo) | integrada | [`Strata-0.2.2-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-mac.dmg) |
|
||||
|
||||
**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).
|
||||
@@ -156,9 +156,9 @@ O [Gyro](https://huggingface.co/agentionai/Qwen3.8-Flash-Next-Gyro-GGUF) (da age
|
||||
| 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. |
|
||||
| **Gyro-M** | 35 GB | 92,0 GB | Qualidade próxima à do IQ3_XXS, com cerca de 20% menos memória da GPU. Ele escreve as respostas 16% mais devagar que o Q2_0 numa RTX 3060 de 12 GB e 21% mais devagar numa R9700. |
|
||||
|
||||
O Gyro roda em placas NVIDIA e AMD (AMD é experimental), e não roda em Intel Arc nem no Mac. Detalhes e medições:
|
||||
O Gyro roda em placas NVIDIA e AMD (AMD é experimental) em placas Intel Arc no Linux (testado numa Arc A750) e, desde a 0.2.2, em Macs com Apple Silicon (Gyro-S num Mac mini de 32 GB: 9 tokens/s, com os especialistas lidos do SSD). Detalhes e medições:
|
||||
[docs/GYRO.md](docs/GYRO.md). Inicie com `strata launch --model gyro-s`.
|
||||
|
||||
### Os outros tamanhos
|
||||
@@ -166,7 +166,7 @@ O Gyro roda em placas NVIDIA e AMD (AMD é experimental), e não roda em Intel A
|
||||
| Sua memória (RAM + VRAM) | Escolha | Observações |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Gyro-S**, ou o **[Coder](docs/MODELS.md#coder)** para código | O Coder precisa de 32 GB de RAM e mantém metade dos experts, então é mais fraco fora de código |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), ou **Gyro-M** (35 GB) | O Swift pensa por menos tempo antes de responder. O Gyro-M chega perto do IQ3_XXS em qualidade, mas escreve as respostas 17 a 21% mais devagar que o Q2_0 |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS (39,2 GB), ou **Gyro-M** (35 GB) | O Swift pensa por menos tempo antes de responder. O Gyro-M chega perto do IQ3_XXS em qualidade, mas escreve as respostas 16 a 21% mais devagar que o Q2_0 |
|
||||
| **48 GB** | **IQ3_XXS** (47,0 GB) | Mais lento, melhor |
|
||||
| **64 GB** | **IQ3_S** (54,8 GB), o original ou o Swift 1.5 | O mais lento, o melhor: empata com o modelo completo nos testes publicados |
|
||||
| **96 GB ou mais** | **UD-IQ4_XS** da Unsloth (cerca de 72 GB de RAM) até **Q8_0** (cerca de 136 GB), de 4 a 8 bits | Mais perto do modelo completo. Com menos RAM, o Strata lê parte deles do SSD, o que é bem mais lento |
|
||||
|
||||
+14
-14
@@ -33,7 +33,7 @@ Strata 运行 **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-N
|
||||
- **免费。** MIT 许可证,不用注册账号,不走云端。
|
||||
- **隐私。** 没有遥测。Strata 下载什么、什么时候联网、如何校验下载内容,都写在 [docs/SAFETY.md](docs/SAFETY.md)。
|
||||
|
||||
版本 0.2.1 · 引擎 0.1.46
|
||||
版本 0.2.2 · 引擎 0.1.47
|
||||
|
||||
## 硬件要求
|
||||
|
||||
@@ -72,16 +72,16 @@ Strata 运行 **[Qwen3.8-Flash-Next](https://huggingface.co/Qwen/Qwen3.8-Flash-N
|
||||
|
||||
| 系统 | 你的显卡 | 要下载的文件 |
|
||||
| --- | --- | --- |
|
||||
| **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) |
|
||||
| **Windows 10 / 11** | NVIDIA RTX,驱动 580 或更新 | [`Strata-0.2.2-windows-cuda13.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda13.exe) |
|
||||
| | NVIDIA,较旧的显卡,或驱动低于 580 | [`Strata-0.2.2-windows-cuda12.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-cuda12.exe) |
|
||||
| | AMD Radeon RX 9000 系列或 Radeon AI PRO R9700 | [`Strata-0.2.2-windows-hip.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip.exe) |
|
||||
| | AMD Radeon RX 7000 系列或 Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-windows-hip-rdna3.exe`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-windows-hip-rdna3.exe) |
|
||||
| **Linux** | NVIDIA RTX,驱动 580 或更新 | [`Strata-0.2.2-linux-nvidia-cuda13.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda13.AppImage) |
|
||||
| | NVIDIA,较旧的显卡,或驱动低于 580 | [`Strata-0.2.2-linux-nvidia-cuda12.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-nvidia-cuda12.AppImage) |
|
||||
| | AMD Radeon RX 9000 系列或 Radeon AI PRO R9700 | [`Strata-0.2.2-linux-amd-rx9070.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx9070.AppImage) |
|
||||
| | AMD Radeon RX 7000 系列 | [`Strata-0.2.2-linux-amd-rx7900.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-rx7900.AppImage) |
|
||||
| | AMD Ryzen AI Max (Strix Halo) | [`Strata-0.2.2-linux-amd-strixhalo.AppImage`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-linux-amd-strixhalo.AppImage) |
|
||||
| **Mac**(Apple 芯片,macOS 15 及以上) | 内置 | [`Strata-0.2.2-mac.dmg`](https://github.com/Niko1221/Strata/releases/latest/download/Strata-0.2.2-mac.dmg) |
|
||||
|
||||
**不确定时:** NVIDIA 显卡的驱动版本在 580 及以上就选 `cuda13`,否则选 `cuda12`。`cuda12` 适用于较旧的显卡和驱动。
|
||||
`hip` 和 `amd` 的文件是给 AMD 显卡用的。其他 AMD 显卡(RX 6000)目前还没有现成的应用,
|
||||
@@ -155,9 +155,9 @@ Set up Strata on this PC for me: https://github.com/Niko1221/Strata - follow doc
|
||||
| 尺寸 | 所需内存(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%。 |
|
||||
| **Gyro-M** | 35 GB | 92.0 GB | 质量约相当于 IQ3_XXS,显存占用少约 20%。输出回答比 Q2_0 慢,12 GB 的 RTX 3060 上慢 16%,R9700 上慢 21%。 |
|
||||
|
||||
Gyro 可在 NVIDIA 和 AMD 显卡上运行(AMD 为实验性支持),不支持 Intel Arc 和 Mac。详情和测试数据:
|
||||
Gyro 可在 NVIDIA 和 AMD 显卡(AMD 为实验性支持)以及 Linux 上的 Intel Arc 显卡(已在 Arc A750 上测试)上运行,自 0.2.2 起也可在 Apple Silicon 的 Mac 上运行(32 GB 的 Mac mini 上 Gyro-S 为 9 token/秒,专家权重从 SSD 读取)。详情和测试数据:
|
||||
[docs/GYRO.md](docs/GYRO.md)。用 `strata launch --model gyro-s` 启动。
|
||||
|
||||
### 其他尺寸
|
||||
@@ -165,7 +165,7 @@ Gyro 可在 NVIDIA 和 AMD 显卡上运行(AMD 为实验性支持),不支
|
||||
| 你的内存(RAM + VRAM) | 选择 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| **32 GB** | **Gyro-S**,写代码可选 **[Coder](docs/MODELS.md#coder)** | Coder 需要 32 GB 内存,只保留一半的专家,所以代码以外的能力较弱 |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS(39.2 GB),或 **Gyro-M**(35 GB) | Swift 回答前思考的时间更短。Gyro-M 的质量接近 IQ3_XXS,但输出回答比 Q2_0 慢 17-21% |
|
||||
| **40 GB** | **[Swift 1.5](docs/MODELS.md#swift-15)** IQ2_XS(39.2 GB),或 **Gyro-M**(35 GB) | Swift 回答前思考的时间更短。Gyro-M 的质量接近 IQ3_XXS,但输出回答比 Q2_0 慢 16-21% |
|
||||
| **48 GB** | **IQ3_XXS**(47.0 GB) | 更慢,更好 |
|
||||
| **64 GB** | **IQ3_S**(54.8 GB),原版或 Swift 1.5 | 最慢,最好:在公开测试中与完整模型持平 |
|
||||
| **96 GB 及以上** | Unsloth 的 **UD-IQ4_XS**(约 72 GB 内存)到 **Q8_0**(约 136 GB),4 到 8 位 | 更接近完整模型。内存不足时,Strata 会从固态硬盘读取一部分,速度会慢很多 |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "strata-desktop",
|
||||
"productName": "Strata",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.2",
|
||||
"private": true,
|
||||
"description": "Strata Desktop - local AI, tuned to one model.",
|
||||
"scripts": {
|
||||
|
||||
Generated
+1
-1
@@ -3524,7 +3524,7 @@ checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7"
|
||||
|
||||
[[package]]
|
||||
name = "strata-desktop"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"serde",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "strata-desktop"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
description = "Strata Desktop — local AI, tuned to one model."
|
||||
edition = "2021"
|
||||
rust-version = "1.77"
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "strata",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "strata",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.2",
|
||||
"dependencies": {
|
||||
"html-to-image": "^1.11.13",
|
||||
"katex": "^0.19.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "strata",
|
||||
"private": true,
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.2",
|
||||
"type": "module",
|
||||
"description": "Strata \u2014 local AI, tuned to one model. Web shell + engine host.",
|
||||
"scripts": {
|
||||
|
||||
@@ -28,6 +28,7 @@ import { ChildProjector } from './children.mjs';
|
||||
import { nearestBudget } from './llm-shape.mjs';
|
||||
import { DeliverableTracker, ActivityGroups } from './deliverables.mjs';
|
||||
import { SkillsUsed, skillInfoFrom } from './skills-used.mjs';
|
||||
import { turnEngineSpeed } from './engine-speed.mjs';
|
||||
import { TurnGuard, guardNote, GUARD_NOTES } from './turn-guard.mjs';
|
||||
import { PlanStore, planPrompt, parsePlan, parsePoints, stepObjective, stepLine, firstSentence, stepSummary, planAsPrompt, editedSteps, planView, researchPrompt, researchWritePrompt, RESEARCH_NUDGE, parseSources } from './plan.mjs';
|
||||
|
||||
@@ -228,6 +229,13 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
|
||||
return next;
|
||||
}
|
||||
|
||||
/** The local engine's own speed for this turn's requests (null when it cannot be read: the stream's estimate stays). */
|
||||
async function engineSpeedOf(route, projector) {
|
||||
const inst = localInstances().find(i => i.id === route.instanceId);
|
||||
if (!inst) return null;
|
||||
return turnEngineSpeed({ baseUrl: inst.baseUrl, key: inst.key, since: projector.startedAt, attempts: projector.streams.size, timeoutMs: 800 });
|
||||
}
|
||||
|
||||
/** Make sure a runtime exists that knows the route this turn needs. */
|
||||
async function ensureRuntime(choice, signal) {
|
||||
for (;;) {
|
||||
@@ -429,7 +437,7 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
|
||||
turn.finish(`The assistant stopped answering for ${mins > 1 ? mins + ' minutes' : 'a minute'}, so I ended this turn. Anything it already finished is kept. Send your message again to go on.`);
|
||||
}, Math.min(15_000, Math.max(200, Math.floor(stallMs / 4))));
|
||||
turn.watchdog.unref?.();
|
||||
function finishTurn(failure) {
|
||||
async function finishTurn(failure) {
|
||||
if (turn.finished) return;
|
||||
turn.finished = true;
|
||||
clearTimeout(turn.settleTimer);
|
||||
@@ -450,7 +458,9 @@ export function createAgentBridge({ dataDir, componentsDir = null, bundledDir =
|
||||
}
|
||||
if (noteReason && !extra.some(f => f.type === 'plan' && f.event === 'proposed')) { const note = guardNote(noteReason); if (note) extra.push(note); }
|
||||
for (const f of extra) emit(f);
|
||||
const frames = failure ? [{ type: 'error', message: plainAgentFailure(failure), details: failure, code: 'AGENT_STOPPED' }] : projector.finish({ model: route.name, contextMax: route.contextWindow });
|
||||
// The answer speed of a turn on this computer is the engine's own (tokens it produced over its decode time, as the Monitor shows it).
|
||||
const engine = !failure && route.kind === 'local' ? await engineSpeedOf(route, projector) : null;
|
||||
const frames = failure ? [{ type: 'error', message: plainAgentFailure(failure), details: failure, code: 'AGENT_STOPPED' }] : projector.finish({ model: route.name, contextMax: route.contextWindow, engine });
|
||||
for (const f of frames.slice(0, -1)) emit(f);
|
||||
// Messages typed while the goal ran and not delivered between steps go back to the page now, to be sent as ordinary messages.
|
||||
if (turn.goalRun && attached) {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* The engine's own answer speed for one assistant turn: every token its requests produced (thinking, answer and tool calls; with
|
||||
* MTP drafting only the tokens it kept) over the time they spent decoding. These are the numbers the Monitor and `/metrics` show,
|
||||
* so the chat footer reads the same. Nothing here is a guess from the stream: it returns null when the engine does not say.
|
||||
*/
|
||||
|
||||
/** Sum the engine's finished requests that began at or after `since` (ms). `attempts` is how many model requests the turn made:
|
||||
* when the engine lists more (another chat ran at the same time), only the newest `attempts` are the turn's. */
|
||||
export function sumEngineRequests(requests, { since, attempts = Infinity } = {}) {
|
||||
if (!Array.isArray(requests)) return null;
|
||||
const mine = requests
|
||||
.filter(r => Number.isFinite(r?.time) && r.time * 1000 >= since - 1500)
|
||||
.sort((a, b) => a.time - b.time)
|
||||
.slice(-Math.max(1, attempts));
|
||||
let tokens = 0, decodeMs = 0, promptRead = 0, promptMs = 0;
|
||||
for (const r of mine) {
|
||||
const made = Number.isFinite(r.engine_generated) ? r.engine_generated : r.output_tokens;
|
||||
if (!(made > 0) || !(r.decode_ms > 0)) continue;
|
||||
tokens += made; decodeMs += r.decode_ms;
|
||||
const read = Number.isFinite(r.prompt_read) ? r.prompt_read : (r.prompt_tokens ?? 0) - (r.reused ?? 0);
|
||||
if (read > 0 && r.prompt_ms > 0) { promptRead += read; promptMs += r.prompt_ms; }
|
||||
}
|
||||
if (!(tokens > 0 && decodeMs > 0)) return null;
|
||||
return {
|
||||
tokens, decodeMs, tps: Number((tokens / (decodeMs / 1000)).toFixed(1)),
|
||||
promptTps: promptRead > 0 && promptMs > 0 ? Number((promptRead / (promptMs / 1000)).toFixed(1)) : null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Ask a local engine for its request history. Any failure is null: the caller keeps the stream's own estimate. */
|
||||
export async function turnEngineSpeed({ baseUrl, key, since, attempts, fetcher = fetch, timeoutMs = 1500 }) {
|
||||
try {
|
||||
const origin = new URL(baseUrl).origin;
|
||||
const res = await fetcher(`${origin}/metrics?requests=all`, {
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
headers: key ? { authorization: `Bearer ${key}` } : undefined,
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
return sumEngineRequests((await res.json())?.requests, { since, attempts });
|
||||
} catch { return null; }
|
||||
}
|
||||
@@ -395,7 +395,7 @@ export class TurnProjector {
|
||||
}
|
||||
|
||||
/** The terminal frames for a finished turn. */
|
||||
finish({ model, contextMax } = {}) {
|
||||
finish({ model, contextMax, engine = null } = {}) {
|
||||
const reason = this.ended ?? { kind: 'interrupted' };
|
||||
const durationMs = Date.now() - this.startedAt;
|
||||
const usage = this.usageSeen
|
||||
@@ -403,8 +403,15 @@ export class TurnProjector {
|
||||
: { tokensIn: null, tokensOut: null, usageSource: 'unavailable' };
|
||||
const decoded = this.decode();
|
||||
const generated = this.usageSeen && this.tokensOut > 0 ? this.tokensOut : decoded.tokens;
|
||||
const tps = decoded.seconds >= 0.3 && generated > 0 ? Number((generated / decoded.seconds).toFixed(1)) : null;
|
||||
const base = { durationMs, ttftMs: this.firstTokenAt ? this.firstTokenAt - this.startedAt : null, model, contextMax, tps, contextTokens: this.contextTokens };
|
||||
// Answer speed, one definition everywhere: all the tokens the model produced over its decode time. The engine's own figures (the ones the
|
||||
// Monitor shows) when the bridge could read them; otherwise the stream's own span. A very short reply has almost no span, so its decode time
|
||||
// is then the time from its first chunk to the end of the turn (only when no tool ran, so no tool time is in it): the number is still shown.
|
||||
let tps = null;
|
||||
if (engine?.tps > 0) tps = engine.tps;
|
||||
else if (generated > 0 && decoded.seconds >= 0.3) tps = Number((generated / decoded.seconds).toFixed(1));
|
||||
else if (generated > 0 && this.tools.size === 0 && this.firstTokenAt && Date.now() - this.firstTokenAt >= 50) tps = Number((generated / ((Date.now() - this.firstTokenAt) / 1000)).toFixed(1));
|
||||
const promptTps = engine?.promptTps > 0 ? engine.promptTps : null;
|
||||
const base = { durationMs, ttftMs: this.firstTokenAt ? this.firstTokenAt - this.startedAt : null, model, contextMax, tps, ...(promptTps ? { promptTps } : {}), contextTokens: this.contextTokens };
|
||||
switch (reason.kind) {
|
||||
case 'completed':
|
||||
return [{ type: 'done', ...base, ...usage, finishReason: 'stop' }];
|
||||
|
||||
@@ -3,6 +3,8 @@ import { spawn } from 'node:child_process';
|
||||
import { probe, which } from './util.mjs';
|
||||
import { classifyRunning, describeConnector, otherTunnelMessage, ownTunnelMessage } from './cloudflared-find.mjs';
|
||||
|
||||
/** A tunnel without a key is allowed (owner decision, 0.2.2); because the link is public the warning is stronger than the local one. */
|
||||
export const NO_KEY_TUNNEL_WARNING = 'No API key is set, so anyone who finds this link can use your model from the internet. Create an API key to protect it.';
|
||||
export function tunnelHostname(value) {
|
||||
const hostname = String(value ?? '').trim().toLowerCase();
|
||||
if (hostname.length > 253 || !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/.test(hostname)) throw new Error('Enter a hostname such as ai.example.com, without https:// or a path.');
|
||||
@@ -13,7 +15,7 @@ export function createCloudflare({ origin, originUrl, hasKey, changed = () => {}
|
||||
let value = { status: 'idle', mode: null, url: null, error: null };
|
||||
const state = () => {
|
||||
const info = binary ? connectorInfo() : null;
|
||||
return { ...value, installed: !!binary, originUrl, connector: info ? { path: info.path, version: info.version, source: info.source, label: info.label, summary: describeConnector(info) } : null, external: other?.running ? externalState() : null };
|
||||
return { ...value, warning: value.status === 'connected' && !hasKey() ? NO_KEY_TUNNEL_WARNING : null, installed: !!binary, originUrl, connector: info ? { path: info.path, version: info.version, source: info.source, label: info.label, summary: describeConnector(info) } : null, external: other?.running ? externalState() : null };
|
||||
};
|
||||
const update = patch => { value = { ...value, ...patch }; changed(state()); };
|
||||
function externalState(token = '') {
|
||||
@@ -52,7 +54,6 @@ export function createCloudflare({ origin, originUrl, hasKey, changed = () => {}
|
||||
}
|
||||
async function start(body) {
|
||||
if (busy || ['starting', 'connected', 'stopping'].includes(value.status)) throw new Error('Stop the current tunnel before starting another.');
|
||||
if (!hasKey()) throw new Error('Create a Strata API key before connecting Cloudflare.');
|
||||
if (!['quick', 'domain'].includes(body.mode)) throw new Error('Choose a temporary URL or your own domain.');
|
||||
const hostname = body.mode === 'domain' ? tunnelHostname(body.hostname) : null;
|
||||
const token = body.mode === 'domain' ? String(body.token ?? '').trim() : '';
|
||||
|
||||
@@ -207,6 +207,43 @@ export function createControl({ control = findControl(), env = process.env, run
|
||||
if (result.json?.plan && Array.isArray(result.json.plan.issues)) return result.json.plan;
|
||||
throw failure(result, 'The files could not be checked.');
|
||||
},
|
||||
/** What picking `to` as the models folder means (sizes, free space, which choices are possible, why not): nothing is changed. */
|
||||
async planModelsMove(from, to) {
|
||||
const result = await call(['models', 'move', to, ...(from ? ['--from', from] : []), '--plan', '--output', 'json'], { timeoutMs: 120_000 });
|
||||
if (result.json?.plan) return result.json.plan;
|
||||
throw failure(result, 'The folder could not be checked.');
|
||||
},
|
||||
/**
|
||||
* Move the model files to `to` (copied, checked, then the old copies removed). `onEvent` gets the progress lines; a file at `cancelFile` stops it
|
||||
* safely. Resolves with the summary (done / stopped, what moved, what is left); a failure is a ControlError whose `output` carries the details.
|
||||
*/
|
||||
async moveModels(from, to, { installDir = null, cancelFile = null, onEvent = null } = {}) {
|
||||
const args = ['models', 'move', to, ...(from ? ['--from', from] : []), ...(installDir ? ['--install-dir', installDir] : []), ...(cancelFile ? ['--cancel-file', cancelFile] : []), '--output', 'jsonl'];
|
||||
const result = await call(args, { timeoutMs: 24 * 3600_000, onLine: onEvent ?? (() => {}) });
|
||||
if (result.json?.ok && result.json.event === 'result') return result.json;
|
||||
const error = result.json?.error;
|
||||
throw Object.assign(new ControlError(error?.message ?? (result.stderr.split(/\r?\n/).filter(Boolean).pop() || 'The models could not be moved.'), { exitCode: result.code, output: result.json }), { code: error?.code ?? 'move_failed', details: error?.details ?? '' });
|
||||
},
|
||||
/** A move of the models folder that the last run left unfinished (`strata models move --status`): null, or {state, from, to, bytes}. */
|
||||
async interruptedModelsMove(dataFolder) {
|
||||
const result = await call(['models', 'move', '--status', ...(dataFolder ? ['--from', dataFolder] : []), '--output', 'json'], { timeoutMs: 30_000 });
|
||||
if (result.json?.ok) return result.json.interrupted ?? null;
|
||||
throw failure(result, 'The models folder could not be checked.');
|
||||
},
|
||||
/** "Finish it": the same move again, from where it stopped. Same events and result as moveModels. */
|
||||
async resumeModelsMove(dataFolder, { installDir = null, cancelFile = null, onEvent = null } = {}) {
|
||||
const args = ['models', 'move', '--resume', ...(dataFolder ? ['--from', dataFolder] : []), ...(installDir ? ['--install-dir', installDir] : []), ...(cancelFile ? ['--cancel-file', cancelFile] : []), '--output', 'jsonl'];
|
||||
const result = await call(args, { timeoutMs: 24 * 3600_000, onLine: onEvent ?? (() => {}) });
|
||||
if (result.json?.ok && result.json.event === 'result') return result.json;
|
||||
const error = result.json?.error;
|
||||
throw Object.assign(new ControlError(error?.message ?? (result.stderr.split(/\r?\n/).filter(Boolean).pop() || 'The models could not be moved.'), { exitCode: result.code, output: result.json }), { code: error?.code ?? 'move_failed', details: error?.details ?? '' });
|
||||
},
|
||||
/** "Leave it": keep the old folder, remove the partial copies in the new one. */
|
||||
async leaveModelsMove(dataFolder, { installDir = null } = {}) {
|
||||
const result = await call(['models', 'move', '--leave', ...(dataFolder ? ['--from', dataFolder] : []), ...(installDir ? ['--install-dir', installDir] : []), '--output', 'json'], { timeoutMs: 24 * 3600_000 });
|
||||
if (result.json?.ok) return result.json;
|
||||
throw Object.assign(failure(result, 'The partial copies could not be removed.'), { code: result.json?.error?.code ?? 'leave_failed', details: result.json?.error?.details ?? '' });
|
||||
},
|
||||
async saveProfile(name, configPath) {
|
||||
const listed = await call(['profile', 'list', '--output', 'json'], { timeoutMs: 20_000 });
|
||||
const known = (listed.json?.profiles ?? []).some(p => p.id === name);
|
||||
|
||||
@@ -182,13 +182,16 @@ export function chooserUnavailable(picked) {
|
||||
return { message: picked.reason, code: 'folder-chooser-unavailable', details: picked.detail };
|
||||
}
|
||||
|
||||
/** A typed path stands in for the dialog: it must be an absolute directory that exists. */
|
||||
export async function typedFolder(value, { isAbsolute }) {
|
||||
const text = typeof value === 'string' ? value.trim() : '';
|
||||
/** A typed path stands in for the dialog: it must be an absolute directory that exists. `fileType` (a RegExp on the name) also lets a
|
||||
* file of that kind through (the model page takes a folder or one .gguf file). Quotes around the path (Windows "Copy as path") go. */
|
||||
export async function typedFolder(value, { isAbsolute, fileType = null }) {
|
||||
let text = typeof value === 'string' ? value.trim() : '';
|
||||
if (text.length > 1 && /^(["'])[\s\S]*\1$/.test(text)) text = text.slice(1, -1).trim();
|
||||
if (!text || text.includes('\0')) return null;
|
||||
if (!isAbsolute(text)) throw new Error('Enter the full path of the folder.');
|
||||
if (!isAbsolute(text)) throw new Error(fileType ? 'Enter the full path of the folder or the file.' : 'Enter the full path of the folder.');
|
||||
let info;
|
||||
try { info = await stat(text); } catch { throw new Error('That folder does not exist.'); }
|
||||
if (!info.isDirectory()) throw new Error('Choose a folder, not a file.');
|
||||
try { info = await stat(text); } catch { throw new Error(fileType ? 'That folder or file does not exist.' : 'That folder does not exist.'); }
|
||||
if (info.isFile() && fileType?.test(text)) return text;
|
||||
if (!info.isDirectory()) throw new Error(fileType ? 'Choose a folder, or a .gguf file.' : 'Choose a folder, not a file.');
|
||||
return cleanChosenPath(text);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* The host's copy of the roles rules it needs to WRITE a model's settings (src/lib/gpu-roles.ts has the screen's, strata_control/gpu_roles.py the
|
||||
* Terminal's and the server's; tests/data/gpu_roles_vectors.json is run against all three).
|
||||
* Roles are { [card number]: 'main' | 'helper' | 'off' }. The engine takes the cards in stage order: Helpers, then the Main card LAST.
|
||||
*/
|
||||
export const ROLES = ['main', 'helper', 'off'];
|
||||
const mem = c => Number(c.vram_gb) || 0;
|
||||
const speed = c => (typeof c.speed === 'number' && c.speed > 0 ? c.speed : 0);
|
||||
const nums = roles => Object.keys(roles).map(Number).sort((a, b) => a - b);
|
||||
const onList = roles => nums(roles).filter(i => roles[i] !== 'off');
|
||||
const shortName = c => { let n = String(c.name || `GPU ${c.index + 1}`); for (const j of ['NVIDIA GeForce ', 'NVIDIA ', 'GeForce ', 'AMD Radeon ', 'AMD ', 'Intel(R) ', 'Intel ']) if (n.startsWith(j)) { n = n.slice(j.length); break; } return n.trim(); };
|
||||
|
||||
export function parseSaved(value) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
||||
const out = {};
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
if (!/^\d+$/.test(k.trim()) || !ROLES.includes(String(v).toLowerCase())) return null;
|
||||
out[Number(k)] = String(v).toLowerCase();
|
||||
}
|
||||
if (!Object.keys(out).length || Object.values(out).filter(r => r === 'main').length > 1) return null;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function normalize(roles, cards) {
|
||||
const cs = new Map(cards.map(c => [c.index, c]));
|
||||
const out = {};
|
||||
for (const i of nums(roles)) if (cs.has(i)) out[i] = ROLES.includes(roles[i]) ? roles[i] : 'off';
|
||||
const list = onList(out), mains = list.filter(i => out[i] === 'main');
|
||||
if (list.length && !mains.length) {
|
||||
const best = [...list].sort((a, b) => speed(cs.get(b)) - speed(cs.get(a)) || Math.round(mem(cs.get(b))) - Math.round(mem(cs.get(a))) || a - b)[0];
|
||||
out[best] = 'main';
|
||||
}
|
||||
for (const extra of mains.slice(1)) out[extra] = 'helper';
|
||||
if (list.length === 1) out[list[0]] = 'main';
|
||||
return out;
|
||||
}
|
||||
|
||||
export function order(roles, cards) {
|
||||
const cs = new Map(cards.map(c => [c.index, c]));
|
||||
const r = normalize(roles, cards);
|
||||
const helpers = nums(r).filter(i => r[i] === 'helper').sort((a, b) => speed(cs.get(a)) - speed(cs.get(b)) || a - b);
|
||||
return [...helpers, ...nums(r).filter(i => r[i] === 'main')];
|
||||
}
|
||||
|
||||
/** "gpu_roles" plus the legacy mirror ("gpu" Main last, "gpu_order": "as_given"); nothing on: "gpu": [] and "gpu_none": true. */
|
||||
export function mirror(roles, cards) {
|
||||
const r = normalize(roles, cards), lst = order(r, cards);
|
||||
const out = { gpu_roles: Object.fromEntries(nums(r).map(i => [String(i), r[i]])), gpu: [], gpu_cards: {} };
|
||||
if (lst.length) { out.gpu = lst.length === 1 ? lst[0] : lst; out.gpu_order = 'as_given'; } else out.gpu_none = true;
|
||||
for (const c of cards) if (c.index in r) out.gpu_cards[String(c.index)] = [shortName(c), c.bus ?? ''].filter(Boolean).join('|');
|
||||
return out;
|
||||
}
|
||||
@@ -72,6 +72,7 @@ import { randomBytes, timingSafeEqual } from 'node:crypto';
|
||||
import { nativeGateway } from './native-gateway.mjs';
|
||||
import { proxyCompletion } from './inference.mjs';
|
||||
import { apiPath } from './api-paths.mjs';
|
||||
import { findFreePort, listenProblem } from './ports.mjs';
|
||||
import { validateChatImages } from './chat-images.mjs';
|
||||
import { createTimeSkip, measureFolder, timeskipRevisionMessage, imageTypeOf } from './timeskip.mjs';
|
||||
|
||||
@@ -269,7 +270,7 @@ function setupState() {
|
||||
flowVersion: 2,
|
||||
completedAt: s.setup.completedAt,
|
||||
history: s.setup.history ?? [],
|
||||
app: { version: appVersion(), modelsDir: store.MODELS_DIR, dataDir: store.DATA_DIR },
|
||||
app: { version: appVersion(), modelsDir: store.MODELS_DIR, dataDir: store.DATA_DIR, modelsDirNotice: store.MODELS_DIR_NOTICE },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -709,6 +710,7 @@ async function runTurn({ message, history = [], onFrame, signal, client, workspa
|
||||
durationMs: Date.now() - started,
|
||||
ttftMs: firstTokenMs ?? undefined,
|
||||
cachedTokens: result.cachedTokens,
|
||||
tps: result.tps,
|
||||
usageSource: result.usageSource,
|
||||
});
|
||||
if(models.REAL){monitor.setRecordedContext({ ...result, requestId: entry.id, sessionId });monitor.idle();}
|
||||
@@ -908,8 +910,10 @@ const tasks = createTaskService({
|
||||
const taskTimer = setInterval(() => tasks.tick(), 15000);
|
||||
taskTimer.unref();
|
||||
|
||||
const localShare = createLocalShare({ handle: openai, hasKey: () => !!store.get().serving?.apiKey, port: Number(process.env.STRATA_SHARE_PORT ?? PORT + 1), changed: () => serving.setSharing(localShare.state()) });
|
||||
const tunnelPort = Number(process.env.STRATA_TUNNEL_PORT ?? PORT + 2);
|
||||
// The share and tunnel listeners take a free port (the next ones above the host's own) instead of a fixed PORT+1 / PORT+2 that Docker, WSL or Hyper-V may hold or reserve.
|
||||
const sharePort = await findFreePort(Number(process.env.STRATA_SHARE_PORT ?? PORT + 1), '0.0.0.0', { skip: [PORT] });
|
||||
const localShare = createLocalShare({ handle: openai, hasKey: () => !!store.get().serving?.apiKey, port: sharePort, changed: () => serving.setSharing(localShare.state()) });
|
||||
const tunnelPort = await findFreePort(Number(process.env.STRATA_TUNNEL_PORT ?? PORT + 2), '127.0.0.1', { skip: [PORT, sharePort] });
|
||||
const tunnelOrigin = createLocalShare({ handle: openai, hasKey: () => !!store.get().serving?.apiKey, host: '127.0.0.1', port: tunnelPort });
|
||||
const connector = createConnectorInstaller({ directory: join(store.HOME, 'engine', 'cloudflare'), componentDirectory: join(componentsDirectory(), 'cloudflared') });
|
||||
const cloudflare = createCloudflare({ origin: tunnelOrigin, originUrl: `http://127.0.0.1:${tunnelPort}`, hasKey: () => !!store.get().serving?.apiKey, changed: serving.setTunnel, locate: connector.locate, prepareConnector: connector.ensure, connectorInfo: connector.connector, findRunning: findRunningConnector });
|
||||
@@ -962,6 +966,17 @@ async function api(req, res, path, method, url) {
|
||||
if(path==='/api/native'&&method==='GET')return json(res,200,await native.refresh(url.searchParams.get('refresh')==='1'));
|
||||
if(path==='/api/native'&&method==='PATCH')return json(res,200,await native.configure(await readBody(req)));
|
||||
if(path==='/api/native/directory'&&method==='POST')return json(res,200,native.setDownloadDirectory((await readBody(req)).directory));
|
||||
/* Settings > Storage > Models folder: pick (or type) a folder, say what it means, then start the choice. Copying is Control's (strata models move). */
|
||||
if(path==='/api/native/models-folder/plan'&&method==='POST'){
|
||||
const typed=await typedFolder((await readBody(req)).path,{isAbsolute});
|
||||
const picked=typed?{path:typed}:await chooseFolder('Choose a folder for your models');
|
||||
if(picked.unavailable){const u=chooserUnavailable(picked);return error(res,501,u.message,{code:u.code});}
|
||||
if(picked.cancelled)return json(res,200,{cancelled:true});
|
||||
return json(res,200,{plan:await native.planModelsFolder(picked.path)});}
|
||||
if(path==='/api/native/models-folder/start'&&method==='POST'){const body=await readBody(req);const result=await native.startModelsFolder({folder:body.path,choice:body.choice,stopRunning:body.stopRunning===true});return json(res,result.id?202:200,result);}
|
||||
if(path==='/api/native/models-folder/interrupted'&&method==='GET')return json(res,200,{interrupted:await native.interruptedMove()});
|
||||
if(path==='/api/native/models-folder/resume'&&method==='POST'){const body=await readBody(req);return json(res,202,await native.finishInterruptedMove({stopRunning:body.stopRunning===true}));}
|
||||
if(path==='/api/native/models-folder/leave'&&method==='POST')return json(res,200,await native.leaveInterruptedMove());
|
||||
if(path==='/api/native/plan'&&method==='POST')return json(res,200,await native.plan(await readBody(req)));
|
||||
if(path==='/api/native/operations'&&method==='POST'){if(storageChanging)return error(res,409,'Wait for the model folder check to finish.');const body=await readBody(req);return json(res,202,native.operation(body.kind,body.args));}
|
||||
if(path==='/api/native/logs'&&method==='GET')return json(res,200,{logs:native.logs()});
|
||||
@@ -979,7 +994,7 @@ async function api(req, res, path, method, url) {
|
||||
return json(res,200,{path:file});}
|
||||
if(path==='/api/native/local/preview'&&method==='POST'){
|
||||
/* "Use a local GGUF": pick (or type) the folder, and Control says what is in it. Nothing is copied or changed. */
|
||||
const typed=await typedFolder((await readBody(req)).path,{isAbsolute});
|
||||
const typed=await typedFolder((await readBody(req)).path,{isAbsolute,fileType:/\.gguf$/i});
|
||||
const picked=typed?{path:typed}:await chooseFolder('Choose the folder that holds your GGUF files');
|
||||
if(picked.unavailable){const u=chooserUnavailable(picked);return error(res,501,u.message,{code:u.code});};
|
||||
if(picked.cancelled)return json(res,200,{cancelled:true});
|
||||
@@ -1006,7 +1021,7 @@ async function api(req, res, path, method, url) {
|
||||
if(nativeSettingsFile&&method==='POST')return json(res,200,await native.openSettingsFile(nativeSettingsFile[1]));
|
||||
|
||||
// Credentials are handled only on the host. Responses and events expose a key-set flag.
|
||||
return error(res,404,'Unknown native control action.');}catch(e){return error(res,/Finish|Unload|running|current operation/.test(e.message)?409:400,e.message);}}
|
||||
return error(res,404,'Unknown native control action.');}catch(e){return error(res,/Finish|Unload|running|current operation/.test(e.message)?409:400,e.message,{...(e.code&&typeof e.code==='string'?{code:e.code}:{}),...(e.details?{details:String(e.details)}:{})});}}
|
||||
|
||||
if (path === '/api/model-providers' && method === 'GET') return json(res, 200, withPictureAnswers(modelProviders.list()));
|
||||
if (path === '/api/model-providers/discover' && method === 'POST') {
|
||||
@@ -2161,6 +2176,12 @@ async function staticFile(req, res, path) {
|
||||
|
||||
/* ------------------------------------------------------------------ start --- */
|
||||
|
||||
server.on('error', error => {
|
||||
if (server.listening) { console.error(`[strata] ${error.message}`); return; }
|
||||
// a taken or Windows-reserved port: one plain sentence instead of a stack trace
|
||||
console.error(`[strata] ${listenProblem(error, PORT, HOST)}`);
|
||||
process.exit(1);
|
||||
});
|
||||
server.listen(PORT, HOST, () => {
|
||||
if (process.send) process.send({ type: 'strata-ready', port: PORT });
|
||||
// Engines a lost owner left behind (an earlier crash, an older version): ended after a PID + start time + command-line check.
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
import http from 'node:http';
|
||||
import os from 'node:os';
|
||||
import { apiPath } from './api-paths.mjs';
|
||||
import { listenProblem } from './ports.mjs';
|
||||
/** Sharing without a key is allowed (owner decision, 0.2.2): the person is told once, in plain words, and the app keeps offering to make a key. */
|
||||
export const NO_KEY_WARNING = 'No API key is set, so any device on this network can use your model. Create an API key to protect it.';
|
||||
export function createLocalShare({ handle, hasKey, changed = () => {}, host = '0.0.0.0', port = 8792 }) {
|
||||
let listener = null, active = false;
|
||||
const address = () => Object.values(os.networkInterfaces()).flat().find(n => n?.family === 'IPv4' && !n.internal)?.address ?? '127.0.0.1';
|
||||
const state = () => ({ active, url: active ? `http://${host === '127.0.0.1' ? host : address()}:${listener.address().port}/v1` : null });
|
||||
const state = () => ({ active, url: active ? `http://${host === '127.0.0.1' ? host : address()}:${listener.address().port}/v1` : null, warning: active && !hasKey() ? NO_KEY_WARNING : null });
|
||||
async function start() {
|
||||
if (active) return state();
|
||||
if (listener) throw new Error('Sharing is already starting.');
|
||||
if (!hasKey()) throw new Error('Set an API key before sharing with other devices.');
|
||||
listener = http.createServer(async (req, res) => {
|
||||
const path = apiPath(new URL(req.url ?? '/', 'http://localhost').pathname);
|
||||
if (!['/v1/models','/v1/chat/completions','/v1/messages'].includes(path)) { res.writeHead(404); res.end('Not found'); return; }
|
||||
@@ -17,7 +19,7 @@ export function createLocalShare({ handle, hasKey, changed = () => {}, host = '0
|
||||
catch { if (!res.headersSent) res.writeHead(500); res.end('Request failed'); }
|
||||
});
|
||||
try { await new Promise((resolve,reject) => { listener.once('error',reject); listener.listen(port,host,resolve); }); }
|
||||
catch(e) { listener.close(); listener=null; throw e; }
|
||||
catch(e) { listener.close(); listener=null; throw new Error(listenProblem(e, port, host)); }
|
||||
active=true; changed(); return state();
|
||||
}
|
||||
async function stop() {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Where the person's models go: what a picked folder means (0.2.2).
|
||||
*
|
||||
* - A drive on its own (D:\ or /) is never filled with model files: it becomes D:\Strata (the data folder) and D:\Strata\Models (the
|
||||
* model library), which the person is told.
|
||||
* - The data folder is the person's own choice, so it is written into the settings file Strata Control reads (`data_dir`). Control's
|
||||
* installer accepts a data folder that is that setting, whatever the folder is called; for a folder it has not been told about it wants a
|
||||
* name starting with "Strata" (a rule against an AI tool filling an arbitrary folder, tools/strata_mcp.py check_data_dir).
|
||||
* - The link that makes `<data>/models` and `<data>/Models` one folder is only needed where the file system tells them apart; where it cannot be
|
||||
* made (exFAT, FAT, a network share) the models simply go in `<data>/models`.
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { dirname, join, parse, resolve } from 'node:path';
|
||||
|
||||
export const isDriveRoot = path => { const p = resolve(path); return p === parse(p).root; };
|
||||
|
||||
/** The data folder for a picked folder: a drive root gets a `Strata` folder of its own. `{ path, note }`. */
|
||||
export function dataFolderFromPick(picked) {
|
||||
const path = resolve(picked);
|
||||
if (!isDriveRoot(path)) return { path, note: null };
|
||||
const target = join(path, 'Strata');
|
||||
return { path: target, note: `${path} is a whole drive, so the models go in ${target}.` };
|
||||
}
|
||||
|
||||
/** The model library folder for a picked folder: a drive root gets `Strata/Models`. `{ path, note }`. */
|
||||
export function libraryFolderFromPick(picked) {
|
||||
const path = resolve(picked);
|
||||
if (!isDriveRoot(path)) return { path, note: null };
|
||||
const target = join(path, 'Strata', 'Models');
|
||||
return { path: target, note: `${path} is a whole drive, so the models go in ${target}.` };
|
||||
}
|
||||
|
||||
/** Make a folder the person asked for; a refusal of the disk is said in words. */
|
||||
export function makeFolder(path) {
|
||||
try { mkdirSync(path, { recursive: true }); }
|
||||
catch (error) { throw new Error(`Strata could not create ${path} (${error.code === 'EACCES' || error.code === 'EPERM' ? 'access was denied' : error.code === 'ENOSPC' ? 'the drive is full' : error.message}). Choose another folder.`); }
|
||||
return path;
|
||||
}
|
||||
|
||||
/** `models` beside `Models`: a link where the two are different names. A disk that cannot hold the link gets a plain `models` folder instead. */
|
||||
export function alignModels(data, library, { link = symlinkSync } = {}) {
|
||||
const models = join(data, 'models');
|
||||
if (existsSync(models)) return { data, linked: false };
|
||||
try { link(process.platform === 'win32' ? library : 'Models', models, process.platform === 'win32' ? 'junction' : 'dir'); return { data, linked: true }; }
|
||||
catch { /* exFAT, FAT, NFS, a share: no links there */ }
|
||||
try { mkdirSync(models, { recursive: true }); } catch { /* the install says what it cannot write */ }
|
||||
return { data, linked: false };
|
||||
}
|
||||
|
||||
|
||||
function readSettings(file) {
|
||||
try { const parsed = JSON.parse(readFileSync(file, 'utf8').replace(/^/, '')); return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}; } catch { return {}; }
|
||||
}
|
||||
|
||||
/** The data folder a settings file names and when it was last changed (`data_dir_at`, seconds; the file's own time for a file that has none). */
|
||||
export function readDataFolder(file) {
|
||||
if (!file) return null;
|
||||
const current = readSettings(file);
|
||||
if (typeof current.data_dir !== 'string' || !current.data_dir) return null;
|
||||
let at = Number(current.data_dir_at);
|
||||
if (!Number.isFinite(at) || at <= 0) { try { at = statSync(file).mtimeMs / 1000; } catch { at = 0; } }
|
||||
return { folder: current.data_dir, at };
|
||||
}
|
||||
|
||||
/** The settings file the Terminal (command-line Strata) keeps: the same place setup.py and strata_control/discovery.py use. */
|
||||
export function terminalSettingsFile(env = process.env, platform = process.platform) {
|
||||
if (env.STRATA_TERMINAL_SETTINGS_FILE) return env.STRATA_TERMINAL_SETTINGS_FILE;
|
||||
if (platform === 'win32') return join(env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'Strata', 'settings.json');
|
||||
return join(env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'strata', 'settings.json');
|
||||
}
|
||||
|
||||
/** Write the person's data folder into a settings file (merged, atomically) with the time of the change; returns whether it changed. Never
|
||||
* throws: the folder still works for the app, only the Terminal and the installer's own check lose the shared setting. */
|
||||
export function rememberDataFolder(file, folder, at = Date.now() / 1000, touch = false) {
|
||||
if (!file || !folder) return false;
|
||||
try {
|
||||
const current = readSettings(file);
|
||||
if (current.data_dir === folder && !touch) return false;
|
||||
mkdirSync(dirname(file), { recursive: true });
|
||||
const temp = `${file}.${process.pid}.tmp`;
|
||||
writeFileSync(temp, JSON.stringify({ ...current, data_dir: folder, data_dir_at: at }, null, 1) + '\n', { mode: 0o600 });
|
||||
renameSync(temp, file);
|
||||
return true;
|
||||
} catch { return false; }
|
||||
}
|
||||
|
||||
const same = (a, b) => resolve(a).toLowerCase() === resolve(b).toLowerCase();
|
||||
|
||||
/**
|
||||
* One models folder for the app and the Terminal. The app keeps its choice in its own settings file (`appFile`), the Terminal in `terminalFile`;
|
||||
* whichever was changed last wins and the other file is brought up to date. Nothing is moved. A folder that is gone (a disk that is not
|
||||
* connected) is never adopted. `app` is the app's own answer when its file names none (`{ folder, at }`).
|
||||
* Returns `{ folder, from }`: the shared folder (null when neither names one) and `from` = 'terminal' when the app has to take over the
|
||||
* Terminal's folder, 'app' when the Terminal's file was updated, null when nothing changed.
|
||||
*/
|
||||
export function syncDataFolder({ appFile, terminalFile, app = null, exists = path => existsSync(path) }) {
|
||||
const mine = readDataFolder(appFile) ?? app;
|
||||
const theirs = readDataFolder(terminalFile);
|
||||
const theirsUsable = theirs && exists(theirs.folder);
|
||||
if (!theirsUsable) {
|
||||
if (mine && theirs && !same(mine.folder, theirs.folder)) { rememberDataFolder(terminalFile, mine.folder, mine.at); return { folder: mine.folder, from: 'app' }; }
|
||||
return { folder: mine?.folder ?? null, from: null };
|
||||
}
|
||||
if (!mine || (!same(mine.folder, theirs.folder) && theirs.at > mine.at)) {
|
||||
rememberDataFolder(appFile, theirs.folder, theirs.at);
|
||||
return { folder: theirs.folder, from: 'terminal' };
|
||||
}
|
||||
if (!same(mine.folder, theirs.folder)) { rememberDataFolder(terminalFile, mine.folder, mine.at); return { folder: mine.folder, from: 'app' }; }
|
||||
return { folder: mine.folder, from: null };
|
||||
}
|
||||
@@ -30,6 +30,16 @@ export const PROVIDER_CATALOG = [
|
||||
const PROTOCOLS = ['openai-chat', 'openai-responses', 'anthropic'];
|
||||
const fail = message => { throw new Error(message); };
|
||||
const text = (value, max = 160) => typeof value === 'string' ? value.trim().slice(0, max) : '';
|
||||
/** The answer speed of a model API's reply: the tokens it reports over the time from its first streamed token to the end, never over the whole
|
||||
* wait (that would put the time to the first token, often seconds of prompt reading, into the speed). null when it reported no tokens;
|
||||
* with no usable first-token time (or under a quarter of a second) the whole time is the best there is. */
|
||||
export function answerSpeed(tokensOut, durationMs, firstTokenMs) {
|
||||
if (!Number.isFinite(tokensOut) || tokensOut <= 0) return null;
|
||||
const decodeMs = Number.isFinite(firstTokenMs) ? durationMs - firstTokenMs : NaN;
|
||||
const ms = decodeMs >= 250 ? decodeMs : durationMs;
|
||||
return Number((tokensOut / Math.max(0.001, ms / 1000)).toFixed(1));
|
||||
}
|
||||
|
||||
export function normalizeBaseUrl(value) {
|
||||
let url; try { url = new URL(text(value, 2048)); } catch { fail('Enter a full http:// or https:// API address.'); }
|
||||
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) fail('Use an HTTP or HTTPS base URL without credentials, a query, or a fragment.');
|
||||
@@ -207,7 +217,7 @@ export function createModelProviders({ directory, changed = () => {}, fetchImpl
|
||||
if (!textLength) fail('The model returned no text. Choose a model that supports text chat.');
|
||||
} finally { await reader.cancel().catch(() => {}); reader.releaseLock(); }
|
||||
const durationMs = Date.now() - started;
|
||||
const result = { tokensIn, tokensOut, usageSource: tokensIn !== null && tokensOut !== null ? 'reported' : 'unavailable', durationMs, ttftMs: firstToken, tps: tokensOut !== null ? tokensOut / Math.max(0.001, durationMs / 1000) : null };
|
||||
const result = { tokensIn, tokensOut, usageSource: tokensIn !== null && tokensOut !== null ? 'reported' : 'unavailable', durationMs, ttftMs: firstToken, tps: answerSpeed(tokensOut, durationMs, firstToken) };
|
||||
onFrame({ type: 'done', ...result }); return result;
|
||||
}
|
||||
/** Connections WITH their keys, for the host's own agent bridge only; never serialised to a client. */
|
||||
|
||||
@@ -347,12 +347,14 @@ export function idle(label = 'Waiting for a request') {
|
||||
hubRef?.send('monitor', snapshot());
|
||||
}
|
||||
|
||||
export function record({ tokensIn, tokensOut, durationMs = 0, ttftMs = null, cachedTokens = null }) {
|
||||
export function record({ tokensIn, tokensOut, durationMs = 0, ttftMs = null, cachedTokens = null, tps: answerTps = null }) {
|
||||
if (Number.isFinite(tokensIn)) state.totals.tokensIn += tokensIn;
|
||||
if (Number.isFinite(tokensOut)) state.totals.tokensOut += tokensOut;
|
||||
state.totals.requests += 1;
|
||||
if (Number.isFinite(tokensOut) && durationMs > 0) {
|
||||
const tps = tokensOut / (durationMs / 1000);
|
||||
// The average is of answer speeds (tokens over decode time). A request with no decode figure is left out: dividing by the whole request would
|
||||
// put the prompt reading into it and read lower than the engine and the chat do.
|
||||
if (Number.isFinite(answerTps) && answerTps > 0) {
|
||||
const tps = answerTps;
|
||||
const n = ++state.totals.usageRequests;
|
||||
state.totals.avgTps = round((state.totals.avgTps * (n - 1) + tps) / n, 1);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import {chooseParallel,applyParallel,withoutParallel,FALLBACK_NOTE} from './native-parallel.mjs';
|
||||
import { randomBytes, randomUUID, createHash } from 'node:crypto';
|
||||
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, realpathSync, copyFileSync, renameSync, unlinkSync, chmodSync, statSync, accessSync, constants } from 'node:fs';
|
||||
import { join, resolve, dirname, basename } from 'node:path';
|
||||
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, realpathSync, copyFileSync, renameSync, rmSync, statfsSync, unlinkSync, chmodSync, statSync, accessSync, constants } from 'node:fs';
|
||||
import { join, resolve, dirname, basename, sep, relative, isAbsolute } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createServer } from 'node:net';
|
||||
import { NATIVE_FIELDS, APP_OWNED_FIELDS, withoutDraftLayer, validateNativeSettings, validateMcpServers, publicMcpServers, publicNativeEnv, restoreNativeEnv } from './native-settings.mjs';
|
||||
@@ -14,12 +14,14 @@ import os from 'node:os';
|
||||
import { runtimeVision } from './native-vision.mjs';
|
||||
import { componentsDirectory, installerEnvironment, controlEnvironment } from './control-runtime.mjs';
|
||||
import { createEngineStep } from './engine-step.mjs';
|
||||
import { dataFolderFromPick, isDriveRoot, makeFolder, rememberDataFolder, syncDataFolder, terminalSettingsFile } from './model-folder.mjs';
|
||||
import { createAutoTune, writeTuneConfig } from './native-autotune.mjs';
|
||||
import { plainError, PHASE_TEXT } from './update-messages.mjs';
|
||||
import { checkRunConfig, describeIssues } from './run-config-check.mjs';
|
||||
import { openTextFile } from './open-text-file.mjs';
|
||||
import { markEngine, engineRefusal } from './engine-version.mjs';
|
||||
import { friendlyModelName } from './model-names.mjs';
|
||||
import { parseSaved as parseRoles, mirror as mirrorRoles } from './gpu-roles.mjs';
|
||||
|
||||
const ROOT=resolve(dirname(fileURLToPath(import.meta.url)),'..');
|
||||
const safeId=value=>typeof value==='string'&&/^[a-z0-9_-]{1,80}$/i.test(value);
|
||||
@@ -51,7 +53,8 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
let defaultId=null,timer=null,closing=false,polling=false;
|
||||
const emit=()=>{const value=state();writeFileSync(join(directory,'native-jobs.json'),JSON.stringify(value.jobs),{mode:0o600});changed(value);};
|
||||
const append=(instanceId,line)=>{logs.push({at:new Date().toISOString(),instanceId,line:String(line).slice(0,2000)});if(logs.length>400)logs.shift();};
|
||||
function state(){return {...snapshot,downloadDirectory:preferences.downloadDirectory??(modelsDirectory?modelsDirectory():snapshot.installed?.data_folder),fields:nativeFields(),instances:[...instances.values()].map(({key,configPath,fingerprint,mcpServers,...i})=>structuredClone(i)),defaultId,jobs:[...jobs.values()].slice(-12).map(({process,abort,engineAbort,rawResult,...j})=>j),capabilities:{responses:sourceText().includes('/v1/responses'),updates:!!snapshot.available,rollback:previousEngine(),componentUpdates:packaged&&typeof control.update==='function',calibration:packaged?false:snapshot.hardware?.gpus?.some(g=>/nvidia/i.test(g.name??''))??false}};}
|
||||
function diskSpace(path){try{const s=statfsSync(path);return {free:Number(s.bavail)*Number(s.bsize),total:Number(s.blocks)*Number(s.bsize)};}catch{return null;}}
|
||||
function state(){const folder=preferences.downloadDirectory??(modelsDirectory?modelsDirectory():snapshot.installed?.data_folder),space=folder?diskSpace(folder):null;return {...snapshot,downloadDirectory:folder,downloadFreeBytes:space?.free??null,downloadTotalBytes:space?.total??null,fields:nativeFields(),instances:[...instances.values()].map(({key,configPath,fingerprint,mcpServers,...i})=>structuredClone(i)),defaultId,jobs:[...jobs.values()].slice(-12).map(({process,abort,engineAbort,rawResult,...j})=>j),capabilities:{responses:sourceText().includes('/v1/responses'),updates:!!snapshot.available,rollback:previousEngine(),componentUpdates:packaged&&typeof control.update==='function',calibration:packaged?false:snapshot.hardware?.gpus?.some(g=>/nvidia/i.test(g.name??''))??false}};}
|
||||
let sourceCache={path:'',time:0,text:''};
|
||||
function sourceText(){try{const path=join(packaged?control.control.dir:preferences.repo,'serve/server.py'),time=statSync(path).mtimeMs;if(sourceCache.path===path&&sourceCache.time===time)return sourceCache.text;sourceCache={path,time,text:readFileSync(path,'utf8')};return sourceCache.text;}catch{return '';}}
|
||||
function nativeFields(){const source=sourceText();return NATIVE_FIELDS.map(field=>({...field,supported:APP_OWNED_FIELDS.includes(field.id)||field.id.startsWith('vision_')||field.id.startsWith('mcp_')||field.id==='vram_reserve_mib'||source.includes(field.id)}));}
|
||||
@@ -72,6 +75,10 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
if(JSON.stringify(next)!==JSON.stringify(config)){try{writeModelConfig(path,next);}catch{/* read-only folder: the load reports it */}}}}
|
||||
function installedInfo(info){if(!info)return info;return {...info,models:(info.models??[]).map(entry=>{const cfg=/^strata-[a-z0-9_-]+\.json$/i.test(entry.config??'')?readJson(entry.configPath??join(repo(),entry.config)):{};const at=cfg.args?.indexOf('--native')??-1,nativeFile=at>=0?cfg.args[at+1]:null;const dataFolder=typeof nativeFile==='string'&&basename(dirname(dirname(nativeFile)))==='models'?dirname(dirname(dirname(nativeFile))):info.data_folder;/* A model the person pointed at their own GGUF folder (not <data>/models/<tag>): remembered, so a later load runs setup with that folder again instead of downloading the files (#3199). */const ownGgufDir=typeof nativeFile==='string'&&basename(dirname(dirname(nativeFile)))!=='models'&&existsSync(nativeFile)?dirname(nativeFile):undefined;return {...entry,ownGgufDir,diskGb:entry.diskGb??entry.disk_gb,diskBytes:entry.diskBytes??entry.disk_bytes,sharedBytes:entry.sharedBytes??entry.shared_bytes,removableGb:entry.removableGb??entry.removable_gb,dataFolder,visionPrepared:!!cfg.vision,/* The picture files are prepared when the run config has a vision section: the Models screen then loads straight away instead of running setup again (it used to read 'off' for every model, which sent every load of a picture-capable model through "Apply settings and load", and setup writes the arguments, the draft layer and the reader back each time). */images:cfg.vision?'yes':'off',draftLayer:cfg.mtp!==false,/* Picture reading is ON unless the person turned it off: no saved choice and not loaded is undefined, not false (owner rule: pictures on by default). */visionEnabled:cfg.ui_vision_enabled??(()=>{const running=[...instances.values()].find(i=>i.model===entry.model&&i.status==='ready');return running?!!running.vision:undefined;})(),visionDevice:cfg.ui_vision_device,resourceProfile:cfg.ui_resource_profile,lowRamMode:cfg.args?.includes('--resident-experts')?'resident':cfg.args?.includes('--mmap-experts')?'mmap':'auto'};})};}
|
||||
function installRoot(){const found=[preferences.installDir].find(d=>d&&existsSync(d));if(found)return realpathSync(found);mkdirSync(defaultInstall(),{recursive:true});return realpathSync(defaultInstall());}
|
||||
/* The settings file Strata Control and the installer read (control-runtime.mjs sets STRATA_SETTINGS_FILE to it): the app's data folder is written there. */
|
||||
function controlSettingsFile(){return process.env.STRATA_SETTINGS_FILE||join(dirname(componentsDirectory()),'app-settings.json');}
|
||||
/* One models folder for the app and the Terminal: the one changed last wins and the other settings file is updated (nothing is moved). */
|
||||
function shareDataFolder(){if(!packaged)return;try{let at=0;if(preferences.downloadDirectory)try{at=statSync(settingsPath).mtimeMs/1000;}catch{/* no file yet */}const found=syncDataFolder({appFile:controlSettingsFile(),terminalFile:terminalSettingsFile(),app:preferences.downloadDirectory?{folder:preferences.downloadDirectory,at}:null});if(found.from==='terminal'&&found.folder&&found.folder!==preferences.downloadDirectory){preferences={...preferences,downloadDirectory:found.folder};writeFileSync(settingsPath,JSON.stringify(preferences,null,2),{mode:0o600});}}catch{/* the shared setting is a convenience: never stop the app */}}
|
||||
function repo(){if(packaged)return installRoot();const path=realpathSync(preferences.repo);if(!existsSync(join(path,'setup.py'))||!existsSync(join(path,'tools/strata_mcp.py'))||!existsSync(join(path,'serve/server.py')))throw new Error('Choose the Strata repository folder containing setup.py and tools/strata_mcp.py.');return path;}
|
||||
function python(){return resolvePython({repo:preferences.repo});}
|
||||
function send(method,params,timeout=30000){return new Promise((resolve,reject)=>{const id=++seq;const alarm=setTimeout(()=>{pending.delete(id);reject(new Error('The native engine did not respond in time. Check engine logs.'));},timeout);pending.set(id,{resolve,reject,alarm});child.stdin.write(JSON.stringify({jsonrpc:'2.0',id,method,params})+'\n');});}
|
||||
@@ -121,7 +128,7 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
refreshing=(async()=>{try{const [catalog,status]=packaged?await (async()=>{const s=await tool('strata_status',{hardware:true});const c=await tool('strata_models').catch(error=>({families:[],contexts:[],recommendation:{family:null,model:null},context_note:'',error:error.message}));return [c,s];})():await Promise.all([tool('strata_models'),tool('strata_status',{hardware:true})]);let visionReader=false;if(packaged){try{const e=await control.engine();const root=e?.engine?.root??(e?.engine?.exe?dirname(e.engine.exe):'');visionReader=!!root&&['strata-vision','strata-vision.exe'].some(n=>existsSync(join(root,n)));}catch{visionReader=false;}}const markImages=c=>c&&Array.isArray(c.families)?{...c,families:c.families.map(f=>({...f,imagesCapable:!!f.images,images:!!f.images&&visionReader,sizes:(f.sizes??[]).map(s=>({...s,imagesCapable:!!s.images,images:!!s.images&&visionReader}))}))}:c;snapshot={available:true,repo:packaged?status.installDir:repo(),version:packaged?(control.version??null):await execute('git',['describe','--tags','--always'],repo()).then(s=>s.trim()).catch(()=>null),summary:status.summary,catalog:markEngine(packaged?markImages(catalog):catalog,status.engine),engineInfo:{version:status.engine?.version??null,source:status.engine?.source??null},visionReader:packaged?visionReader:true,installed:installedInfo(status.installed),hardware:status.hardware,error:null,install:status.install,external:status.running??null,...(packaged?{packaged:true,installDir:status.installDir,engine:status.engine,installerError:status.installerError}:{})};lastRead=Date.now();if(status.install?.running&&![...jobs.values()].some(j=>j.state==='running'))begin('install',j=>watchInstall(j,status.install));}catch(error){snapshot={...snapshot,available:false,error:error.message};lastRead=Date.now();}emit();return state();})().finally(()=>{refreshing=null;});const run=refreshing;if(!force&&snapshot.available){run.catch(()=>{});return state();}return run;
|
||||
}
|
||||
function configure({repository}){if(packaged){if(instances.size||[...jobs.values()].some(j=>j.state==='running'))throw new Error('Unload models and finish operations before changing the installation.');const next=realpathSync(String(repository));if(!statSync(next).isDirectory())throw new Error('Choose the folder that holds your Strata model configurations.');preferences={...preferences,installDir:next};packagedInstall=null;disconnect();writeFileSync(settingsPath,JSON.stringify(preferences,null,2),{mode:0o600});lastRead=0;return refresh(true);}if(instances.size||[...jobs.values()].some(j=>j.state==='running'))throw new Error('Unload models and finish operations before changing the installation.');const next=realpathSync(String(repository));const old=preferences;preferences={...preferences,repo:next};try{repo();}catch(e){preferences=old;throw e;}disconnect();writeFileSync(settingsPath,JSON.stringify(preferences,null,2),{mode:0o600});lastRead=0;return refresh(true);}
|
||||
function setDownloadDirectory(input){if([...jobs.values()].some(j=>j.state==='running'))throw new Error('Finish the current operation before changing the folder.');if(typeof input!=='string'||!input.trim()||/[\r\n\0]/.test(input))throw new Error('Choose a model folder.');const path=realpathSync(input.trim());if(!statSync(path).isDirectory())throw new Error('Choose a folder, not a file.');accessSync(path,constants.R_OK|constants.W_OK);preferences={...preferences,downloadDirectory:path};writeFileSync(settingsPath,JSON.stringify(preferences,null,2),{mode:0o600});emit();return state();}
|
||||
function setDownloadDirectory(input,{insideJob=false}={}){if(!insideJob&&[...jobs.values()].some(j=>j.state==='running'))throw new Error('Finish the current operation before changing the folder.');if(typeof input!=='string'||!input.trim()||/[\r\n\0]/.test(input))throw new Error('Choose a model folder.');let path;try{path=realpathSync(input.trim());}catch{throw new Error('That folder does not exist. Check the path, or that the drive is connected.');}if(!statSync(path).isDirectory())throw new Error('Choose a folder, not a file.');/* A whole drive (D:\) is not filled with models: D:\Strata is made and used. */if(isDriveRoot(path)){const own=dataFolderFromPick(path).path;makeFolder(own);path=realpathSync(own);}try{accessSync(path,constants.R_OK|constants.W_OK);}catch{throw new Error('Strata cannot write in that folder. Choose a folder you can save files in.');}preferences={...preferences,downloadDirectory:path};/* the same choice for the installer and the Terminal: the Control settings file */if(packaged)rememberDataFolder(controlSettingsFile(),path,Date.now()/1000,true);writeFileSync(settingsPath,JSON.stringify(preferences,null,2),{mode:0o600});shareDataFolder();emit();return state();}
|
||||
function recordFile(model){if(!safeId(model))throw new Error('Choose an installed model.');const entry=snapshot.installed?.models?.find(i=>i.model===model);if(!entry)throw new Error('This model is not installed. Download and prepare it first.');const path=realpathSync(entry.configPath??join(repo(),entry.config));if(dirname(path)!==repo()||!/^strata-[a-z0-9_-]+\.json$/i.test(basename(path)))throw new Error('The model configuration is outside the Strata installation.');return {entry,path};}
|
||||
function record(model){const {entry,path}=recordFile(model);return {entry,path,config:readJson(path)};}
|
||||
/* The text file a power user edits by hand: where it is and what is wrong in it (run-config-check.mjs). */
|
||||
@@ -133,7 +140,9 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
function save(model,{values,mcp}){if([...jobs.values()].some(j=>j.state==='running'))throw new Error('Finish the current operation before saving native settings.');const {path,config}=record(model);if(busy(requests().filter(r=>instances.get(r.instanceId)?.model===model)))throw new Error('Finish this model’s requests before saving its settings.');const patch=validateNativeSettings(values??{}),next={...config};for(const field of nativeFields())if(field.supported===false&&patch[field.id]!=null&&JSON.stringify(patch[field.id])!==JSON.stringify(config[field.id]))throw new Error(`${field.label} needs a newer native Strata installation. Update it in Settings → About.`);for(const [key,value] of Object.entries(patch)){if(key==='env')next.env=restoreNativeEnv(value,config.env??{});else if(value===null)delete next[key];else next[key]=value;}if('vram_reserve_mib' in patch){const base=captureUserArgs(next);const mine=setUserFlag(base,'--vram-reserve-mib',patch.vram_reserve_mib);if(mine.length)next[USER_KEY]=mine;else delete next[USER_KEY];next.args=mine.length&&patch.vram_reserve_mib!==null?mergeUserArgs(next.args??[],mine):setUserFlag(next.args??[],'--vram-reserve-mib',null);delete next.ui_reserve_applied;if(Array.isArray(next.args_written))next.args_written=[...next.args];}if(mcp!==undefined){next.mcp_servers=validateMcpServers(mcp,config.mcp_servers??config.mcpServers??{});delete next.mcpServers;}copyFileSync(path,path+'.bak');chmodSync(path+'.bak',0o600);const temp=path+'.ui-tmp';writeFileSync(temp,JSON.stringify(next,null,2)+'\n',{mode:0o600});renameSync(temp,path);return settings(model);}
|
||||
function previousEngine(){if(packaged)return !!snapshot.engine?.previous;try{const data=snapshot.installed?.data_dir??snapshot.installed?.data_folder;return [join(preferences.repo,'engine/.previous'),data&&join(data,'engine/.previous')].filter(Boolean).some(existsSync)&&readFileSync(join(preferences.repo,'setup.py'),'utf8').includes('--rollback-engine');}catch{return false;}}
|
||||
async function plan(args){await refresh();if(!snapshot.available)throw new Error(snapshot.error);return tool('strata_install',installArgs(args,false));}
|
||||
function installArgs(args,confirm){const allowed=['family','model','context','vision','kv','gpu','gpus','backend','low_ram','port','gguf_dir'];const size=snapshot.catalog?.families?.find(f=>f.family===args.family)?.sizes.find(s=>s.model===args.model);const existing=snapshot.installed?.models?.find(m=>m.model===size?.id);/* A model adopted from the person's own GGUF folder reports that folder's parent as its data folder: it is theirs, not Strata's, so a second preparation never uses it for the packs. */if(args.gguf_dir===undefined&&existing?.ownGgufDir)args={...args,gguf_dir:existing.ownGgufDir};const data=nativeDataDirectory(args.data_dir??(args.gguf_dir?undefined:existing?.dataFolder)??preferences.downloadDirectory??modelsDirectory?.());return {...(data?{data_dir:data}:{}),...Object.fromEntries(allowed.filter(k=>args[k]!==undefined).map(k=>[k,args[k]])),/* One card is "gpu", never "gpus" (the installer wants two or more there: "Gpus='0' is not valid", #3203). */...(/^\d{1,2}$/.test(String(args.gpus??''))?{gpu:Number(args.gpus),gpus:undefined}:{}),/* Picture reading is only downloaded when it was asked for (Read pictures); the default stays the smaller download. */...(packaged&&args.vision===undefined?{vision:'no'}:{}),confirm};}
|
||||
/* The folder the person picked in the first-run chooser can be the one that holds their GGUF files (or a folder inside it). It is theirs, not a Strata data folder: packs are never made in it, and a Strata folder around it (<Strata>/models/<tag>) is used instead (#3507, #3424). */
|
||||
function ownFilesDataFolder(candidate,args){if(!candidate||args.data_dir!==undefined||!args.gguf_dir)return candidate;const same=(a,b)=>{const x=resolve(a),y=resolve(b);return process.platform==='win32'?x.toLowerCase()===y.toLowerCase():x===y;};const inside=(a,b)=>{const rel=relative(resolve(b),resolve(a));return rel===''||(!!rel&&!rel.startsWith('..')&&!isAbsolute(rel));};if(!same(candidate,args.gguf_dir)&&!inside(candidate,args.gguf_dir))return candidate;const models=dirname(resolve(args.gguf_dir)),top=dirname(models);return basename(models).toLowerCase()==='models'&&/strata/i.test(basename(top))?top:undefined;}
|
||||
function installArgs(args,confirm){const allowed=['family','model','context','vision','kv','gpu','gpus','backend','low_ram','port','gguf_dir'];const size=snapshot.catalog?.families?.find(f=>f.family===args.family)?.sizes.find(s=>s.model===args.model);const existing=snapshot.installed?.models?.find(m=>m.model===size?.id);/* A model adopted from the person's own GGUF folder reports that folder's parent as its data folder: it is theirs, not Strata's, so a second preparation never uses it for the packs. */if(args.gguf_dir===undefined&&existing?.ownGgufDir)args={...args,gguf_dir:existing.ownGgufDir};const data=nativeDataDirectory(ownFilesDataFolder(args.data_dir??(args.gguf_dir?undefined:existing?.dataFolder)??preferences.downloadDirectory??modelsDirectory?.(),args));if(data&&packaged&&!args.data_dir)rememberDataFolder(controlSettingsFile(),data);return {...(data?{data_dir:data}:{}),...Object.fromEntries(allowed.filter(k=>args[k]!==undefined).map(k=>[k,args[k]])),/* One card is "gpu", never "gpus" (the installer wants two or more there: "Gpus='0' is not valid", #3203). */...(/^\d{1,2}$/.test(String(args.gpus??''))?{gpu:Number(args.gpus),gpus:undefined}:{}),/* Picture reading is only downloaded when it was asked for (Read pictures); the default stays the smaller download. */...(packaged&&args.vision===undefined?{vision:'no'}:{}),confirm};}
|
||||
|
||||
function begin(kind,run,metadata={}){if([...jobs.values()].some(j=>j.state==='running'))throw new Error('Finish or cancel the current engine operation first.');const job={id:randomUUID(),kind,state:'running',startedAt:new Date().toISOString(),summary:'Starting…',result:null,error:null,...metadata};for(const [id,old] of jobs)if(jobs.size>=12&&old.state!=='running')jobs.delete(id);jobs.set(job.id,job);emit();void run(job).then(result=>{if(job.state==='running'){Object.assign(job,{state:'done',summary:result?.summary??'Complete',result,finishedAt:new Date().toISOString()});}}).catch(error=>{if(job.state==='running')Object.assign(job,{state:'error',error:error.message,summary:error.message,...(error.details?{details:error.details}:{}),finishedAt:new Date().toISOString()});}).finally(()=>{if(job.kind.startsWith('update')||job.kind==='rollback')disconnect();lastRead=0;emit();void refresh(true);});return state().jobs.find(j=>j.id===job.id);}
|
||||
async function watchInstall(job,current){job.summary=current.step??'Resuming preparation status…';job.result={install:current};while(job.state==='running'){await new Promise(r=>setTimeout(r,1500));const result=await tool('strata_install');job.summary=result.summary;job.result=result;emit();if(!result.install?.running){if(result.install?.exit_code!==0)throw new Error(result.install?.error??'Preparation did not finish. Retry to reuse completed files.');return {summary:'Model preparation complete. Load it from Models.'};}}}
|
||||
@@ -201,8 +210,10 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
const resources=args.resourceProfile?resourceSettings({...base0,[USER_KEY]:mine,vision},snapshot.hardware,args.resourceProfile,modelArena(args.model),values0):null;
|
||||
const cfg={...config,...visionPreferences,...(resources?{...resources,ui_resource_profile:args.resourceProfile}:{}),host:'127.0.0.1',open_browser:false,...validateNativeSettings(args.values??{}),vision};
|
||||
/* The app's suggested cards only fill a gap: a model whose settings file already names its GPUs ("gpu": [1, 0], with "gpu_order": "as_given") keeps them (#3256). A card the person picked in the screen always wins. */
|
||||
const namedInFile=Array.isArray(config.gpu)?config.gpu.length>0:config.gpu!==undefined&&config.gpu!==null&&config.gpu!=='';
|
||||
if(args.gpus!==undefined&&!(args.gpusFromRecommendation&&namedInFile)){const detected=new Set((snapshot.hardware?.gpus??[]).map(g=>g.index));if(!Array.isArray(args.gpus)||!args.gpus.length||new Set(args.gpus).size!==args.gpus.length||args.gpus.some(g=>!Number.isInteger(g)||!detected.has(g)))throw new Error('Choose distinct detected GPUs. CPU-only text inference is not supported by this native adapter.');cfg.gpu=args.gpus.length===1?args.gpus[0]:args.gpus;cfg.layer_split=args.gpus.length>1?validateNativeSettings({layer_split:args.layerSplit??'auto'}).layer_split:'auto';if(cfg.layer_split!=='auto'&&cfg.layer_split.split(',').length!==args.gpus.length-1)throw new Error('Choose one layer split point between each pair of GPUs.');}
|
||||
const namedInFile=(Array.isArray(config.gpu)?config.gpu.length>0:config.gpu!==undefined&&config.gpu!==null&&config.gpu!=='')||!!config.gpu_roles;
|
||||
/* Roles chosen in the screen (Main / Helper / Off per card, gpu-roles.mjs): the card list is the roles' mirror - Helpers first, the Main card LAST, Off cards left out - with "gpu_roles" beside it. Every card Off is allowed: the server says plainly what the engine can do. */
|
||||
if(args.gpuRoles!==undefined){const make=snapshot.hardware?.engine_vendor,cards=(snapshot.hardware?.gpus??[]).filter(g=>!make||!g.vendor||g.vendor===make),named=parseRoles(args.gpuRoles);if(!named)throw new Error('Choose Main, Helper or Off for each graphics card, with one Main.');const known=new Set(cards.map(g=>g.index));if(Object.keys(named).some(k=>!known.has(Number(k))))throw new Error('Choose detected graphics cards.');const roles=Object.fromEntries(cards.map(g=>[g.index,named[g.index]??'off'])),m=mirrorRoles(roles,cards);for(const k of ['gpu','gpu_order','gpu_none','gpu_roles','gpu_cards','hip_ordinal'])delete cfg[k];Object.assign(cfg,m);const count=Array.isArray(m.gpu)?m.gpu.length:m.gpu_none?0:1;cfg.layer_split=count>1?validateNativeSettings({layer_split:args.layerSplit??'auto'}).layer_split:'auto';if(cfg.layer_split!=='auto'&&cfg.layer_split.split(',').length!==count-1)throw new Error('Choose one layer split point between each pair of GPUs.');}
|
||||
else if(args.gpus!==undefined&&!(args.gpusFromRecommendation&&namedInFile)){const detected=new Set((snapshot.hardware?.gpus??[]).map(g=>g.index));if(!Array.isArray(args.gpus)||!args.gpus.length||new Set(args.gpus).size!==args.gpus.length||args.gpus.some(g=>!Number.isInteger(g)||!detected.has(g)))throw new Error('Choose distinct detected GPUs. CPU-only text inference is not supported by this native adapter.');cfg.gpu=args.gpus.length===1?args.gpus[0]:args.gpus;for(const k of ['gpu_roles','gpu_cards','gpu_none'])delete cfg[k];delete cfg.hip_ordinal;cfg.layer_split=args.gpus.length>1?validateNativeSettings({layer_split:args.layerSplit??'auto'}).layer_split:'auto';if(cfg.layer_split!=='auto'&&cfg.layer_split.split(',').length!==args.gpus.length-1)throw new Error('Choose one layer split point between each pair of GPUs.');}
|
||||
if(cfg.vram_reserve_mib!==undefined&&cfg.vram_reserve_mib!==null){cfg.args=[...(cfg.args??[])];const at=cfg.args.indexOf('--vram-reserve-mib');if(at>=0)cfg.args.splice(at,2);cfg.args.push('--vram-reserve-mib',String(cfg.vram_reserve_mib));}
|
||||
if(mine.length){cfg.args=mergeUserArgs(cfg.args??[],mine);cfg[USER_KEY]=mine;}else delete cfg[USER_KEY];
|
||||
if(cfg.vision){if(cfg.vision_tokens!=null)cfg.vision={...cfg.vision,max_tokens:cfg.vision_tokens};if(cfg.vision_min_tokens!=null)cfg.vision={...cfg.vision,min_tokens:cfg.vision_min_tokens};}cfg.mcp={...(cfg.mcp??{}),...(cfg.mcp_timeout_s!=null?{timeout_s:cfg.mcp_timeout_s}:{}),...(cfg.mcp_max_rounds!=null?{max_rounds:cfg.mcp_max_rounds}:{}),...(cfg.mcp_max_result_chars!=null?{max_result_chars:cfg.mcp_max_result_chars}:{})};
|
||||
@@ -372,10 +383,104 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
return autotuner.start(job,job.kind==='recheck'?'recheck':'tune',{configPath:path,presetId:args.presetId,mode:args.mode,use:args.use,goal:room?'room':'speed',leaveRam:room?20:0,allowSmallDifferences:args.allowSmallDifferences===true,name:args.name},{onProgress:p=>{job.summary=p.text??job.summary;emit();}});
|
||||
}
|
||||
function operation(kind,args={}){if(!['install','load','benchmark','tune','autotune','recheck','calibrate','update-engine','update-strata','rollback','remove'].includes(kind))throw new Error('Choose a supported native operation.');if(!snapshot.available)throw new Error(snapshot.error??'Connect the native Strata installation first.');if(kind==='install'||kind==='load'){const refusal=engineRefusal(snapshot.catalog,args.model,snapshot.engineInfo);if(refusal)throw new Error(refusal);}return begin(kind,job=>kind==='install'?install(job,args):kind==='load'?loadEngine(job,args):kind==='benchmark'?benchmark(job,args):kind==='tune'?tune(job,args):kind==='autotune'||kind==='recheck'?autotune(job,args):kind==='remove'?removeModel(job,args):program(job,args),{guided:args.guided===true,model:args.model,...(args.family?{family:args.family}:{})});}
|
||||
async function cancel(id){const job=jobs.get(id);if(!job||job.state!=='running')throw new Error('This operation is no longer running.');if(job.kind==='install'){job.engineAbort?.abort();await tool('strata_install',{cancel:true});}else if(job.abort)job.abort.abort();else if(job.process){if(!['tune','calibrate','autotune','recheck'].includes(job.kind))throw new Error('File updates cannot be cancelled safely while files are being replaced.');job.state='cancelled';await stopProcess(job.process);}job.state='cancelled';job.summary=job.kind==='install'?'Stopped. Completed downloads are kept.':'Stopped.';emit();return state();}
|
||||
/* ---- The models folder card (0.2.2): what a picked folder means, and the three choices. The copying is Control's (strata models move). ---- */
|
||||
const currentModelsFolder=()=>preferences.downloadDirectory??(modelsDirectory?modelsDirectory():snapshot.installed?.data_folder)??null;
|
||||
const moveFile=()=>join(directory,'models-move.cancel');
|
||||
const folderWords=error=>({message:error.message,details:error.details||''});
|
||||
/** A picked folder as the data folder: a whole drive (D:\) becomes D:\Strata, and the person is told (the models then live in D:\Strata\Models). */
|
||||
function pickedModelsFolder(input){
|
||||
if(typeof input!=='string'||!input.trim()||/[\r\n\0]/.test(input))throw new Error('Choose a models folder.');
|
||||
const picked=resolve(input.trim()),target=dataFolderFromPick(picked);
|
||||
return {picked,folder:target.path,wholeDrive:!!target.note,shown:target.note?join(target.path,'Models'):target.path};
|
||||
}
|
||||
async function planModelsFolder(input){
|
||||
if(!packaged)throw new Error('Changing the models folder is part of the Strata app.');
|
||||
const now=currentModelsFolder();if(!now)throw new Error('Strata is still reading your models folder. Try again in a moment.');
|
||||
const pick=pickedModelsFolder(input),plan=await control.planModelsMove(now,pick.folder);
|
||||
return {...plan,...pick,current:now,running:instances.size,busy:[...jobs.values()].some(j=>j.state==='running')};
|
||||
}
|
||||
/* "Start empty": the old models are no longer listed. Their run configs are moved (not deleted) into old-models beside the others; the model files are not touched. */
|
||||
function setAsideConfigs(old){
|
||||
const folder=repo(),kept=join(folder,'old-models'),aside=[];
|
||||
const under=value=>typeof value==='string'&&[ 'models','packs','mtp' ].some(item=>{const base=join(old,item).toLowerCase();const v=value.toLowerCase();return v===base||v.startsWith(base+sep)||v.startsWith(base+'/');});
|
||||
const scan=value=>Array.isArray(value)?value.some(scan):value&&typeof value==='object'?Object.values(value).some(scan):under(value);
|
||||
for(const name of readdirSync(folder)){
|
||||
if(!/^strata-[a-z0-9_-]+\.json$/i.test(name)||name.endsWith('.shared-settings.json'))continue;
|
||||
if(!scan(readJson(join(folder,name))))continue;
|
||||
mkdirSync(kept,{recursive:true});renameSync(join(folder,name),join(kept,name));aside.push(name);
|
||||
}
|
||||
return aside;
|
||||
}
|
||||
async function startModelsFolder({folder:input,choice,stopRunning=false}){
|
||||
if(!packaged)throw new Error('Changing the models folder is part of the Strata app.');
|
||||
if(!['move','keep','empty'].includes(choice))throw new Error('Choose what to do with your models.');
|
||||
if([...jobs.values()].some(j=>j.state==='running'))throw new Error('Finish or stop the download to change the folder.');
|
||||
const plan=await planModelsFolder(input);
|
||||
if(plan.issue)throw Object.assign(new Error(plan.issue.message),{details:plan.issue.details,code:plan.issue.code});
|
||||
if(!plan.choices[choice].possible)throw Object.assign(new Error(plan.choices[choice].reason||'That choice is not possible here.'),{code:'not_possible'});
|
||||
if(instances.size){
|
||||
if(!stopRunning)throw Object.assign(new Error('A model is running. Stop it first, then change the folder.'),{code:'running'});
|
||||
for(const id of [...instances.keys()])await unload(id);
|
||||
}
|
||||
const old=plan.current,target=plan.folder;
|
||||
if(choice!=='move'){
|
||||
makeFolder(target);
|
||||
const aside=choice==='empty'?setAsideConfigs(old):[];
|
||||
setDownloadDirectory(target);lastRead=0;await refresh(true);
|
||||
return {done:true,choice,folder:target,oldFolder:old,setAside:aside};
|
||||
}
|
||||
return runModelsMove(old,target,(from,to,options)=>control.moveModels(from,to,options));
|
||||
}
|
||||
/* The move as a job (the progress dialog reads it). `run` is moveModels for a new move, resumeModelsMove for "Finish it". */
|
||||
function runModelsMove(old,target,run){
|
||||
rmSync(moveFile(),{force:true});
|
||||
return begin('models-move',async job=>{
|
||||
job.cancelFile=moveFile();job.summary='Starting…';
|
||||
try{
|
||||
const result=await run(old,target,{installDir:repo(),cancelFile:moveFile(),onEvent:event=>{
|
||||
if(event.event!=='progress')return;
|
||||
job.progress={done:event.done,total:event.total,file:event.file,group:event.group,index:event.index,count:event.count,secondsLeft:event.seconds_left||0,sameDrive:!!event.same_drive};
|
||||
job.summary=`${Math.round(event.done/1e8)/10} of ${Math.round(event.total/1e8)/10} GB`;emit();}});
|
||||
const finished={done:!!result.done,stopped:!!result.stopped,moved:result.moved??[],remaining:result.remaining??[],bytes:result.bytes_moved??0,total:result.total_bytes??0,from:old,to:target,files:result.files_moved??0,sameDrive:!!result.same_drive};
|
||||
/* Only a whole move changes the folder the app uses: a stopped move leaves it where it was, and every model works where it is. */
|
||||
if(result.done){setDownloadDirectory(target,{insideJob:true});finished.folder=target;}
|
||||
job.progress=null;
|
||||
return {summary:result.done?'Your models are moved.':'Moving stopped. Nothing was lost.',...finished};
|
||||
}catch(error){
|
||||
const left=error.output;
|
||||
job.result={failed:true,code:error.code,moved:left?.moved??[],remaining:left?.remaining??[],bytes:left?.bytes_moved??0,total:left?.total_bytes??0,from:old,to:target};
|
||||
job.progress=null;
|
||||
throw error;
|
||||
}finally{rmSync(moveFile(),{force:true});}
|
||||
},{family:undefined});
|
||||
}
|
||||
/* "A move was interrupted. Finish it or leave it": the next start finds the unfinished move (Control keeps a note in the models folder in use). */
|
||||
async function interruptedMove(){
|
||||
if(!packaged)return null;
|
||||
const now=currentModelsFolder();if(!now)return null;
|
||||
if([...jobs.values()].some(j=>j.kind==='models-move'&&j.state==='running'))return null;
|
||||
try{return await control.interruptedModelsMove(now);}catch{return null;}
|
||||
}
|
||||
async function finishInterruptedMove({stopRunning=false}={}){
|
||||
const st=await interruptedMove();if(!st)throw new Error('No move was interrupted.');
|
||||
if([...jobs.values()].some(j=>j.state==='running'))throw new Error('Finish or stop the download to change the folder.');
|
||||
if(instances.size){
|
||||
if(!stopRunning)throw Object.assign(new Error('A model is running. Stop it first, then change the folder.'),{code:'running'});
|
||||
for(const id of [...instances.keys()])await unload(id);
|
||||
}
|
||||
return runModelsMove(st.from,st.to,(from,to,options)=>control.resumeModelsMove(from,options));
|
||||
}
|
||||
async function leaveInterruptedMove(){
|
||||
const st=await interruptedMove();if(!st)throw new Error('No move was interrupted.');
|
||||
if([...jobs.values()].some(j=>j.state==='running'))throw new Error('Finish or stop the download to change the folder.');
|
||||
const result=await control.leaveModelsMove(st.from,{installDir:repo()});
|
||||
lastRead=0;await refresh(true);
|
||||
return {done:true,folder:st.from,partial:st.to,removed:result.removed??0,restored:result.restored??0};
|
||||
}
|
||||
async function cancel(id){const job=jobs.get(id);if(!job||job.state!=='running')throw new Error('This operation is no longer running.');if(job.kind==='install'){job.engineAbort?.abort();await tool('strata_install',{cancel:true});}else if(job.kind==='models-move'){/* a safe stop: the copy in flight is dropped, files already moved stay */try{writeFileSync(job.cancelFile,'stop');}catch{/* the run ends anyway */}job.summary='Stopping…';emit();return state();}else if(job.abort)job.abort.abort();else if(job.process){if(!['tune','calibrate','autotune','recheck'].includes(job.kind))throw new Error('File updates cannot be cancelled safely while files are being replaced.');job.state='cancelled';await stopProcess(job.process);}job.state='cancelled';job.summary=job.kind==='install'?'Stopped. Completed downloads are kept.':'Stopped.';emit();return state();}
|
||||
async function close(){closing=true;clearInterval(timer);const installing=packaged&&child&&[...jobs.values()].some(j=>j.kind==='install'&&j.state==='running');if(installing)await send('tools/call',{name:'strata_install',arguments:{cancel:true}},15000).catch(()=>{});const operations=[];for(const job of jobs.values()){job.abort?.abort();if(job.process)operations.push(stopProcess(job.process));if(job.state==='running')job.state='cancelled';}await Promise.all(operations);await Promise.all([...instances.keys()].map(id=>unload(id,true).catch(()=>{})));disconnect();}
|
||||
// Remove nonserializable process references from public jobs.
|
||||
const publicState=()=>{const value=state();value.jobs=value.jobs.map(({process,engineAbort,rawResult,...job})=>job);return value;};
|
||||
async function localPreview(path){if(!packaged)throw new Error('Checking local model files is part of the Strata app.');if(typeof path!=='string'||!path.trim()||[...path].some(c=>c.charCodeAt(0)<32))throw new Error('Choose a folder.');return control.importPlan(path.trim());}
|
||||
return {state:publicState,tuneStatus,tuneEffective,tuneOff,tunePresets:{list:()=>autotuner.list(),action:(kind,id,extra)=>autotuner.action(kind,id,extra),undoFor:async args=>autotuner.undoFor(tuneConfig(args).path)},refresh,localPreview,configure,setDownloadDirectory,plan,settings,save,settingsFile,openSettingsFile,operation,cancel,unload,resolveInstance,poll,logs:()=>logs.slice(),benchmarkReport:()=>readJson(join(directory,'native-benchmark.json'),null),setDefault(id){defaultId=resolveInstance(id).id;emit();return publicState();},hasLive:()=>[...instances.values()].some(i=>i.status==='ready'),readyInstances:()=>[...instances.values()].filter(i=>i.status==='ready'),close};
|
||||
return {state:publicState,tuneStatus,tuneEffective,tuneOff,tunePresets:{list:()=>autotuner.list(),action:(kind,id,extra)=>autotuner.action(kind,id,extra),undoFor:async args=>autotuner.undoFor(tuneConfig(args).path)},refresh,localPreview,configure,setDownloadDirectory,planModelsFolder,startModelsFolder,interruptedMove,finishInterruptedMove,leaveInterruptedMove,plan,settings,save,settingsFile,openSettingsFile,operation,cancel,unload,resolveInstance,poll,logs:()=>logs.slice(),benchmarkReport:()=>readJson(join(directory,'native-benchmark.json'),null),setDefault(id){defaultId=resolveInstance(id).id;emit();return publicState();},hasLive:()=>[...instances.values()].some(i=>i.status==='ready'),readyInstances:()=>[...instances.values()].filter(i=>i.status==='ready'),close};
|
||||
}
|
||||
@@ -30,7 +30,10 @@ export function defaultReserves(config,hardware,baselineReserve=0){
|
||||
const headless=placed.length>0&&placed.every(computeOnly); // no display on the card (the desktop is on an integrated GPU): no desktop to keep room for
|
||||
const floor=headless?(config.vision?ENGINE_DEFAULT_RESERVE_MIB:COMPUTE_ONLY_RESERVE_MIB):gpuMemory.length&&Math.min(...gpuMemory)>16?1024:1536;
|
||||
const safety=headless?floor:Math.max(config.vision?2048:floor,baselineReserve);
|
||||
return {work:gpuMemory.length&&!headless?Math.max(safety,Math.floor(Math.min(...gpuMemory)*1024*.2)):safety,performance:safety};
|
||||
/* A card that shares the PC's memory (Strix Halo, other APUs) has no memory of its own to keep a fifth of: that fifth is RAM, and "Leave 20% for my work" already keeps it
|
||||
* through the RAM budget. A fifth of the shared pool here held back 16 GB of expert cache for nothing (#3510); the display floor stays. */
|
||||
const shared=placed.length>0&&placed.some(g=>g.uma===true||g.unified===true);
|
||||
return {work:gpuMemory.length&&!headless&&!shared?Math.max(safety,Math.floor(Math.min(...gpuMemory)*1024*.2)):safety,performance:safety};
|
||||
}
|
||||
|
||||
/** The reserve the person asked for (null: nothing) and the default for each profile. A 0.2.0-beta file has the key and the same number in "args" with no note that the
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/** Ports the host listens on: a free one for the helpers, and a plain sentence when the main one cannot be taken. */
|
||||
import net from 'node:net';
|
||||
|
||||
/** Why a listen failed, in words a person can act on (Windows keeps whole ranges for Hyper-V, WSL and Docker). */
|
||||
export function listenProblem(error, port, host = '127.0.0.1') {
|
||||
const code = error?.code;
|
||||
if (code === 'EADDRINUSE') return `Port ${port} is already in use. Strata may already be running; close it or the other program, or start Strata with another port (STRATA_PORT).`;
|
||||
if (code === 'EACCES') return `Windows or a firewall does not let Strata use port ${port}. Hyper-V, WSL and Docker keep ranges of ports for themselves (see "netsh int ipv4 show excludedportrange protocol=tcp"). Start Strata with another port (STRATA_PORT), for example 8801.`;
|
||||
if (code === 'EADDRNOTAVAIL') return `${host} is not an address of this computer, so Strata cannot listen on it. Check STRATA_BIND.`;
|
||||
return `Strata cannot listen on ${host}:${port} (${error?.message ?? code}).`;
|
||||
}
|
||||
|
||||
function tryListen(port, host) {
|
||||
return new Promise(resolve => {
|
||||
const probe = net.createServer();
|
||||
probe.once('error', () => resolve(null));
|
||||
probe.listen(port, host, () => { const got = probe.address().port; probe.close(() => resolve(got)); });
|
||||
});
|
||||
}
|
||||
|
||||
/** `preferred` when it can be taken, else the next free port above it (up to `tries` of them), else any free port. Never throws. */
|
||||
export async function findFreePort(preferred, host = '127.0.0.1', { skip = [], tries = 20 } = {}) {
|
||||
for (let offset = 0; offset < tries; offset++) {
|
||||
const port = preferred + offset;
|
||||
if (port > 65535 || skip.includes(port)) continue;
|
||||
const got = await tryListen(port, host);
|
||||
if (got) return got;
|
||||
}
|
||||
return (await tryListen(0, host)) ?? preferred;
|
||||
}
|
||||
@@ -70,7 +70,7 @@ export function patch(body) {
|
||||
const s = getStore();
|
||||
const serving = { ...(s.serving ?? {}) };
|
||||
if (typeof body.otherDevices === 'boolean' && body.otherDevices !== !isLocal()) throw new Error('Change the host listen address and restart to update network access.');
|
||||
if (body.apiKey === null && (!isLocal() || sharing.active || tunnelActive())) throw new Error('Stop local sharing and Cloudflare before removing the API key.');
|
||||
// The key is optional (owner decision, 0.2.2): removing it while sharing or the tunnel runs is allowed; the app warns in plain words (local-share.mjs, cloudflare.mjs).
|
||||
if (body.apiKey === null) serving.apiKey = null;
|
||||
else if (typeof body.apiKey === 'string' && body.apiKey.trim()) serving.apiKey = body.apiKey.trim();
|
||||
else if (body.generateKey) serving.apiKey = `sk-strata-${randomBytes(12).toString('hex')}`;
|
||||
@@ -149,7 +149,8 @@ export function cancel(id) {
|
||||
export function rateSample() {
|
||||
const done = requests.filter((r) => r.state === 'done' && r.tokensOut > 0);
|
||||
if (!done.length) return 0;
|
||||
const tps = done.map((r) => r.tokensOut / Math.max(0.001, r.durationMs / 1000));
|
||||
const tps = done.filter((r) => r.tps > 0).map((r) => r.tps); // answer speeds (decode time), not tokens over the whole request
|
||||
if (!tps.length) return 0;
|
||||
return round(tps.reduce((a, b) => a + b, 0) / tps.length, 1);
|
||||
}
|
||||
|
||||
|
||||
+31
-15
@@ -1,6 +1,7 @@
|
||||
import { migrateSetup } from './setup.mjs';
|
||||
/** Tiny persisted state for the host: one JSON file under STRATA_HOME. */
|
||||
import { accessSync, constants, mkdirSync, readFileSync, writeFileSync, existsSync, realpathSync, readdirSync, statSync, statfsSync, symlinkSync } from 'node:fs';
|
||||
import { alignModels, isDriveRoot, libraryFolderFromPick, makeFolder } from './model-folder.mjs';
|
||||
import { dirname, isAbsolute, join, parse, relative, resolve } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import { defaultModelDirectory } from './model-directory-default.mjs';
|
||||
@@ -58,29 +59,42 @@ function read() {
|
||||
|
||||
let state = read();
|
||||
const DEFAULT_MODELS_DIRECTORY=defaultModelDirectory();
|
||||
function initialModelsDirectory() {
|
||||
if(state.modelsDir)return state.modelsDir;
|
||||
const legacy=realpathSync(join(HOME,'models'));
|
||||
// Keep an existing library in place. A new install uses Documents/Strata/Models.
|
||||
/**
|
||||
* The model folder for a new install: the saved one, an existing library inside STRATA_HOME, else Documents/Strata/Models. A Documents folder
|
||||
* that cannot be made (Controlled folder access, a disconnected share, an offline OneDrive known folder) must not stop the host from starting:
|
||||
* the models go to STRATA_HOME/models instead, and `notice` says so in plain words.
|
||||
*/
|
||||
export function pickModelsDirectory({ saved, home, preferred, fixedByEnv = false }) {
|
||||
if (saved) return { directory: saved, notice: '' };
|
||||
const fallback = join(home, 'models');
|
||||
let legacy = fallback;
|
||||
try { legacy = realpathSync(fallback); } catch { /* the folder is made at start; keep the plain path */ }
|
||||
function hasWeights(folder,depth=0){try{return readdirSync(folder,{withFileTypes:true}).some(entry=>entry.isFile()&&/\.gguf(?:\.part|\.partial)?$/i.test(entry.name)||entry.isDirectory()&&depth<4&&hasWeights(join(folder,entry.name),depth+1));}catch{return false;}}
|
||||
if(!process.env.STRATA_MODELS_DIR&&hasWeights(legacy))return legacy;
|
||||
const directory=DEFAULT_MODELS_DIRECTORY;
|
||||
mkdirSync(directory,{recursive:true});
|
||||
return realpathSync(directory);
|
||||
// Keep an existing library in place. A new install uses Documents/Strata/Models.
|
||||
if (!fixedByEnv && hasWeights(legacy)) return { directory: legacy, notice: '' };
|
||||
try {
|
||||
mkdirSync(preferred, { recursive: true });
|
||||
accessSync(preferred, constants.R_OK | constants.W_OK | constants.X_OK);
|
||||
return { directory: realpathSync(preferred), notice: '' };
|
||||
} catch (error) {
|
||||
return { directory: legacy, notice: `Strata could not use ${preferred} for models (${error.code ?? error.message}), so models go to ${legacy}. You can pick another folder in Settings.` };
|
||||
}
|
||||
}
|
||||
export let MODELS_DIR = initialModelsDirectory();
|
||||
const MODELS_PICK = pickModelsDirectory({ saved: state.modelsDir, home: HOME, preferred: DEFAULT_MODELS_DIRECTORY, fixedByEnv: !!process.env.STRATA_MODELS_DIR });
|
||||
/** Plain words for the person when the usual model folder could not be used (empty otherwise). */
|
||||
export const MODELS_DIR_NOTICE = MODELS_PICK.notice;
|
||||
if (MODELS_DIR_NOTICE) console.warn(`[strata] ${MODELS_DIR_NOTICE}`);
|
||||
export let MODELS_DIR = MODELS_PICK.directory;
|
||||
|
||||
/** Native setup writes weights to data_dir/models. Keep its data root one level up. */
|
||||
export function nativeDataDirectory(input) {
|
||||
if(!input||process.env.STRATA_MODELS_DIR||!existsSync(DEFAULT_MODELS_DIRECTORY))return input;
|
||||
if(MODELS_DIR_NOTICE)return input;
|
||||
const root=realpathSync(DEFAULT_MODELS_DIRECTORY);
|
||||
if(resolve(input)!==resolve(DEFAULT_MODELS_DIRECTORY)&&realpathSync(input)!==root)return input;
|
||||
const data=dirname(root),nativeModels=join(data,'models');
|
||||
// On Windows/macOS case-insensitive volumes, Models already resolves as models.
|
||||
// On Linux, a relative alias keeps the native layout inside the requested folder.
|
||||
if(!existsSync(nativeModels))symlinkSync(process.platform==='win32'?root:'Models',nativeModels,process.platform==='win32'?'junction':'dir');
|
||||
const a=statSync(nativeModels),b=statSync(root);
|
||||
if(realpathSync(nativeModels)!==root&&!(a.ino&&a.ino===b.ino&&a.dev===b.dev))throw new Error('The default model folder conflicts with another library. Choose a different folder.');
|
||||
const data=dirname(root);
|
||||
// `models` and `Models` are one folder on Windows and macOS; where they are not (Linux) a link makes them one, and where no link can be made (exFAT, a share) models go in `models`.
|
||||
alignModels(data,root);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -95,6 +109,8 @@ export function validateModelsDirectory(input) {
|
||||
if (!statSync(directory).isDirectory()) throw new Error();
|
||||
accessSync(directory, constants.R_OK | constants.W_OK | constants.X_OK);
|
||||
} catch { throw new Error('This folder is unavailable or not writable. Check the path and folder permissions.'); }
|
||||
// A whole drive (D:\) is not filled with models: D:\Strata\Models is made and used.
|
||||
if (isDriveRoot(directory)) directory = realpathSync(makeFolder(libraryFolderFromPick(directory).path));
|
||||
const data = realpathSync(DATA_DIR);
|
||||
const contains = (parent, child) => { const r = relative(parent, child); return !r || !r.startsWith('..') && !isAbsolute(r); };
|
||||
if (directory === parse(directory).root || contains(directory, data) || contains(data, directory)) throw new Error('Choose a separate folder for models, outside Strata’s app data.');
|
||||
|
||||
@@ -19,7 +19,7 @@ export function CloudflareDialog({ open, onClose, onCreateKey }: {
|
||||
const steps = mode === 'quick' ? ['Address', 'Connector', 'API key', 'Connect'] : ['Address', 'Connector', 'API key', 'Domain', 'Connect'];
|
||||
const last = steps.length - 1;
|
||||
const installed = !!tunnel?.installed, keyReady = !!serving?.access.apiKeySet, other = tunnel?.external ?? null;
|
||||
const canContinue = step === 1 ? installed : step === 2 ? keyReady : step === 3 && mode === 'domain' ? !!hostname.trim() && !!token.trim() : true;
|
||||
const canContinue = step === 1 ? installed : step === 2 ? true : step === 3 && mode === 'domain' ? !!hostname.trim() && !!token.trim() : true;
|
||||
async function inspect() { setPending(true); setError(''); try {
|
||||
setDetected(await api.get<TunnelState>('/api/serving/tunnel'));
|
||||
}
|
||||
@@ -80,7 +80,7 @@ export function CloudflareDialog({ open, onClose, onCreateKey }: {
|
||||
} }
|
||||
const titles = ['Choose your address', installed ? 'Connector ready' : 'Preparing your connector', 'Protect your connection', ...(mode === 'domain' ? ['Set up your domain'] : []), 'Ready to connect'];
|
||||
return <Dialog open={open} title={tr("Connect with Cloudflare")} onClose={dismiss} actions={<>
|
||||
{tr(active ? <><Btn onClick={dismiss}>{tr("Done")}</Btn><Btn disabled={pending || tunnel?.status === 'stopping'} onClick={() => void stop()}>{tr(tunnel?.status === 'stopping' ? 'Stopping…' : 'Stop tunnel')}</Btn></> : <><Btn disabled={pending} onClick={back}>{tr(step ? 'Back' : 'Cancel')}</Btn>{tr(step === last ? <Btn variant="primary" disabled={pending || !installed || !keyReady || (mode === 'domain' && (!hostname.trim() || !token.trim()))} onClick={() => void connect()}>{tr(pending ? 'Connecting…' : (error || tunnel?.error) ? 'Try again' : mode === 'quick' ? 'Create temporary URL' : 'Connect domain')}</Btn> : <Btn variant="primary" disabled={pending || !canContinue} onClick={next}>{tr("Continue")}<Icon name="chevron" className="wizard-next" size={14}/></Btn>)}</>)}
|
||||
{tr(active ? <><Btn onClick={dismiss}>{tr("Done")}</Btn><Btn disabled={pending || tunnel?.status === 'stopping'} onClick={() => void stop()}>{tr(tunnel?.status === 'stopping' ? 'Stopping…' : 'Stop tunnel')}</Btn></> : <><Btn disabled={pending} onClick={back}>{tr(step ? 'Back' : 'Cancel')}</Btn>{tr(step === last ? <Btn variant="primary" disabled={pending || !installed || (mode === 'domain' && (!hostname.trim() || !token.trim()))} onClick={() => void connect()}>{tr(pending ? 'Connecting…' : (error || tunnel?.error) ? 'Try again' : mode === 'quick' ? 'Create temporary URL' : 'Connect domain')}</Btn> : <Btn variant="primary" disabled={pending || !canContinue} onClick={next}>{tr("Continue")}<Icon name="chevron" className="wizard-next" size={14}/></Btn>)}</>)}
|
||||
</>}>
|
||||
<div className="tunnel-wizard">
|
||||
{tr(!active ? <ol className="tunnel-progress" aria-label={tr("Connection progress")}>{tr(steps.map((label, i) => <li key={label} data-current={step === i || undefined} data-complete={step > i || undefined}><button disabled={i >= step || pending} aria-current={step === i ? 'step' : undefined} onClick={() => { setError(''); setStep(i); }}><span>{tr(step > i ? <Icon name="check" size={13}/> : i + 1)}</span><small>{tr(label)}</small></button></li>))}</ol> : null)}
|
||||
@@ -92,9 +92,9 @@ export function CloudflareDialog({ open, onClose, onCreateKey }: {
|
||||
(e.currentTarget.parentElement?.children[mode === 'quick' ? 1 : 0] as HTMLElement)?.focus();
|
||||
} }} onClick={() => setMode(choice.value)}><Icon name={choice.icon} size={20}/><span><strong>{tr(choice.title)}</strong><small>{tr(choice.detail)}</small></span><span className="tunnel-choice-dot">{tr(mode === choice.value ? <Icon name="check" size={13}/> : null)}</span></button>))}</div></> : null)}
|
||||
{tr(step === 1 ? <div className="tunnel-automatic" data-ready={installed || undefined} aria-live="polite" aria-busy={pending}><span className="tunnel-stage__symbol"><Icon name={installed ? 'check' : error ? 'warning' : 'refresh'} size={28}/></span><p className="small muted">{tr(installed ? 'Your Cloudflare connector is installed and ready.' : error ? 'Automatic setup needs another try.' : 'Strata downloads and sets up the Cloudflare connector for you.')}</p>{tr(installed ? <>{tr(tunnel?.connector ? <p className="xs muted">{tr("Using {0}", [tunnel.connector.summary])}</p> : null)}{tr(other ? <p className="xs muted" role="status">{tr(other.others.length === 1 && !other.own.length ? "cloudflared is already running for another tunnel (PID {0})." : other.own.length ? "Strata’s tunnel for this address is already running (PID {0})." : "cloudflared is already running for other tunnels (PIDs {0}).", [(other.own.length ? other.own : other.others).join(", ")])}</p> : null)}</> : error ? <Btn onClick={() => setRetry(v => v + 1)}>{tr("Retry setup")}</Btn> : <span className="small muted" role="status">{tr("Setting up\u2026")}</span>)}</div> : null)}
|
||||
{tr(step === 2 ? <><p className="small muted">{tr("Only apps with your Strata API key can use this connection.")}</p>{tr(keyReady ? <div className="tunnel-check"><Icon name="shield"/><div><strong>{tr("API key ready")}</strong><span>{tr("Use your existing key in the app you connect.")}</span></div></div> : <div className="tunnel-key-step"><Icon name="shield" size={30}/><p className="small">{tr("Create a key to protect your API. You\u2019ll return here when it\u2019s ready.")}</p><Btn onClick={onCreateKey}>{tr("Create an API key")}</Btn></div>)}</> : null)}
|
||||
{tr(step === 2 ? <><p className="small muted">{tr(keyReady ? "Only apps with your Strata API key can use this connection." : "Without an API key, anyone who finds the link can use your model.")}</p>{tr(keyReady ? <div className="tunnel-check"><Icon name="shield"/><div><strong>{tr("API key ready")}</strong><span>{tr("Use your existing key in the app you connect.")}</span></div></div> : <div className="tunnel-key-step"><Icon name="shield" size={30}/><p className="small">{tr("Create a key to protect your API. You\u2019ll return here when it\u2019s ready. You can also continue without one, but anyone who finds the link can then use your model.")}</p><Btn onClick={onCreateKey}>{tr("Create an API key (recommended)")}</Btn></div>)}</> : null)}
|
||||
{tr(mode === 'domain' && step === 3 ? <><p className="small muted">{tr("Create a tunnel in your ")}<a href="https://dash.cloudflare.com/" target="_blank" rel="noreferrer">{tr("Cloudflare dashboard \u2197")}</a>{tr(". Copy the token from its connector setup.")}</p><label className="input-field"><span>{tr("Tunnel token")}</span><input type="password" className="st-input" autoComplete="off" spellCheck={false} value={token} onChange={e => setToken(e.target.value)} placeholder={tr("Paste only the token after --token")}/></label><label className="input-field"><span>{tr("Your hostname")}</span><input className="st-input" autoComplete="off" spellCheck={false} value={hostname} onChange={e => setHostname(e.target.value)} placeholder={tr("ai.example.com")}/></label><div><p className="small muted">{tr("Add a published application route for this hostname with this service URL:")}</p><div className="install-command"><code>{tunnel?.originUrl ?? 'Loading…'}</code><IconBtn name="copy" title={tr("Copy tunnel service URL")} disabled={!tunnel?.originUrl} onClick={() => void navigator.clipboard.writeText(tunnel!.originUrl)}/></div></div><p className="xs muted">{tr("The token is used for this connection only and is not saved.")}</p></> : null)}
|
||||
{tr(step === last ? <><p className="small muted">{tr(mode === 'quick' ? 'Cloudflare will create your address when you connect.' : 'Start the connector for the domain you configured.')}</p><dl className="tunnel-review"><div><dt>{tr("Address")}</dt><dd>{tr(mode === 'quick' ? 'Free temporary URL' : hostname)}</dd></div><div><dt>{tr("Connector")}</dt><dd><Icon name="check" size={14}/>{tr(tunnel?.connector?.version ? "Installed · cloudflared {0}" : "Installed", tunnel?.connector?.version ? [tunnel.connector.version] : undefined)}</dd></div><div><dt>{tr("Access")}</dt><dd><Icon name="shield" size={14}/>{tr("API key required")}</dd></div></dl><p className="small muted">{tr(mode === 'quick' ? 'The address changes each time. Turn off streaming in your client; replies arrive all at once.' : 'Use this hostname and your Strata API key in your client. Streaming is supported.')}</p><p className="xs muted">{tr("Only the inference API is shared. Your computer and Strata must stay on.")}</p></> : null)}
|
||||
{tr(step === last ? <><p className="small muted">{tr(mode === 'quick' ? 'Cloudflare will create your address when you connect.' : 'Start the connector for the domain you configured.')}</p><dl className="tunnel-review"><div><dt>{tr("Address")}</dt><dd>{tr(mode === 'quick' ? 'Free temporary URL' : hostname)}</dd></div><div><dt>{tr("Connector")}</dt><dd><Icon name="check" size={14}/>{tr(tunnel?.connector?.version ? "Installed · cloudflared {0}" : "Installed", tunnel?.connector?.version ? [tunnel.connector.version] : undefined)}</dd></div><div><dt>{tr("Access")}</dt><dd><Icon name="shield" size={14}/>{tr(keyReady ? "API key required" : "No API key: anyone with the link can use it")}</dd></div></dl><p className="small muted">{tr(mode === 'quick' ? 'The address changes each time. Turn off streaming in your client; replies arrive all at once.' : 'Use this hostname and your Strata API key in your client. Streaming is supported.')}</p><p className="xs muted">{tr("Only the inference API is shared. Your computer and Strata must stay on.")}</p></> : null)}
|
||||
</section>)}
|
||||
{tr(error || tunnel?.error ? <ErrorNotice message={error || tunnel?.error || 'Connection failed'} next={tr("Check your internet connection, then retry.")}/> : null)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { tr } from '../lib/i18n';
|
||||
import { useId, useState } from 'react';
|
||||
import { Btn, Icon } from './ui';
|
||||
import {
|
||||
CPU_ONLY_LINE, NO_CARD_LINE, OFF_AT_OR_BELOW_GB, gpuLabel, noteText, order, recommend, setRole, shortName, vramOn, warnings,
|
||||
type Role, type RoleCard, type Roles, type RoleWarning,
|
||||
} from '../lib/gpu-roles';
|
||||
|
||||
const ROLE_WORD: Record<Role, string> = { main: 'Main', helper: 'Helper', off: 'Off' };
|
||||
const HINT: Record<Role, string> = { main: 'Does the main work', helper: 'Holds part of the model', off: 'Not used' };
|
||||
|
||||
/** The one plain line above the cards, built from translated pieces. */
|
||||
export function recommendationLine(cards: RoleCard[], vendor?: string | null): string {
|
||||
const rec = recommend(cards, vendor);
|
||||
if (rec.main === null) return tr('No graphics card that Strata can use was found.');
|
||||
const byi = new Map(cards.map(c => [c.index, c]));
|
||||
const m = byi.get(rec.main)!;
|
||||
if (cards.length === 1) return tr('Strata will use your {0}.', [shortName(m)]);
|
||||
const why = { same: 'the cards are alike', memory: 'it has more memory', faster: 'it is faster', only: 'it is the only card', none: '' }[rec.why];
|
||||
let s = tr('{0} is Main: {1}.', [shortName(m), tr(why)]);
|
||||
const helpers = Object.keys(rec.roles).map(Number).filter(i => rec.roles[i] === 'helper').map(i => shortName(byi.get(i)!));
|
||||
if (helpers.length) s += ' ' + tr(helpers.length > 1 ? '{0} help.' : '{0} helps.', [helpers.join(', ')]);
|
||||
for (const i of Object.keys(rec.roles).map(Number)) if (rec.roles[i] === 'off') {
|
||||
const code = rec.reasons[i], c = byi.get(i)!;
|
||||
s += ' ' + tr('{0} starts Off: {1}', [shortName(c), code === 'cannot_run' ? tr('Strata cannot run it.') : code === 'igpu' ? tr('built-in graphics.') : tr('{0} GB or less.', [OFF_AT_OR_BELOW_GB])]);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function warningText(w: RoleWarning, cards: RoleCard[]): string {
|
||||
const c = w.card !== null ? cards.find(x => x.index === w.card) : undefined;
|
||||
if (w.code === 'cannot_run') return tr('{0} cannot run Strata.', [c ? shortName(c) : '']);
|
||||
if (w.code === 'igpu_main') return tr('{0} is built-in graphics; it is slow as Main.', [c ? shortName(c) : '']);
|
||||
if (w.code === 'cpu_only') return tr(w.text === CPU_ONLY_LINE ? CPU_ONLY_LINE : NO_CARD_LINE);
|
||||
return tr('Not enough memory for this setup: about {0} GB is read from the drive.', [w.text.match(/about (\d+) GB/)?.[1] ?? '']);
|
||||
}
|
||||
|
||||
export function noteLine(code: string | null, cards: RoleCard[]): string {
|
||||
if (!code) return '';
|
||||
const kind = code.split(':')[0], c = cards.find(x => x.index === Number(code.split(':')[1]));
|
||||
if (kind === 'old_main_helper') return tr('{0} is now a Helper.', [c ? shortName(c) : '']);
|
||||
if (kind === 'promoted') return tr('{0} is now Main.', [c ? shortName(c) : '']);
|
||||
if (kind === 'only_main') return tr('The only card that is on has to be Main.');
|
||||
return noteText(code, cards);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main / Helper / Off for every graphics card (the mockup GpuRoles-mockup.html). One click changes a role; "Back to recommended" restores the
|
||||
* recommendation. Warnings are short and never block Load. `roles` are the roles in force (the recommendation until the person changes one).
|
||||
*/
|
||||
export function GpuRoles({ cards, roles, custom, recommendedRoles, vendor, onChange, onBack, memoryGb, ramGb, spillGb = 0, engineRunsCpuOnly = false, disabled = false, usedBy }: {
|
||||
cards: RoleCard[];
|
||||
roles: Roles;
|
||||
/** The person changed a role: "Your choice", and "Back to recommended" is offered. */
|
||||
custom: boolean;
|
||||
/** The roles Strata recommends (the tags and "Back to recommended" use them); the pure recommendation when not given. */
|
||||
recommendedRoles?: Roles;
|
||||
vendor?: string | null;
|
||||
onChange: (next: Roles) => void;
|
||||
onBack: () => void;
|
||||
memoryGb?: number | null;
|
||||
ramGb?: number | null;
|
||||
/** About how many GB of the model are read from the drive with these cards (0 when everything fits or it is unknown). */
|
||||
spillGb?: number;
|
||||
engineRunsCpuOnly?: boolean;
|
||||
disabled?: boolean;
|
||||
/** Names of the loaded models that already use a card ("In use by ..."). */
|
||||
usedBy?: (card: number) => string[];
|
||||
}) {
|
||||
const titleId = useId();
|
||||
const [note, setNote] = useState('');
|
||||
const rec = { ...recommend(cards, vendor), ...(recommendedRoles ? { roles: recommendedRoles } : {}) };
|
||||
const single = cards.length === 1;
|
||||
const warns = warnings(roles, cards, { spillGb, engineRunsCpuOnly });
|
||||
const cardWarn = (i: number) => warns.filter(w => w.card === i);
|
||||
const globalWarn = warns.filter(w => w.card === null);
|
||||
const lst = order(roles, cards);
|
||||
const change = (idx: number, role: Role) => {
|
||||
const out = setRole(roles, cards, idx, role);
|
||||
setNote(noteLine(out.note, cards));
|
||||
onChange(out.roles);
|
||||
};
|
||||
const back = () => { setNote(''); onBack(); };
|
||||
const fix = (w: RoleWarning) => w.fix && change(w.fix.card, w.fix.role);
|
||||
const fixLabel = (w: RoleWarning) => {
|
||||
if (!w.fix) return '';
|
||||
const target = cards.find(c => c.index === w.fix!.card);
|
||||
return w.fix.role === 'off' ? tr('Turn it off') : tr('Make {0} Main', [target ? shortName(target) : '']);
|
||||
};
|
||||
const have = vramOn(roles, cards) + Math.max(0, (ramGb ?? 0) - 10);
|
||||
return <div className="gpuroles" aria-labelledby={titleId} data-custom={custom}>
|
||||
<div className="gpuroles__head">
|
||||
<span className="native-choice__label" id={titleId}>{tr('Graphics cards')}</span>
|
||||
{custom ? <button type="button" className="gpuroles__link" disabled={disabled} onClick={back}>{tr('Back to recommended')}</button> : null}
|
||||
</div>
|
||||
<div className="gpuroles__reco" data-custom={custom}>
|
||||
<Icon name="bolt" size={18}/>
|
||||
<p><strong>{tr(custom ? 'Your choice.' : 'Recommended.')}</strong> {custom ? tr('You changed the roles. Strata will use exactly these.') : recommendationLine(cards, vendor)}</p>
|
||||
</div>
|
||||
<div className="gpuroles__rows" role="list">
|
||||
{cards.map(c => {
|
||||
const role = roles[c.index] ?? 'off', isRec = rec.roles[c.index] === role, w = cardWarn(c.index), other = usedBy?.(c.index) ?? [];
|
||||
const shared = !!(c.integrated || c.uma || c.unified_gb);
|
||||
const options: Role[] = single ? ['main', 'off'] : ['main', 'helper', 'off'];
|
||||
return <div className="gpuroles__row" role="listitem" data-role={role} key={c.index}>
|
||||
<div className="gpuroles__top">
|
||||
<span className="gpuroles__ico"><Icon name="gpu" size={20}/></span>
|
||||
<span className="gpuroles__name">
|
||||
<strong>{gpuLabel(c)} · {c.name}{isRec ? <span className="gpuroles__tag">{tr('Recommended')}</span> : <span className="gpuroles__tag gpuroles__tag--neutral">{tr('Recommended: {0}', [tr(ROLE_WORD[rec.roles[c.index] ?? 'off'])])}</span>}</strong>
|
||||
<small>{shared ? tr("Uses your computer's memory (built-in graphics)") : tr('{0} GB memory', [Math.round(c.vram_gb)])} · {tr(HINT[role])}</small>
|
||||
</span>
|
||||
<span className="gpuroles__roles" role="radiogroup" aria-label={tr('Role of {0}', [gpuLabel(c)])}>
|
||||
{options.map(r => <button type="button" role="radio" key={r} data-r={r} aria-checked={role === r} disabled={disabled} onClick={() => change(c.index, r)}>{tr(ROLE_WORD[r])}</button>)}
|
||||
</span>
|
||||
</div>
|
||||
{other.length ? <div className="gpuroles__note gpuroles__note--info"><Icon name="info" size={16}/><p>{tr('In use by {0}', [other.join(', ')])}</p></div> : null}
|
||||
{w.map(x => <div className="gpuroles__note gpuroles__note--warn" key={x.code}><Icon name="warning" size={16}/><p>{warningText(x, cards)}</p>{x.fix ? <Btn size="sm" disabled={disabled} onClick={() => fix(x)}>{fixLabel(x)}</Btn> : null}</div>)}
|
||||
</div>;
|
||||
})}
|
||||
</div>
|
||||
<div className="gpuroles__toast" role="status" aria-live="polite">{note ? <div className="gpuroles__note gpuroles__note--info"><Icon name="info" size={16}/><p>{note}</p></div> : null}</div>
|
||||
{globalWarn.map(x => <div className="gpuroles__note gpuroles__note--warn" key={x.code}><Icon name="warning" size={16}/><p>{warningText(x, cards)}</p>{x.code === 'cpu_only' && custom ? <Btn size="sm" disabled={disabled} onClick={back}>{tr('Back to recommended')}</Btn> : null}</div>)}
|
||||
<details className="gpuroles__details">
|
||||
<summary>{tr('Details')}</summary>
|
||||
<div className="gpuroles__body">
|
||||
<dl>
|
||||
<dt>{tr('Main')}</dt><dd>{tr('The card that does the final steps of every answer: it writes the next words, runs the draft-ahead step and checks it, and keeps your conversation. Pick your fastest card with plenty of memory. There is only ever one.')}</dd>
|
||||
<dt>{tr('Helper')}</dt><dd>{tr('Holds a share of the model and its fast memory, so more of it fits on graphics cards instead of being read from your drive. Prompts are read about 20% faster.')}</dd>
|
||||
<dt>{tr('Off')}</dt><dd>{tr('Strata does not touch this card. Good for a card you use for something else, such as a game or your screen.')}</dd>
|
||||
</dl>
|
||||
{memoryGb ? <div className="gpuroles__fit" data-state={lst.length ? (spillGb >= 1 ? 'bad' : 'ok') : 'bad'}><span className="gpuroles__dot" aria-hidden="true"/><span>{lst.length ? (spillGb >= 1 ? tr('The model needs about {0} GB. These cards and your memory give about {1} GB, so about {2} GB would be read from your drive.', [Math.round(memoryGb), Math.round(have), Math.round(spillGb)]) : tr('The model needs about {0} GB. These cards and your memory give about {1} GB. It fits.', [Math.round(memoryGb), Math.round(have)])) : tr('Only your processor and memory are used.')}</span></div> : null}
|
||||
<div className="gpuroles__expert xs muted">{lst.length ? tr('For experts: the engine starts on cards {0} (Main last).', [lst.join(',')]) : null} {custom ? '' : tr('Not saved while you keep the recommendation: Strata picks again if you add or remove a card.')}</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>;
|
||||
}
|
||||
@@ -78,6 +78,19 @@ export function GuidedSetup() {
|
||||
throw e;
|
||||
} });
|
||||
}
|
||||
/* Files the person already has are checked and adopted by the "Use a local GGUF" page, not made the data folder (that downloaded them again, #3507). */
|
||||
async function ownModel(path?: string) {
|
||||
await perform(async () => { leaving.current = true; if (saveTimer.current)
|
||||
clearTimeout(saveTimer.current); try {
|
||||
await persist(step, { manual: true }, true);
|
||||
openSetup(false);
|
||||
navigate('models', 'local', path ? { path } : undefined);
|
||||
}
|
||||
catch (e) {
|
||||
leaving.current = false;
|
||||
throw e;
|
||||
} });
|
||||
}
|
||||
async function finish() {
|
||||
if (leaving.current)
|
||||
return;
|
||||
@@ -192,9 +205,9 @@ export function GuidedSetup() {
|
||||
<OnboardingMemory hardware={hardware} native={native?.hardware}/>{tr(!version || version.needsNewerEngine ? <p className="small muted">{tr("This model needs a newer engine. Update your engine or import a local GGUF for this model.")}</p> : null)}
|
||||
<div className="onboard-version-heading"><strong>{tr("Choose a version")}</strong><span>{tr(version ? `${catalogVersionLabel(version)} selected` : '')}</span></div>
|
||||
<div className="onboard-versions" role="radiogroup" aria-label={tr("Model version")} onKeyDown={radioKeys}>{tr(cards.map(v => versionCard(v)))}</div>
|
||||
{tr(others.length ? <div className="onboard-more-versions"><VersionPicker versions={others} selectedId={version?.id ?? ''} hardware={hardware ? { ram_gb: native?.hardware?.ram_gb ?? hardware.memory.totalGb, vram_gb: Math.max(0, ...(native?.hardware?.gpus ?? []).filter(g => g.usable !== false).map(g => g.vram_gb)) } : null} downloaded={readyIds} showRecommended={false} moreLabel="More versions" onSelect={id => { setSize(id); setPlan(null); }} onOwnGguf={() => setFolderOpen(true)}/></div> : null)}
|
||||
{tr(others.length ? <div className="onboard-more-versions"><VersionPicker versions={others} selectedId={version?.id ?? ''} hardware={hardware ? { ram_gb: native?.hardware?.ram_gb ?? hardware.memory.totalGb, vram_gb: Math.max(0, ...(native?.hardware?.gpus ?? []).filter(g => g.usable !== false).map(g => g.vram_gb)) } : null} downloaded={readyIds} showRecommended={false} moreLabel="More versions" onSelect={id => { setSize(id); setPlan(null); }} onOwnGguf={() => void ownModel()}/></div> : null)}
|
||||
<div className="onboard-folder"><Icon name="folder" size={20}/><div><strong>{tr(directory ? 'Your model folder' : 'Already downloaded the model?')}</strong><small>{tr(directory || "Choose the folder and we'll start your models with Strata.")}</small></div><Btn size="sm" onClick={() => setFolderOpen(v => !v)}>{tr(folderOpen ? 'Close' : directory ? 'Change' : 'Choose folder')}</Btn></div>
|
||||
{tr(folderOpen ? <OnboardingFolder directory={folder} onChoose={path => void perform(async () => { await nativeAction('directory', { directory: path }); setDirectory(path); setFolderOpen(false); setPlan(null); })}/> : null)}
|
||||
{tr(folderOpen ? <OnboardingFolder directory={folder} onChoose={(path, ggufFiles) => ggufFiles > 0 && !/strata/i.test(path.split(/[\\/]/).filter(Boolean).pop() ?? '') ? void ownModel(path) : void perform(async () => { await nativeAction('directory', { directory: path }); setDirectory(path); setFolderOpen(false); setPlan(null); })}/> : null)}
|
||||
</> : <div className="onboard-engine-connect"><Icon name="models" size={24}/><strong>{tr("Connect your Strata installation")}</strong><Btn onClick={() => void skip(false, 'Engine')}>{tr("Connect engine")}</Btn></div>)}
|
||||
</div> : step === 3 ? <div className="onboard-prepare-stage">
|
||||
<section className="onboard-download"><EngineWarning native={native}/><div className="onboard-download-identity"><Icon name="models" size={19}/><strong>{tr(selectedFamily?.title)}</strong><span>{tr(version ? catalogVersionLabel(version) : '')}</span><small>{tr(version?.model)}</small></div><OnboardingArt kind="download" progress={progress?.percent ?? 0} counter={counter}/><div className="onboard-download-info">
|
||||
@@ -228,7 +241,7 @@ function OnboardingError({ message, onFolder }: {
|
||||
}
|
||||
function OnboardingFolder({ directory, onChoose }: {
|
||||
directory: string;
|
||||
onChoose: (path: string) => void;
|
||||
onChoose: (path: string, ggufFiles: number) => void;
|
||||
}) {
|
||||
const [path, setPath] = useState(directory), [folder, setFolder] = useState<LocalModelFolder | null>(null), [pending, setPending] = useState(false), [error, setError] = useState('');
|
||||
async function browse(input: string, initial = false) { setPending(true); setError(''); try {
|
||||
@@ -252,5 +265,5 @@ function OnboardingFolder({ directory, onChoose }: {
|
||||
setPending(false);
|
||||
} }
|
||||
useEffect(() => { void browse(directory, true); }, []);
|
||||
return <section className="onboard-folder-browser" aria-label={tr("Choose your model folder")}><form onSubmit={e => { e.preventDefault(); void browse(path); }}><input className="st-input" aria-label={tr("Existing model folder")} value={path} onChange={e => { setPath(e.target.value); setFolder(null); }} placeholder={tr("Full folder path")} spellCheck={false}/><Btn type="submit" size="sm" disabled={pending || !path.trim()}>{tr(pending ? 'Checking…' : 'Open')}</Btn></form><p>{tr("Choose the Strata data folder that holds your model downloads. Matching files are reused; missing files are prepared automatically.")}</p><div className="onboard-folder-shortcuts">{folder?.shortcuts?.map(item => <Btn key={item.path} size="sm" disabled={pending} onClick={() => void browse(item.path)}>{tr(item.name)}</Btn>)}</div><div className="onboard-folder-files">{tr(folder?.parent ? <button onClick={() => void browse(folder.parent!)}><Icon name="folder" size={16}/>{tr(" Up a folder")}</button> : null)}{folder?.entries.filter(e => e.kind === 'folder').slice(0, 30).map(e => <button key={e.path} onClick={() => void browse(e.path)}><Icon name="folder" size={16}/>{tr(e.name)}<Icon name="chevron" size={12}/></button>)}{tr(folder ? <small>{tr(folder.entries.filter(e => e.kind === 'model').length)}{tr(" GGUF files in this folder. Nested downloads are checked by the installer.")}</small> : null)}</div>{tr(folder ? <Btn variant="primary" size="sm" disabled={pending} onClick={() => onChoose(folder.directory)}>{tr("Use this folder")}</Btn> : null)}{tr(error ? <ErrorNotice message={error}/> : null)}</section>;
|
||||
return <section className="onboard-folder-browser" aria-label={tr("Choose your model folder")}><form onSubmit={e => { e.preventDefault(); void browse(path); }}><input className="st-input" aria-label={tr("Existing model folder")} value={path} onChange={e => { setPath(e.target.value); setFolder(null); }} placeholder={tr("Full folder path")} spellCheck={false}/><Btn type="submit" size="sm" disabled={pending || !path.trim()}>{tr(pending ? 'Checking…' : 'Open')}</Btn></form><p>{tr("Choose the Strata data folder that holds your model downloads. Matching files are reused; missing files are prepared automatically.")}</p><div className="onboard-folder-shortcuts">{folder?.shortcuts?.map(item => <Btn key={item.path} size="sm" disabled={pending} onClick={() => void browse(item.path)}>{tr(item.name)}</Btn>)}</div><div className="onboard-folder-files">{tr(folder?.parent ? <button onClick={() => void browse(folder.parent!)}><Icon name="folder" size={16}/>{tr(" Up a folder")}</button> : null)}{folder?.entries.filter(e => e.kind === 'folder').slice(0, 30).map(e => <button key={e.path} onClick={() => void browse(e.path)}><Icon name="folder" size={16}/>{tr(e.name)}<Icon name="chevron" size={12}/></button>)}{tr(folder ? <small>{tr(folder.entries.filter(e => e.kind === 'model').length)}{tr(" GGUF files in this folder. Nested downloads are checked by the installer.")}</small> : null)}</div>{tr(folder ? <Btn variant="primary" size="sm" disabled={pending} onClick={() => onChoose(folder.directory, folder.entries.filter(e => e.kind === 'model').length)}>{tr("Use this folder")}</Btn> : null)}{tr(error ? <ErrorNotice message={error}/> : null)}</section>;
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
import { tr, localeTag } from '../lib/i18n';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Bar, Btn, Dialog, Icon } from './ui';
|
||||
import { api, ApiError } from '../lib/client';
|
||||
import { nativeAction, refreshNative, useNative, type NativeJob, type NativeState } from '../lib/native';
|
||||
import { nativeShortName } from '../lib/native-names';
|
||||
import { sizeGb } from '../lib/format';
|
||||
|
||||
type Choice = 'move' | 'keep' | 'empty';
|
||||
interface Plan {
|
||||
ok: boolean;
|
||||
issue: { code: string; message: string; details: string } | null;
|
||||
old: string; new: string; same_drive: boolean;
|
||||
free_bytes: number; models_bytes: number; needed_bytes: number;
|
||||
models: { group: string; name: string; bytes: number }[];
|
||||
choices: Record<Choice, { possible: boolean; reason: string }>;
|
||||
folder: string; picked: string; wholeDrive: boolean; shown: string; current: string; running: number; busy: boolean;
|
||||
}
|
||||
interface Outcome { done?: boolean; stopped?: boolean; failed?: boolean; code?: string; moved?: string[]; remaining?: string[]; bytes?: number; total?: number; from?: string; to?: string; sameDrive?: boolean }
|
||||
|
||||
const gb = (bytes: number) => sizeGb(bytes / 1e9);
|
||||
const driveOf = (path: string) => /^[A-Za-z]:/.test(path) ? path.slice(0, 2) : path;
|
||||
const list = (items: string[]) => { try { return new Intl.ListFormat(localeTag(), { type: 'conjunction' }).format(items); } catch { return items.join(', '); } };
|
||||
|
||||
/** The model a folder name belongs to, in the words the Storage list uses ("Gyro-S"); the draft layer and loose files have no name of their own. */
|
||||
function modelName(group: string, native: NativeState | null) {
|
||||
const hit = native?.installed?.models.find(m => m.model.toLowerCase() === group.toLowerCase());
|
||||
return hit ? nativeShortName(hit.model) : null;
|
||||
}
|
||||
|
||||
function timeLeft(seconds: number) {
|
||||
if (seconds <= 0) return null;
|
||||
if (seconds < 60) return tr('Less than a minute left');
|
||||
const minutes = Math.ceil(seconds / 60);
|
||||
if (minutes <= 1) return tr('About a minute left');
|
||||
if (minutes < 120) return tr('About {0} minutes left', [minutes]);
|
||||
return tr('About {0} hours left', [Math.ceil(minutes / 60)]);
|
||||
}
|
||||
|
||||
function Failure({ message, next, details }: { message: string; next?: string; details?: string }) {
|
||||
return <div className="error-notice" role="alert"><Icon name="error" size={18}/><div><strong>{tr(message)}</strong>{next ? <p>{tr(next)}</p> : null}{details ? <details className="error-detail"><summary className="small">{tr('Details')}</summary><pre className="small muted">{details}</pre></details> : null}</div></div>;
|
||||
}
|
||||
|
||||
/** Settings > Storage > Models folder: the card, the folder dialog with its three choices, the progress of a move and what came of it. */
|
||||
export function ModelsFolderCard({ blocked }: { blocked: boolean }) {
|
||||
const native = useNative();
|
||||
const [plan, setPlan] = useState<Plan | null>(null);
|
||||
const [stage, setStage] = useState<'issue' | 'drive' | 'choose' | null>(null);
|
||||
const [choice, setChoice] = useState<Choice>('move');
|
||||
const [pending, setPending] = useState(false);
|
||||
const [error, setError] = useState<{ message: string; details?: string } | null>(null);
|
||||
const [typing, setTyping] = useState(false), [typed, setTyped] = useState('');
|
||||
const [jobId, setJobId] = useState<string | null>(null);
|
||||
const [outcome, setOutcome] = useState<{ job: NativeJob } | null>(null);
|
||||
const [notice, setNotice] = useState('');
|
||||
const [stopping, setStopping] = useState(false);
|
||||
const [unfinished, setUnfinished] = useState<{ from: string; to: string } | null>(null);
|
||||
const [resumed, setResumed] = useState<{ from: string; to: string } | null>(null);
|
||||
const folder = native?.downloadDirectory ?? '';
|
||||
const free = native?.downloadFreeBytes ?? null;
|
||||
const usage = native?.installed?.usage?.total_bytes ?? 0;
|
||||
const moving = native?.jobs.find(j => j.kind === 'models-move' && j.state === 'running') ?? null;
|
||||
const finished = jobId ? native?.jobs.find(j => j.id === jobId && j.kind === 'models-move' && j.state !== 'running') : undefined;
|
||||
useEffect(() => { if (finished && !outcome) { setOutcome({ job: finished }); setStopping(false); void refreshNative(true); } }, [finished, outcome]);
|
||||
const total = native?.downloadTotalBytes ?? null;
|
||||
const used = free !== null && total ? Math.min(100, Math.max(1, ((total - free) / total) * 100)) : 0;
|
||||
useEffect(() => { if (moving && !jobId) setJobId(moving.id); }, [moving, jobId]);
|
||||
|
||||
/* A move that a crash or a stop left half done: found again at the next start, with its two choices. */
|
||||
useEffect(() => {
|
||||
if (moving || outcome) return;
|
||||
let live = true;
|
||||
api.get<{ interrupted: { from: string; to: string } | null }>('/api/native/models-folder/interrupted')
|
||||
.then(r => { if (live) setUnfinished(r.interrupted); }).catch(() => { if (live) setUnfinished(null); });
|
||||
return () => { live = false; };
|
||||
}, [moving, outcome, folder]);
|
||||
async function finishMove() {
|
||||
if (!unfinished) return;
|
||||
setPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await api.post<NativeJob>('/api/native/models-folder/resume', { stopRunning: true });
|
||||
setResumed(unfinished);
|
||||
setUnfinished(null);
|
||||
if (result.id) { setJobId(result.id); setOutcome(null); await refreshNative(true); }
|
||||
}
|
||||
catch (e) { setError({ message: (e as Error).message, details: ((e as ApiError).body as { details?: string } | undefined)?.details }); }
|
||||
finally { setPending(false); }
|
||||
}
|
||||
async function leaveMove() {
|
||||
if (!unfinished) return;
|
||||
setPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
await api.post('/api/native/models-folder/leave', {});
|
||||
setNotice(tr('The partial copies were removed. Your models are in {0}.', [unfinished.from]));
|
||||
setUnfinished(null);
|
||||
await refreshNative(true);
|
||||
}
|
||||
catch (e) { setError({ message: (e as Error).message, details: ((e as ApiError).body as { details?: string } | undefined)?.details }); }
|
||||
finally { setPending(false); }
|
||||
}
|
||||
|
||||
function leave() { setPlan(null); setStage(null); setError(null); setTyping(false); }
|
||||
async function pick(path = '') {
|
||||
setPending(true);
|
||||
setError(null);
|
||||
setNotice('');
|
||||
try {
|
||||
const result = await api.post<{ cancelled?: boolean; plan?: Plan }>('/api/native/models-folder/plan', { path });
|
||||
if (result.cancelled || !result.plan) return;
|
||||
const next = result.plan;
|
||||
setTyping(false);
|
||||
setPlan(next);
|
||||
if (next.issue?.code === 'same') { setPlan(null); return; }
|
||||
setChoice(next.choices.move.possible ? 'move' : 'keep');
|
||||
setStage(next.issue ? 'issue' : next.wholeDrive ? 'drive' : 'choose');
|
||||
}
|
||||
catch (e) {
|
||||
if (e instanceof ApiError && e.status === 501) { setTyping(true); setError({ message: e.message }); }
|
||||
else setError({ message: (e as Error).message, details: ((e as ApiError).body as { details?: string } | undefined)?.details });
|
||||
}
|
||||
finally { setPending(false); }
|
||||
}
|
||||
async function start() {
|
||||
if (!plan) return;
|
||||
setPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await api.post<NativeJob & { done?: boolean; setAside?: string[] }>('/api/native/models-folder/start', { path: plan.picked, choice, stopRunning: true });
|
||||
if (result.id) { setJobId(result.id); setOutcome(null); setStage(null); await refreshNative(true); setPlan(prev => prev); return; }
|
||||
setNotice(choice === 'keep' ? tr('Strata now also uses {0}. New downloads go there, and your current models stay where they are.', [plan.shown])
|
||||
: tr('Strata now uses {0}. Your old model files are still in {1}; Strata does not delete them.', [plan.shown, plan.current]));
|
||||
await refreshNative(true);
|
||||
leave();
|
||||
}
|
||||
catch (e) { setError({ message: (e as Error).message, details: ((e as ApiError).body as { details?: string } | undefined)?.details }); }
|
||||
finally { setPending(false); }
|
||||
}
|
||||
async function stop() {
|
||||
if (!moving) return;
|
||||
setStopping(true);
|
||||
try { await nativeAction(`operations/${moving.id}/cancel`); }
|
||||
catch (e) { setError({ message: (e as Error).message }); }
|
||||
}
|
||||
function closeResult() { setOutcome(null); setJobId(null); setResumed(null); leave(); setNotice(''); }
|
||||
|
||||
const progress = moving?.progress as { done: number; total: number; file: string; group: string; index: number; count: number; secondsLeft: number; sameDrive: boolean } | null | undefined;
|
||||
const percent = progress && progress.total > 0 ? Math.round((progress.done / progress.total) * 100) : 0;
|
||||
const planNames = useMemo(() => (plan?.models ?? []).map(m => ({ ...m, label: modelName(m.group, native) })).filter(m => m.label), [plan, native]);
|
||||
const savedPlan = useMemo(() => plan, [plan]);
|
||||
|
||||
return <>
|
||||
<section className="storage-directory" aria-labelledby="models-folder-title">
|
||||
<div className="storage-directory__heading"><span><Icon name="folder" size={18}/><strong id="models-folder-title">{tr('Models folder')}</strong></span><Btn size="sm" disabled={!folder || blocked || pending || !!moving} onClick={() => void pick()}>{tr(pending && !plan ? 'Waiting for the folder…' : 'Change folder')}</Btn></div>
|
||||
<p className="small muted">{tr('Strata saves the big model files here.')}</p>
|
||||
<code className="storage-directory__path">{folder || tr('Reading model directory…')}</code>
|
||||
{free !== null ? <div className="models-free"><Bar value={used} label={tr('Disk use')}/><div className="models-free__row small"><span><strong>{tr('{0} free', [gb(free)])}</strong> {tr('on this drive')}</span><span className="muted">{tr('Your models use {0}', [gb(usage)])}</span></div></div> : null}
|
||||
{blocked && !moving ? <p className="small muted">{tr('Finish or stop the download to change the folder.')}</p> : null}
|
||||
{typing ? <form className="storage-directory__edit" onSubmit={e => { e.preventDefault(); if (typed.trim()) void pick(typed.trim()); }}>
|
||||
<label htmlFor="models-folder-path" className="small">{tr('Directory path')}</label>
|
||||
<input id="models-folder-path" className="st-input" value={typed} onChange={e => setTyped(e.target.value)} placeholder={tr('Full path to an existing folder')} spellCheck={false} autoFocus/>
|
||||
<div className="row"><Btn variant="primary" type="submit" disabled={pending || !typed.trim()}>{tr('Use this directory')}</Btn></div>
|
||||
</form> : null}
|
||||
{unfinished && !moving ? <div className="models-notice models-notice--info" role="alert"><Icon name="info" size={18}/><div>
|
||||
<strong>{tr('A move was interrupted. Finish it or leave it.')}</strong><br/>
|
||||
{tr('Strata was moving your models from {0} to {1} when it stopped. Nothing is lost.', [unfinished.from, unfinished.to])}
|
||||
<p className="small muted">{tr('Finishing puts all your models in {1}. Leaving it keeps them in {0} and removes the partial copies from {1}.', [unfinished.from, unfinished.to])}</p>
|
||||
<div className="row"><Btn variant="primary" size="sm" disabled={pending || blocked} onClick={() => void finishMove()}>{tr('Finish the move')}</Btn><Btn size="sm" disabled={pending || blocked} onClick={() => void leaveMove()}>{tr('Leave it')}</Btn></div>
|
||||
</div></div> : null}
|
||||
{notice ? <p className="small storage-directory__notice" role="status"><Icon name="check" size={15}/>{notice}</p> : null}
|
||||
{error && !stage ? <Failure message={error.message} details={error.details}/> : null}
|
||||
</section>
|
||||
|
||||
<Dialog className="models-dialog" open={stage === 'issue' && !!plan?.issue} compact title={tr(plan?.issue?.code === 'not_found' ? "That folder isn't there" : "That folder can't be used")} onClose={leave}
|
||||
actions={<>{plan?.issue?.code === 'not_found' ? null : <Btn onClick={leave}>{tr('Cancel')}</Btn>}<Btn variant="primary" onClick={() => { leave(); void pick(); }}>{tr('Choose another folder')}</Btn></>}>
|
||||
{plan?.issue ? <Failure message={plan.issue.message} details={plan.issue.details}
|
||||
next={plan.issue.code === 'not_writable' ? 'Choose a folder you own, for example one in Documents or on another drive.' : plan.issue.code === 'not_found' ? 'Check that the drive is plugged in, then pick the folder again. Your models were not touched.' : undefined}/> : null}
|
||||
</Dialog>
|
||||
|
||||
<Dialog className="models-dialog" open={stage === 'drive' && !!plan} compact title={tr('Use this folder for your models?')} onClose={leave}
|
||||
actions={<><Btn onClick={leave}>{tr('Cancel')}</Btn><Btn variant="primary" onClick={() => setStage('choose')}>{tr('Continue')}</Btn></>}>
|
||||
{plan ? <>
|
||||
<div className="models-picked"><span className="small muted">{tr('New folder')}</span><code className="storage-directory__path">{plan.shown}</code><span className="small muted">{tr('{0} free on this drive', [gb(plan.free_bytes)])}</span></div>
|
||||
<div className="models-notice models-notice--info"><Icon name="info" size={18}/><div>{tr('You picked the whole drive {0}. Strata keeps its files in a folder of their own, so your models go in {1}.', [plan.picked, plan.shown])}</div></div>
|
||||
<p className="small muted">{tr('The three choices come next.')}</p>
|
||||
</> : null}
|
||||
</Dialog>
|
||||
|
||||
<Dialog className="models-dialog" open={stage === 'choose' && !!plan} title={tr('Use this folder for your models?')} onClose={() => { if (!pending) leave(); }}
|
||||
actions={plan ? <>{plan.choices.move.possible ? <Btn disabled={pending} onClick={leave}>{tr('Cancel')}</Btn> : <Btn disabled={pending} onClick={() => { leave(); void pick(); }}>{tr('Choose another folder')}</Btn>}<Btn variant="primary" disabled={pending} onClick={() => void start()}>{tr(pending ? 'Working…' : choice === 'move' ? 'Move my models' : choice === 'keep' ? 'Use both folders' : 'Start empty')}</Btn></> : null}>
|
||||
{plan ? <ChooseBody plan={plan} choice={choice} setChoice={setChoice} names={planNames} error={error}/> : null}
|
||||
</Dialog>
|
||||
|
||||
<Dialog className="models-dialog" open={!!moving && !outcome} compact noClose title={tr('Moving your models')} onClose={() => undefined}
|
||||
actions={<Btn disabled={stopping} onClick={() => void stop()}>{tr(stopping ? 'Stopping…' : 'Stop moving')}</Btn>}>
|
||||
<div className="models-free"><Bar value={percent} label={tr('Progress')}/><div className="models-free__row small"><span><strong>{tr('{0} of {1} GB', [progress ? (progress.done / 1e9 >= 10 ? Math.round(progress.done / 1e9) : (progress.done / 1e9).toFixed(1)) : 0, Math.round((progress?.total ?? 0) / 1e9)])}</strong></span><span className="muted">{progress && !progress.sameDrive ? timeLeft(progress.secondsLeft) : null}</span></div></div>
|
||||
<p className="small">{tr('Keep Strata open. You can keep using other apps. Your models cannot be loaded until the move is done.')}</p>
|
||||
{progress ? <details className="models-details"><summary>{tr('Details')}</summary><ul>
|
||||
<li>{tr('Now copying: {0}, file {1} of {2}', [modelName(progress.group, native) ?? progress.group, progress.index, progress.count])}</li>
|
||||
<li>{tr('From {0} to {1}', [savedPlan?.current ?? resumed?.from ?? '', savedPlan?.folder ?? resumed?.to ?? ''])}</li></ul></details> : null}
|
||||
</Dialog>
|
||||
|
||||
<ResultDialog outcome={outcome?.job ?? null} plan={savedPlan} native={native} onClose={closeResult} onAgain={() => { closeResult(); void pick(); }}/>
|
||||
</>;
|
||||
}
|
||||
|
||||
function ChooseBody({ plan, choice, setChoice, names, error }: { plan: Plan; choice: Choice; setChoice: (c: Choice) => void; names: (Plan['models'][number] & { label: string | null })[]; error: { message: string; details?: string } | null }) {
|
||||
const none = plan.models_bytes === 0;
|
||||
const short = !none && !plan.choices.move.possible;
|
||||
const lacking = Math.max(0, plan.needed_bytes - plan.free_bytes);
|
||||
const options: { id: Choice; title: string; text: string; need: string }[] = [
|
||||
{ id: 'move', title: 'Move my models there', text: none ? 'You have no models to move.' : plan.same_drive ? 'Strata moves your {0} of models to the new folder. This is the same drive, so it takes a moment.' : 'Strata copies your {0} of models to the new folder, checks them, then removes the old copies.',
|
||||
need: none ? '' : plan.same_drive ? 'Needs no extra space.' : 'Needs {0} free in the new folder. You have {1}.' },
|
||||
{ id: 'keep', title: 'Keep them where they are and also use the new folder', text: 'Nothing is copied. Your current models keep working, and new downloads go to the new folder.', need: short ? 'Needs no space now. New downloads must fit in {1}.' : 'Needs no space now.' },
|
||||
{ id: 'empty', title: 'Start empty', text: 'Strata uses the new folder from now on and does not list your current models. Your old files are not deleted.', need: 'Needs no space now. Each model you download later needs its own space.' },
|
||||
];
|
||||
const values = [gb(plan.models_bytes), gb(plan.free_bytes)];
|
||||
const running = plan.running > 0;
|
||||
return <>
|
||||
<div className="models-picked"><span className="small muted">{tr('New folder')}</span><code className="storage-directory__path">{plan.shown}</code><span className="small muted">{tr('{0} free on this drive', [gb(plan.free_bytes)])}</span></div>
|
||||
{short ? <div className="models-notice models-notice--warn" role="alert"><Icon name="warning" size={18}/><div><strong>{tr('Your models need {0}, this drive has {1} free.', [gb(plan.needed_bytes), gb(plan.free_bytes)])}</strong><br/>{tr('Free up about {0} there, pick another folder, or choose one of the other two options.', [gb(lacking)])}</div></div> : null}
|
||||
<fieldset className="models-choices" aria-label={tr('What should happen to your models')}>
|
||||
{options.map(o => { const possible = plan.choices[o.id].possible; return <label className="models-choice" key={o.id} data-disabled={!possible || undefined}>
|
||||
<input type="radio" name="models-folder-choice" checked={choice === o.id} disabled={!possible} onChange={() => setChoice(o.id)}/>
|
||||
<span><strong>{tr(o.title)}</strong>{o.id === 'move' && possible && !none ? <span className="models-choice__tag">{tr('Recommended')}</span> : null}<br/><span className="small">{tr(o.text, o.id === 'move' ? [gb(plan.models_bytes)] : undefined)}</span></span>
|
||||
{o.need ? <span className="models-choice__need">{tr(o.need, o.id === 'move' ? [gb(plan.needed_bytes), gb(plan.free_bytes)] : [values[0], values[1]])}</span> : null}
|
||||
</label>; })}
|
||||
</fieldset>
|
||||
{running ? <p className="small muted">{tr('A running model is stopped first; load it again when the move is done.')}</p> : null}
|
||||
{error ? <Failure message={error.message} details={error.details}/> : null}
|
||||
<details className="models-details"><summary>{tr('Details')}</summary><ul>
|
||||
{names.length ? <li>{tr('{0} would move.', [list(names.map(m => `${m.label}, ${gb(m.bytes)}`))])}</li> : null}
|
||||
{short ? <li>{tr('Free space is measured on the drive that holds {0}.', [plan.shown])}</li> : <>
|
||||
<li>{tr('Old folder: {0}', [plan.current])}</li>
|
||||
<li>{tr('Strata copies one file at a time and checks its size before it removes the old one.')}</li>
|
||||
<li>{tr('You can stop at any time; files already moved stay usable.')}</li></>}
|
||||
</ul></details>
|
||||
</>;
|
||||
}
|
||||
|
||||
function ResultDialog({ outcome, plan, native, onClose, onAgain }: { outcome: NativeJob | null; plan: Plan | null; native: NativeState | null; onClose: () => void; onAgain: () => void }) {
|
||||
if (!outcome) return null;
|
||||
const result = (outcome.result ?? {}) as Outcome;
|
||||
const sized = (groups: string[]) => list(groups.map(g => { const m = plan?.models.find(x => x.group === g); const name = modelName(g, native); return name ? (m ? `${name} (${gb(m.bytes)})` : name) : null; }).filter((x): x is string => !!x));
|
||||
if (outcome.state === 'error') {
|
||||
const full = result.code === 'disk_full';
|
||||
const where = driveOf(result.to ?? '');
|
||||
const some = (result.moved?.length ?? 0) > 0;
|
||||
return <Dialog open compact title={tr(full ? 'The drive ran out of space' : 'The move stopped')} onClose={onClose}
|
||||
actions={<><Btn onClick={onClose}>{tr('Close')}</Btn><Btn variant="primary" onClick={onAgain}>{tr('Choose another folder')}</Btn></>}>
|
||||
<div className="error-notice" role="alert"><Icon name="error" size={18}/><div>
|
||||
<strong>{full ? tr('{0} filled up after {1}, so the move stopped.', [where, gb(result.bytes ?? 0)]) : tr(outcome.error ?? 'The models could not be moved.')}</strong>
|
||||
<p>{full ? (some ? tr('Nothing was lost. Models already moved are in the new folder, the rest are still in {0}, and the half-copied file was removed. Free up space on {1} or choose another folder, then try again.', [result.from ?? '', where])
|
||||
: tr('Nothing was lost. All your models are still in {0}, and the half-copied file was removed. Free up space on {1} or choose another folder, then try again.', [result.from ?? '', where]))
|
||||
: tr('Nothing was lost. Your models that were not moved are still in {0}. Choose another folder to try again.', [result.from ?? ''])}</p>
|
||||
{outcome.details ? <details className="error-detail"><summary className="small">{tr('Details')}</summary><pre className="small muted">{outcome.details}</pre></details> : null}
|
||||
</div></div>
|
||||
</Dialog>;
|
||||
}
|
||||
if (result.stopped) {
|
||||
const moved = sized(result.moved ?? []), left = sized(result.remaining ?? []);
|
||||
return <Dialog open compact title={tr('Moving stopped')} onClose={onClose} actions={<Btn variant="primary" onClick={onClose}>{tr('Close')}</Btn>}>
|
||||
<div className="models-notice models-notice--info"><Icon name="info" size={18}/><div><strong>{tr('Nothing was lost.')}</strong><br/>
|
||||
{moved ? tr('{0} is in the new folder.', [moved]) + ' ' : ''}{left ? tr('{0} is still in the old folder.', [left]) + ' ' : ''}{moved && left ? tr('Both work.') : ''}</div></div>
|
||||
<p className="small muted">{tr('Models in both folders are listed as normal. You can finish moving later with Change folder.')}</p>
|
||||
</Dialog>;
|
||||
}
|
||||
return <Dialog open compact title={tr('Your models are moved')} onClose={onClose} actions={<Btn variant="primary" onClick={onClose}>{tr('Done')}</Btn>}>
|
||||
<div className="models-ok"><span className="models-ok__mark"><Icon name="check" size={20}/></span><div><strong>{(() => { const [before, after] = tr('{0} now live in {1}', [gb(result.total ?? 0), '@@']).split('@@'); return <>{before}<code className="models-path">{plan?.shown ?? result.to ?? ''}</code>{after}</>; })()}</strong>
|
||||
{result.sameDrive ? null : <><br/><span className="small muted">{tr('The old copies were removed and {0} is free again on {1}.', [gb(result.total ?? 0), driveOf(result.from ?? '')])}</span></>}</div></div>
|
||||
</Dialog>;
|
||||
}
|
||||
@@ -1,25 +1,28 @@
|
||||
import { tr } from '../lib/i18n';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Screen } from './layout';
|
||||
import { Btn, ErrorNotice, Icon } from './ui';
|
||||
import { api } from '../lib/client';
|
||||
import { parseLocation } from '../lib/router';
|
||||
import { nativeAction, useNative, type NativeJob } from '../lib/native';
|
||||
import { nativeCatalogModels, catalogVersionLabel } from '../lib/native-catalog';
|
||||
import { sizeGb } from '../lib/format';
|
||||
import { NativeProgress } from './NativeModels';
|
||||
|
||||
interface Preview { path: string; preview: { file_count: number; total_bytes: number; file_set_status: string; issues: string[]; messages?: string[]; projectors?: { name: string; status: string; pinned: boolean; plain: string }[]; reads_pictures?: boolean; candidate_selection: { family: string; model: string } | null } }
|
||||
interface Preview { path: string; preview: { model_dir?: string; plain_issues?: string[]; already_set_up?: { data_dir?: string | null; pack?: string | null } | null; file_count: number; total_bytes: number; file_set_status: string; issues: string[]; messages?: string[]; projectors?: { name: string; status: string; pinned: boolean; plain: string }[]; reads_pictures?: boolean; candidate_selection: { family: string; model: string } | null } }
|
||||
|
||||
/** The words for why a folder of GGUF files cannot be used: a sentence and what to do, the checker's own codes under Details. */
|
||||
function problem(issues: string[]): { message: string; next: string } {
|
||||
const has = (re: RegExp) => issues.some(i => re.test(i));
|
||||
if (has(/incomplete_or_invalid_shard_set|multiple_model_groups/))
|
||||
return { message: 'Some parts of this model are missing, or the folder holds more than one model.', next: 'Put every part of one model in one folder, then check again.' };
|
||||
if (has(/multiple_model_groups/))
|
||||
return { message: 'This folder holds more than one model.', next: 'Type the full path of the .gguf file of the one you want, or choose the folder of that model.' };
|
||||
if (has(/incomplete_or_invalid_shard_set/))
|
||||
return { message: 'Some parts of this model are missing.', next: 'Put every part of one model (all the -0000N-of-0000M files) in one folder, then check again.' };
|
||||
if (has(/unsupported_architecture|unsupported_or_missing_architecture/))
|
||||
return { message: 'This model type isn’t supported by the Strata engine.', next: 'Add it as a Custom model API instead: start it with llama-server, then open Settings > Models > Add model provider > Custom model API.' };
|
||||
if (has(/unrecognized_or_unsupported_model_variant|expert_quantization|tokenizer/))
|
||||
return { message: 'Strata can’t run these files yet.', next: 'Strata runs the Qwen3.8-Flash-Next models. Download one from Models, or choose a folder with its files.' };
|
||||
return { message: 'These files could not be checked.', next: 'Check that the folder holds complete GGUF files, then try again.' };
|
||||
return { message: 'These files could not be checked.', next: 'Check that the folder holds complete GGUF files, then try again. The file named under Details says what is wrong with it.' };
|
||||
}
|
||||
|
||||
/** "Use a local GGUF": choose the folder of a model you already have, Strata checks it, and prepares and loads it without copying
|
||||
@@ -47,13 +50,18 @@ export function NativeLocalModel({ onBack }: { onBack: () => void }) {
|
||||
catch (e) { setError(e instanceof Error ? e.message : String(e)); }
|
||||
finally { setPending(false); }
|
||||
}
|
||||
/* The first-run page hands over the folder the person chose (#3507): it is checked at once, as if they had pressed Check files. */
|
||||
useEffect(() => { const path = parseLocation(window.location.hash).query.get('path'); if (path) { setTyped(path); void check(path); } }, []);
|
||||
/* Files that sit in a Strata data folder (<data>/models/<tag>) with their prepared model data beside them: that folder is used as the data
|
||||
* folder, so the prepared data is not made a second time. A folder the installer would not accept as a data folder is not passed. */
|
||||
const ownDataFolder = (() => { const d = checked?.preview.already_set_up?.pack ? checked.preview.already_set_up.data_dir : null; return d && /strata/i.test(d.split(/[\\/]/).filter(Boolean).pop() ?? '') ? d : null; })();
|
||||
async function prepare() {
|
||||
if (!usable || !selection || !checked)
|
||||
return;
|
||||
setPending(true);
|
||||
setError('');
|
||||
try {
|
||||
const result = await nativeAction<NativeJob>('operations', { kind: 'install', args: { family: selection.family, model: selection.model, gguf_dir: checked.path, context: native?.catalog?.recommendation?.context ?? 32768, vision: checked.preview.projectors?.some(p => p.status === 'ok' && p.pinned) && native?.visionReader ? 'yes' : 'no', kv: 'int8', loadAfter: true } });
|
||||
const result = await nativeAction<NativeJob>('operations', { kind: 'install', args: { family: selection.family, model: selection.model, gguf_dir: checked.preview.model_dir ?? checked.path, ...(ownDataFolder ? { data_dir: ownDataFolder } : {}), context: native?.catalog?.recommendation?.context ?? 32768, vision: checked.preview.projectors?.some(p => p.status === 'ok' && p.pinned) && native?.visionReader ? 'yes' : 'no', kv: 'int8', loadAfter: true } });
|
||||
setJobId(result.id);
|
||||
}
|
||||
catch (e) { setError(e instanceof Error ? e.message : String(e)); }
|
||||
@@ -64,8 +72,8 @@ export function NativeLocalModel({ onBack }: { onBack: () => void }) {
|
||||
<div className="native-model-layout">
|
||||
<div className="stack stack--lg">
|
||||
<section className="st-card native-choice"><h2 className="h3">{tr("Use a local GGUF…")}</h2>
|
||||
<p className="small muted">{tr("Already have a supported model on this computer? Choose the folder that holds its files. Strata checks them, prepares the model and loads it. Nothing is copied or downloaded.")}</p>
|
||||
<label className="input-field"><span>{tr("Folder with the GGUF files")}</span><input className="st-input" value={typed} placeholder={tr("Full path to the folder")} spellCheck={false} onChange={e => setTyped(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && typed.trim()) void check(typed); }}/></label>
|
||||
<p className="small muted">{tr("Already have a supported model on this computer? Choose the folder that holds its files, or one of its .gguf files. Strata checks them, prepares the model and loads it. Nothing is copied or downloaded.")}</p>
|
||||
<label className="input-field"><span>{tr("Folder with the GGUF files, or one .gguf file")}</span><input className="st-input" value={typed} placeholder={tr("Full path to the folder or the file")} spellCheck={false} onChange={e => setTyped(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && typed.trim()) void check(typed); }}/></label>
|
||||
<div className="native-settings-actions"><Btn icon="folder" disabled={pending || !!active} onClick={() => void check('')}>{tr("Choose folder")}</Btn><Btn disabled={pending || !typed.trim() || !!active} onClick={() => void check(typed)}>{tr(pending && !jobId ? 'Checking…' : 'Check files')}</Btn></div>
|
||||
</section>
|
||||
{tr(checked ? <section className="st-card native-choice">{tr(usable ? <>
|
||||
@@ -77,6 +85,8 @@ export function NativeLocalModel({ onBack }: { onBack: () => void }) {
|
||||
</> : <>
|
||||
<h2 className="h3">{tr(found!.message)}</h2>
|
||||
<p className="small muted">{tr(found!.next)}</p>
|
||||
{tr(checked.preview.plain_issues?.map(m => <p className="small muted" key={m}>{tr(m)}</p>))}
|
||||
{tr(checked.preview.messages?.map(m => <p className="small muted" key={m}>{tr(m)}</p>))}
|
||||
{tr(checked.preview.issues.length ? <details><summary className="small">{tr("Details")}</summary><pre className="small muted error-detail-text">{checked.preview.issues.join(String.fromCharCode(10))}</pre></details> : null)}
|
||||
</>)}</section> : null)}
|
||||
{tr(active ? <NativeProgress job={active}/> : job?.state === 'error' ? <ErrorNotice message={job.error ?? job.summary} detail={job.details}/> : job?.state === 'done' ? <p className="small" role="status"><Icon name="check" size={15}/> {tr(job.summary)}</p> : null)}
|
||||
|
||||
@@ -4,7 +4,9 @@ import { Screen } from './layout';
|
||||
import { VersionPicker } from './VersionPicker';
|
||||
import { FamilySwitch } from './FamilySwitch';
|
||||
import { PcFitCard } from './PcFitCard';
|
||||
import { fitOn, runCards } from '../lib/native-fit';
|
||||
import { fitOn, memoryNeeded } from '../lib/native-fit';
|
||||
import { GpuRoles } from './GpuRoles';
|
||||
import { fromLegacy, normalize as normalizeRoles, order as roleOrder, recommend as recommendRoles, rows as roleRows, sameRoles, vramOn, type RoleCard, type Roles } from '../lib/gpu-roles';
|
||||
import { Btn, Dialog, ErrorNotice, Icon, Select } from './ui';
|
||||
import { installProgress, installStage, stageText, useDownloadEta } from '../lib/download-eta';
|
||||
import { useKept } from '../lib/kept-state';
|
||||
@@ -37,8 +39,7 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
// The choices are kept while the page is closed (Models -> Monitor -> Models used to start them over and a person lost track of what was downloading).
|
||||
const [family, setFamily] = useKept('models.family', ''), [size, setSize] = useKept('models.size', ''), [context, setContext] = useKept('models.context', 32768), [vision, setVision] = useKept('models.vision', 'yes');
|
||||
const manual = useStore(s => s.setup?.choices?.onboarding?.manual ?? false);
|
||||
const [expert, setExpert] = useKept('models.expert', manual), [multiple, setMultiple] = useKept('models.multiple', false), [gpus, setGpus] = useKept<number[]>('models.gpus', []), [kv, setKv] = useKept('models.kv', 'int8'), [lowRam, setLowRam] = useKept('models.lowRam', 'auto'), [split, setSplit] = useKept('models.split', 'auto'), [deviceChoice, setDeviceChoice] = useKept('models.device', 'auto');
|
||||
const [cardsKey, setCardsKey] = useKept<string | null>('models.cards', null);
|
||||
const [expert, setExpert] = useKept('models.expert', manual), [multiple, setMultiple] = useKept('models.multiple', false), [roles, setRoles] = useKept<Roles | null>('models.roles', null), [kv, setKv] = useKept('models.kv', 'int8'), [lowRam, setLowRam] = useKept('models.lowRam', 'auto'), [split, setSplit] = useKept('models.split', 'auto');
|
||||
const [resourceProfile, setResourceProfile] = useKept<'work' | 'performance'>('models.profile', 'work');
|
||||
const [pending, setPending] = useState(false), [error, setError] = useState(''), [plan, setPlan] = useState<Record<string, any> | null>(null), [loadedError, setLoadedError] = useState(''), [tuneOpen, setTuneOpen] = useState(false), [jobId, setJobId] = useKept('models.job', ''), [manage, setManage] = useState(false);
|
||||
const [toastedJob, setToastedJob] = useKept('models.toasted', ''), [appliedSig, setAppliedSig] = useKept('models.applied', '');
|
||||
@@ -53,7 +54,7 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
// The model that is running is shown as running, with Unload: not offered for loading again.
|
||||
const downloaded = native?.installed?.models ?? [], allSizes = families.flatMap(f => f.sizes);
|
||||
// This computer for the fit figures: the biggest usable graphics card, or the ones chosen under Expert settings.
|
||||
const runGpus = runCards(native?.hardware?.gpus, native?.hardware?.engine_vendor);
|
||||
const engineVendor = native?.hardware?.engine_vendor ?? null, roleCards: RoleCard[] = roleRows(native?.hardware?.gpus as RoleCard[] | undefined, engineVendor);
|
||||
const pcGpus = (native?.hardware?.gpus ?? []).filter(g => g.usable !== false), pcGpu = [...pcGpus].sort((x, y) => y.vram_gb - x.vram_gb)[0] ?? null;
|
||||
const pcRam = native?.hardware?.ram_gb || hardware?.memory.totalGb || null, pcVram = pcGpu?.vram_gb ?? 0;
|
||||
// What the models that stay loaded (Run multiple models) already take: the fit figures are for what is left.
|
||||
@@ -85,19 +86,20 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
// Graphics cards (strata_control/gpu_fit.py: the one rule of the app, the Terminal and setup): the recommended option is pre-selected, the person may pick another
|
||||
// A card a loaded model already uses (a second model on it usually does not fit): said on its row, and not the pre-selected one while another is free.
|
||||
const usedBy = (cards: number[]) => (native?.instances ?? []).filter(i => i.status !== 'error' && i.model !== selectedSize?.id && i.gpus.some(g => cards.includes(g))).map(i => i.label);
|
||||
const cardOptions = selectedSize?.gpu_options && selectedSize.gpu_options.options.length >= 2 ? selectedSize.gpu_options : null;
|
||||
const loadedCards = loadedSelected ? cardOptions?.options.find(o => o.cards.length === loadedSelected.gpus.length && o.cards.every(c => loadedSelected.gpus.includes(c))) : undefined;
|
||||
const activeCards = cardOptions ? (cardOptions.options.find(o => o.key === cardsKey) ?? loadedCards ?? (multiple ? cardOptions.options.find(o => o.recommended && !usedBy(o.cards).length) ?? cardOptions.options.find(o => !usedBy(o.cards).length) : undefined) ?? cardOptions.options.find(o => o.recommended) ?? cardOptions.options[0]) : null;
|
||||
const cardsGpus: number[] = activeCards?.cards ?? [];
|
||||
const chosenCards = expert && gpus.length ? gpus : cardsGpus;
|
||||
const fitHardware = { ram_gb: pcRam ? Math.max(0, pcRam - keptRam) : pcRam, vram_gb: Math.max(0, pcVram - keptVramOn(chosenCards)) };
|
||||
// "(card 1)" in a card name is the number the screens call GPU 2 (Monitor, the model picker, the loaded list): one numbering everywhere.
|
||||
const gpuNumbers = (text: string) => text.replace(/\(card (\d+)\)/g, (_m, n) => `(GPU ${Number(n) + 1})`);
|
||||
// The fit sentence for a card choice, less what the kept models take of RAM and of that card's memory.
|
||||
const lessKept = (words: string, cards: number[]) => { const gone = Math.round(keptRam + keptVramOn(cards)); return gone > 0 ? words.replace(/of the (\d+) GB available/, (_m, n) => `of the ${Math.max(0, Number(n) - gone)} GB available`) : words; };
|
||||
const shownSize = useRef(selectedSize?.id);
|
||||
useEffect(() => { if (shownSize.current !== selectedSize?.id) { shownSize.current = selectedSize?.id; setCardsKey(null); } }, [selectedSize?.id]);
|
||||
const values = useMemo(() => ({ family: selectedSize?.sourceFamily, model: selectedSize?.model, context, vision: selectedSize?.images ? vision : 'no', kv, low_ram: lowRam, resourceProfile, ...(expert && gpus.length === 1 ? { gpu: gpus[0] } : expert && gpus.length ? { gpus: gpus.join(',') } : cardsGpus.length > 1 ? { gpus: cardsGpus.join(',') } : cardsGpus.length === 1 ? { gpu: cardsGpus[0] } : {}), loadAfter: true, multiple: expert && multiple }), [selectedSize?.sourceFamily, selectedSize?.model, context, vision, kv, lowRam, resourceProfile, gpus.join(','), cardsGpus.join(','), expert, multiple]);
|
||||
// Main / Helper / Off per card (src/lib/gpu-roles.ts, strata_control/gpu_roles.py): the recommendation until the person changes a role; a loaded model shows what it was started with.
|
||||
// With Run multiple models, a card another loaded model already uses starts Off for this one (while a free card exists).
|
||||
const busy = (i: number) => expert && multiple && usedBy([i]).length > 0, freeCards = roleCards.filter(c => !busy(c.index));
|
||||
const recommended = freeCards.length && freeCards.length < roleCards.length ? { ...recommendRoles(freeCards, engineVendor), roles: { ...Object.fromEntries(roleCards.map(c => [c.index, 'off'])), ...recommendRoles(freeCards, engineVendor).roles } as Roles } : recommendRoles(roleCards, engineVendor);
|
||||
const loadedRoles = loadedSelected && roleCards.length > 1 ? fromLegacy(loadedSelected.gpus, roleCards, 'as_given') : null;
|
||||
const effectiveRoles: Roles = roleCards.length > 1 ? (roles ? normalizeRoles(roles, roleCards) : loadedRoles ?? recommended.roles) : recommended.roles;
|
||||
const customRoles = !sameRoles(effectiveRoles, recommended.roles);
|
||||
const changeRoles = (next: Roles) => setRoles(sameRoles(next, recommended.roles) ? null : next);
|
||||
const cardsGpus: number[] = roleCards.length > 1 ? roleOrder(effectiveRoles, roleCards) : [];
|
||||
const chosenCards = cardsGpus;
|
||||
const fitHardware = { ram_gb: pcRam ? Math.max(0, pcRam - keptRam) : pcRam, vram_gb: Math.max(0, (roleCards.length > 1 ? vramOn(effectiveRoles, roleCards) : pcVram) - keptVramOn(chosenCards)) };
|
||||
// A layer split written for another number of cards no longer fits: back to automatic.
|
||||
useEffect(() => { if (cardsGpus.length <= 1 && split !== 'auto') setSplit('auto'); }, [cardsGpus.length]);
|
||||
const values = useMemo(() => ({ family: selectedSize?.sourceFamily, model: selectedSize?.model, context, vision: selectedSize?.images ? vision : 'no', kv, low_ram: lowRam, resourceProfile, ...(cardsGpus.length > 1 ? { gpus: cardsGpus.join(',') } : cardsGpus.length === 1 ? { gpu: cardsGpus[0] } : {}), loadAfter: true, multiple: expert && multiple }), [selectedSize?.sourceFamily, selectedSize?.model, context, vision, kv, lowRam, resourceProfile, cardsGpus.join(','), expert, multiple]);
|
||||
const downloadValues = { ...values, vision: selectedSize?.images && vision !== 'no' ? 'yes' : 'no', loadVision: selectedSize?.images ? vision : 'no' };
|
||||
useEffect(() => { if (!native?.catalog || family)
|
||||
return; const current = native.instances.find(i => i.id === native.defaultId); const rec = native.catalog.recommendation ?? { family: null, model: null }; const loadedFamily = families.find(f => f.sizes.some(s => s.id === current?.model)); const have = new Set((native.installed?.models ?? []).filter(m => m.ready).map(m => m.model)); const installedFamily = families.find(f => f.sizes.some(s => have.has(s.id))); const f = loadedFamily ?? installedFamily ?? families.find(f => f.family === rec.family || f.sizes.some(s => s.sourceFamily === rec.family)) ?? families[0]; if (f) {
|
||||
@@ -149,7 +151,7 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
} }
|
||||
async function load() { if (!selectedSize)
|
||||
return; await perform(async () => { if (installed?.ready && context === installed.context && (!selectedSize?.images || vision === 'no' || installed.images && installed.images !== 'off') && (!installed.kv || kv === installed.kv || kv === tunedNow?.fields.kv) && (lowRam === (installed.lowRamMode ?? 'auto') || lowRam === tunedNow?.fields.lowRam)) {
|
||||
const result = await nativeAction<NativeJob>('operations', { kind: 'load', args: { model: selectedSize.id, resourceProfile, vision: selectedSize?.images ? vision : 'no', multiple: expert && multiple, ...(expert && gpus.length ? { gpus, layerSplit: split } : cardsGpus.length ? { gpus: cardsGpus, gpusFromRecommendation: !cardsKey } : {}), values: {} } });
|
||||
const result = await nativeAction<NativeJob>('operations', { kind: 'load', args: { model: selectedSize.id, resourceProfile, vision: selectedSize?.images ? vision : 'no', multiple: expert && multiple, ...(roleCards.length > 1 ? (roles && customRoles ? { gpuRoles: effectiveRoles, layerSplit: expert && cardsGpus.length > 1 ? split : 'auto' } : cardsGpus.length ? { gpus: cardsGpus, gpusFromRecommendation: true, layerSplit: expert && cardsGpus.length > 1 ? split : 'auto' } : {}) : {}), values: {} } });
|
||||
setJobId(result.id);
|
||||
}
|
||||
else
|
||||
@@ -165,10 +167,10 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
setPending(false);
|
||||
} }
|
||||
async function download() { await perform(async () => { const result = await nativeAction<NativeJob>('operations', { kind: 'install', args: downloadValues }); setJobId(result.id); setPlan(null); }); }
|
||||
return <Screen plainHeader title={<span className="sr-only">{tr("Models")}</span>} titleDetail={<FamilySwitch families={families} selected={selectedFamily?.family} onPick={f => { setFamily(f.family); setSize((f.sizes.find(s => native?.installed?.models.some(m => m.model === s.id && m.ready)) ?? f.sizes[0])?.id ?? ''); setVision('yes'); setError(''); }}/>} actions={<Btn icon="sliders" aria-pressed={expert} aria-expanded={expert} aria-controls="native-expert-settings" onClick={() => { setExpert(v => !v); setMultiple(false); setGpus([]); setDeviceChoice('auto'); }}>{tr(expert ? 'Close expert mode' : 'Expert mode')}</Btn>}>
|
||||
return <Screen plainHeader title={<span className="sr-only">{tr("Models")}</span>} titleDetail={<FamilySwitch families={families} selected={selectedFamily?.family} onPick={f => { setFamily(f.family); setSize((f.sizes.find(s => native?.installed?.models.some(m => m.model === s.id && m.ready)) ?? f.sizes[0])?.id ?? ''); setVision('yes'); setError(''); }}/>} actions={<Btn icon="sliders" aria-pressed={expert} aria-expanded={expert} aria-controls="native-expert-settings" onClick={() => { setExpert(v => !v); setMultiple(false); }}>{tr(expert ? 'Close expert mode' : 'Expert mode')}</Btn>}>
|
||||
<div className="native-model-layout native-model-layout--fit">
|
||||
<div className="stack">
|
||||
{tr(native?.instances.length && expert ? <section className="st-card native-running"><h2 className="h4">{tr(native.instances.length === 1 ? 'Loaded model' : 'Loaded models')}</h2>{tr(native.instances.map(i => <div key={i.id}><span><strong>{tr(i.label)}</strong><small>{tr(i.gpus.length ? i.gpus.map(g => `GPU ${g + 1}`).join(' + ') : 'Automatic GPU')} · {tr(i.status === 'ready' ? i.phase : i.status)}{tr(native.defaultId === i.id ? ' · Chat default' : '')}</small>{tr(i.error ? <small className="inline-error">{tr(plainFailureText(i.error).message)}</small> : null)}</span>{tr(expert && multiple && native.defaultId !== i.id ? <Btn size="sm" variant="ghost" onClick={() => void loadedAction(`instances/${i.id}/default`)}>{tr("Use in chat")}</Btn> : null)}<Btn size="sm" disabled={pending || !!active} onClick={() => void loadedAction(`instances/${i.id}/unload`)}>{tr("Unload")}</Btn></div>))}{tr(loadedError ? <ErrorNotice message={loadedError} next={tr("Stop or answer the chat that is still using this model, then try again.")}/> : null)}</section> : null)}
|
||||
{tr(native?.instances.length && expert ? <section className="st-card native-running"><h2 className="h4">{tr(native.instances.length === 1 ? 'Loaded model' : 'Loaded models')}</h2>{tr(native.instances.map(i => <div key={i.id}><span><strong>{tr(i.label)}</strong><small>{tr(i.gpus.length ? i.gpus.map((g, n) => `GPU ${g + 1}${i.gpus.length > 1 && n === i.gpus.length - 1 ? ' (Main)' : ''}`).join(' + ') : 'Automatic GPU')} · {tr(i.status === 'ready' ? i.phase : i.status)}{tr(native.defaultId === i.id ? ' · Chat default' : '')}</small>{tr(i.error ? <small className="inline-error">{tr(plainFailureText(i.error).message)}</small> : null)}</span>{tr(expert && multiple && native.defaultId !== i.id ? <Btn size="sm" variant="ghost" onClick={() => void loadedAction(`instances/${i.id}/default`)}>{tr("Use in chat")}</Btn> : null)}<Btn size="sm" disabled={pending || !!active} onClick={() => void loadedAction(`instances/${i.id}/unload`)}>{tr("Unload")}</Btn></div>))}{tr(loadedError ? <ErrorNotice message={loadedError} next={tr("Stop or answer the chat that is still using this model, then try again.")}/> : null)}</section> : null)}
|
||||
<section className="st-card native-choice">{tr(selectedFamily && (!selectedSize || selectedSize.needsNewerEngine) ? <><h2 className="h3">{tr("This model needs a newer engine.")}</h2><p className="small muted">{tr("Update your engine or import a local GGUF for this model.")}</p><Btn onClick={() => { setSettingsSection('Engine'); go('settings'); }}>{tr("Update engine")}</Btn><Btn variant="ghost" onClick={onImport ?? onOtherModels}>{tr("Use a local GGUF\u2026")}</Btn></> : null)}{tr(!selectedFamily ? <><h2 className="h3">{tr("Set up Strata")}</h2><p className="small muted">{tr("Connect your local installation to choose an optimized model.")}</p><Btn variant="primary" onClick={onConnectEngine ?? (() => { setSettingsSection('Engine'); go('settings'); })}>{tr("Connect engine")}</Btn>{tr(native?.error ? <p className="small inline-error">{tr(native.error)}</p> : null)}<Btn variant="ghost" onClick={onOtherModels}>{tr("Other local models")}</Btn></> : null)}
|
||||
{tr(selectedSize ? <>
|
||||
<div className="native-choice__head"><span className="native-choice__label">{tr("Version")}</span><span className="native-choice__tools"><NativePresets values={values} onApply={v => { const f = families.find(f => f.family === v.family || f.sizes.some(s => s.sourceFamily === v.family)); if (!f)
|
||||
@@ -179,7 +181,8 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
<div className="native-choice__col"><span className="native-choice__label">{tr("Conversation context")}</span><Select ariaLabel={tr("Conversation context")} value={String(context)} options={contextChoices(native?.catalog?.contexts ?? [8192, 32768, 65536, 131072, 262144], context).map(n => ({ value: String(n), label: n % 1024 === 0 ? `${n / 1024}K` : `${n.toLocaleString()} tokens` }))} onChange={n => setContext(Number(n))}/></div>
|
||||
<div className="native-choice__col"><span className="native-choice__label">{tr("Computer use")}</span><div className="native-resource-choice"><button aria-pressed={resourceProfile === 'work'} data-selected={resourceProfile === 'work'} onClick={() => setResourceProfile('work')}><Icon name="computer" size={20}/><span><strong>{tr("Leave 20% for my work")}</strong><small>{tr("Headroom for your other apps")}</small></span>{tr(resourceProfile === 'work' ? <Icon name="check" size={15}/> : null)}</button><button aria-pressed={resourceProfile === 'performance'} data-selected={resourceProfile === 'performance'} onClick={() => setResourceProfile('performance')}><Icon name="bolt" size={20}/><span><strong>{tr("Full performance")}</strong><small>{tr("Give Strata priority")}</small></span>{tr(resourceProfile === 'performance' ? <Icon name="check" size={15}/> : null)}</button></div></div>
|
||||
</div>
|
||||
{tr(cardOptions ? <div className="native-cards"><span className="native-choice__label">{tr("Graphics cards")}</span><div className="native-cards__list" role="radiogroup" aria-label={tr("Graphics cards")}>{cardOptions.options.map(o => { const on = activeCards?.key === o.key; return <button key={o.key} type="button" role="radio" aria-checked={on} data-selected={on} disabled={!!active} className="native-cards__opt" onClick={() => setCardsKey(o.key)}><span className="native-cards__dot" aria-hidden="true"/><span><strong>{gpuNumbers(o.label)}</strong><small data-warn={o.verdict === 'spills' || o.verdict === 'tight' || usedBy(o.cards).length > 0}>{[lessKept(o.words, o.cards), ...o.notes, ...(usedBy(o.cards).length ? [tr('Already used by {0}', [usedBy(o.cards).join(', ')])] : [])].join('. ')}.</small></span></button>; })}{cardOptions.ignored.length ? <p className="small muted native-cards__note">{`Not used: ${cardOptions.ignored.map(g => `${g.name ?? 'a card'} (${g.why_ignored})`).join('; ')}.`}</p> : null}</div></div> : null)}
|
||||
{roleCards.length > 1 ? <GpuRoles cards={roleCards} roles={effectiveRoles} custom={customRoles} recommendedRoles={recommended.roles} vendor={engineVendor} disabled={!!active} onChange={changeRoles} onBack={() => setRoles(null)} memoryGb={selectedSize ? memoryNeeded(selectedSize) || null : null} ramGb={pcRam} spillGb={selectedSize ? fitOn(selectedSize, fitHardware).spillGb : 0} usedBy={i => usedBy([i])}/> : null}
|
||||
{tr(roleCards.length > 1 && (native?.hardware?.gpus ?? []).length > roleCards.length ? <p className="small muted native-cards__note">{tr('Not used: {0} (another make of graphics card than this engine drives).', [(native?.hardware?.gpus ?? []).filter(g => !roleCards.some(r => r.index === g.index && r.vendor === g.vendor)).map(g => g.name).join(', ')])}</p> : null)}
|
||||
{tr(selectedSize?.imagesCapable && !selectedSize.images ? <p className="small muted native-spill-note">{tr("This model can read pictures, but the engine installed here does not include the picture reader yet, so Read pictures is off. Update the engine in Settings → About to turn it on.")}</p> : null)}
|
||||
{tr(selectedSize?.images ? <><div className="native-image-reading"><label><input type="checkbox" checked={vision !== 'no'} disabled={!selectedSize?.images || !!active} onChange={e => setVision(e.target.checked ? 'yes' : 'no')}/><span><strong>{tr("Read pictures ")}<small>{tr("On by default")}</small></strong><small>{tr("Understand photos, screenshots and charts. The picture files download automatically. Turn it off to save memory.")}</small></span></label>{tr(expert && vision !== 'no' ? <Select ariaLabel={tr("Image encoder device")} value={vision === 'yes' ? 'yes' : vision} options={[{ value: 'yes', label: 'Automatic' }, { value: 'gpu', label: 'GPU' }, { value: 'cpu', label: 'CPU' }]} onChange={setVision}/> : null)}</div>{tr(vision !== 'no' && installed?.ready && installed.visionPrepared === false ? <div className="native-vision-missing" role="status"><span>{tr("Picture reading needs a few more files that are not on this computer yet.")}</span><Btn size="sm" disabled={pending || !!active} onClick={() => void download()}>{tr("Download picture-reading files")}</Btn></div> : null)}</> : null)}
|
||||
</> : null)}
|
||||
@@ -188,10 +191,10 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
{tr(job?.state === 'error' ? <ErrorNotice message={job.error ?? job.summary} action={tr("Retry")} onRetry={() => void load()}/> : null)}
|
||||
{tr(error ? <ErrorNotice message={error}/> : null)}
|
||||
{selectedSize && installed?.ready ? <NativeTuneBadge model={selectedSize.id} resourceProfile={resourceProfile} vision={selectedSize.images ? vision : 'no'}/> : null}
|
||||
<div className="native-model-actions">{tr(loadedSelected ? <><Btn disabled={pending || !!active} onClick={() => void perform(() => nativeAction(`instances/${loadedSelected.id}/unload`))}>{tr("Unload")}</Btn>{tr(onReady ? <Btn variant="primary" onClick={onReady}>{tr("Start chatting")}</Btn> : <span className="native-ready">{tr("Ready")}</span>)}</> : onReady && native?.instances.some(i => i.status === 'ready') ? <Btn variant="primary" onClick={onReady}>{tr("Start chatting")}</Btn> : <><Btn icon="gauge" disabled={!installed?.ready || !native?.available || !!active || !selectedSize || selectedSize.needsNewerEngine} onClick={() => setTuneOpen(true)}>{tr("Tune model")}</Btn><Btn variant="primary" icon="models" disabled={pending || !native?.available || !!active || !selectedSize || !!selectedSize.needsNewerEngine || expert && deviceChoice === 'split' && gpus.length < 2} onClick={() => void load()}>{tr(pending ? 'Checking…' : installed?.ready ? 'Load model' : 'Download & load')}</Btn></>)}</div>
|
||||
<div className="native-model-actions">{tr(loadedSelected ? <><Btn disabled={pending || !!active} onClick={() => void perform(() => nativeAction(`instances/${loadedSelected.id}/unload`))}>{tr("Unload")}</Btn>{tr(onReady ? <Btn variant="primary" onClick={onReady}>{tr("Start chatting")}</Btn> : <span className="native-ready">{tr("Ready")}</span>)}</> : onReady && native?.instances.some(i => i.status === 'ready') ? <Btn variant="primary" onClick={onReady}>{tr("Start chatting")}</Btn> : <><Btn icon="gauge" disabled={!installed?.ready || !native?.available || !!active || !selectedSize || selectedSize.needsNewerEngine} onClick={() => setTuneOpen(true)}>{tr("Tune model")}</Btn><Btn variant="primary" icon="models" disabled={pending || !native?.available || !!active || !selectedSize || !!selectedSize.needsNewerEngine} onClick={() => void load()}>{tr(pending ? 'Checking…' : installed?.ready ? 'Load model' : 'Download & load')}</Btn></>)}</div>
|
||||
</section>
|
||||
{tr(expert && IMAGE_GENERATION_VISIBLE ? <><details className="native-generation"><summary>{tr("Image generation ")}<span className="muted">{tr("Optional")}</span></summary><RunImageModel library={imageLibrary} disabled={pending || !!active} onChange={() => { }}/></details></> : null)}
|
||||
{tr(expert ? <section id="native-expert-settings" ref={expertSection} className="st-card native-execution"><h2 className="h4">{tr("Expert settings")}</h2>{tr(tunedNow ? <p className="small muted native-tuned-note">{tr("Showing the values of your tuning '{0}'.", [tunedNow.name])}</p> : null)}<label className="native-toggle"><span><strong>{tr("Run multiple models")}</strong><small>{tr("Loading another model keeps the others loaded.")}</small></span><button className="st-toggle" role="switch" aria-label={tr("Run multiple models")} aria-checked={multiple} onClick={() => setMultiple(v => !v)}/></label><h3 className="small strong">{tr("Run on")}</h3><p className="small muted">{tr("Automatic uses the installation\u2019s GPU placement. Choose a GPU, or split this model across several GPUs.")}</p><Select ariaLabel={tr("Run on")} value={deviceChoice} options={[{ value: 'auto', label: 'Automatic' }, ...runGpus.map(g => ({ value: String(g.index), label: `GPU ${g.index + 1} · ${g.name}` })), ...(runGpus.length > 1 ? [{ value: 'split', label: 'Split across GPUs…' }] : [])]} onChange={value => { setDeviceChoice(value); setGpus(value === 'auto' ? [] : value === 'split' ? runGpus.map(g => g.index) : [Number(value)]); }}/>{tr(deviceChoice === 'split' ? <div className="native-gpu-options">{tr(runGpus.map(g => <label key={g.index}><input type="checkbox" checked={gpus.includes(g.index)} onChange={e => setGpus(old => e.target.checked ? [...old, g.index] : old.filter(i => i !== g.index))}/><span>{tr("GPU ")}{tr(g.index + 1)} · {tr(g.name)}<small>{tr(g.vram_gb)}{tr(" GB VRAM")}</small></span></label>))}</div> : null)}{tr(gpus.length > 1 ? <label className="input-field"><span>{tr("Layer split")}</span><input className="st-input" value={split} placeholder={tr("auto or 16,32")} onChange={e => setSplit(e.target.value)}/></label> : null)}<div className="native-field-grid"><label className="input-field"><span>{tr("Context memory")}</span><Select ariaLabel={tr("Native KV format")} value={kv} options={[{ value: 'int8', label: 'INT8' }, { value: 'q4_0', label: 'Q4' }, { value: 'k8v4', label: 'K8 / V4' }]} onChange={setKv}/></label><label className="input-field"><span>{tr("Low-RAM mode")}</span><Select ariaLabel={tr("Native low RAM mode")} value={lowRam} options={['auto', 'resident', 'mmap'].map(v => ({ value: v, label: v === 'auto' ? 'Automatic' : v === 'resident' ? 'Keep experts in RAM' : 'Read experts from disk' }))} onChange={setLowRam}/></label></div>{tr(installed ? <><NativeSettingsFile model={installed.model}/><NativeSettings model={installed.model}/></> : <p className="small muted">{tr("More engine parameters become available after preparing this version.")}</p>)}</section> : null)}
|
||||
{tr(expert ? <section id="native-expert-settings" ref={expertSection} className="st-card native-execution"><h2 className="h4">{tr("Expert settings")}</h2>{tr(tunedNow ? <p className="small muted native-tuned-note">{tr("Showing the values of your tuning '{0}'.", [tunedNow.name])}</p> : null)}<label className="native-toggle"><span><strong>{tr("Run multiple models")}</strong><small>{tr("Loading another model keeps the others loaded.")}</small></span><button className="st-toggle" role="switch" aria-label={tr("Run multiple models")} aria-checked={multiple} onClick={() => setMultiple(v => !v)}/></label>{tr(cardsGpus.length > 1 ? <label className="input-field"><span>{tr("Layer split")}</span><input className="st-input" value={split} placeholder={tr("auto or 16,32")} onChange={e => setSplit(e.target.value)}/></label> : null)}<div className="native-field-grid"><label className="input-field"><span>{tr("Context memory")}</span><Select ariaLabel={tr("Native KV format")} value={kv} options={[{ value: 'int8', label: 'INT8' }, { value: 'q4_0', label: 'Q4' }, { value: 'k8v4', label: 'K8 / V4' }]} onChange={setKv}/></label><label className="input-field"><span>{tr("Low-RAM mode")}</span><Select ariaLabel={tr("Native low RAM mode")} value={lowRam} options={['auto', 'resident', 'mmap'].map(v => ({ value: v, label: v === 'auto' ? 'Automatic' : v === 'resident' ? 'Keep experts in RAM' : 'Read experts from disk' }))} onChange={setLowRam}/></label></div>{tr(installed ? <><NativeSettingsFile model={installed.model}/><NativeSettings model={installed.model}/></> : <p className="small muted">{tr("More engine parameters become available after preparing this version.")}</p>)}</section> : null)}
|
||||
</div>
|
||||
<aside className="stack stack--lg"><PcFitCard gpuName={pcGpu?.name} ramGb={pcRam} vramGb={pcVram} inUse={keptModels.length ? { ramGb: Math.round(keptRam), vramGb: Math.round(keptVramOn(chosenCards)) } : null} fit={selectedSize ? fitOn(selectedSize, fitHardware) : null}/><section className="st-card"><h2 className="h4">{tr("Model files")}</h2><p className="small muted">{tr("Strata prepares all shards, the draft layer and shared files. Completed downloads are reused.")}</p><code className="native-path">{installed?.dataFolder ?? native?.downloadDirectory ?? native?.installed?.data_folder ?? 'Choose an installation'}</code><Btn size="sm" variant="ghost" onClick={() => { setSettingsSection('Storage'); go('settings'); }}>{tr("Storage settings")}</Btn></section><Btn size="sm" variant="ghost" onClick={onOtherModels}>{tr("Other local models\u2026")}</Btn></aside>
|
||||
</div>
|
||||
@@ -231,4 +234,4 @@ catch (e) {
|
||||
}
|
||||
finally {
|
||||
setStopping(false);
|
||||
} } return <section className="st-card native-progress" aria-live="polite"><div className="section-toolbar"><h2 className="h4">{tr(job.kind === 'install' ? (label ? tr('Preparing {0}', [label]) : 'Preparing your model') : job.kind === 'load' ? 'Loading model' : job.kind === 'autotune' || job.kind === 'recheck' ? 'Tuning your model' : 'Engine operation')}</h2>{tr(job.kind === 'install' || job.kind === 'load' || job.kind === 'autotune' || job.kind === 'recheck' ? <div className="onboard-progress-actions">{tr(eta ? <span className="small muted">{eta}</span> : null)}<Btn size="sm" disabled={stopping} onClick={() => void stop()}>{tr(stopping ? 'Stopping…' : 'Stop')}</Btn></div> : null)}</div><p className="small">{tr(job.kind === 'install' ? (download && /^5\//.test(job.result?.install?.step ?? '') ? stageText(installStage(job.result?.install)) : installStepLabel(job.result?.install?.step)) : job.summary)}</p>{tr(download ? <><progress max={100} value={download.percent ?? 0}/><p className="xs muted">{tr(download.file === 'model files' ? 'Model files' : download.file === 'picture reader' ? 'Picture reader' : download.file)} · {tr(download.percent ?? 0)}%{tr(Number.isFinite(download.done_gb) && Number.isFinite(download.total_gb) ? ` · ${download.done_gb!.toFixed(1)} / ${download.total_gb!.toFixed(1)} GB` : '')}</p></> : null)}<DownloadParts parts={job.result?.install?.parts}/>{tr(detailsText ? <details><summary className="small">{tr("Details")}</summary><pre className="small muted error-detail-text">{detailsText}</pre></details> : null)}{tr(error ? <ErrorNotice message={error}/> : null)}</section>; }
|
||||
} } return <section className="st-card native-progress" aria-live="polite"><div className="section-toolbar"><h2 className="h4">{tr(job.kind === 'install' ? (label ? tr('Preparing {0}', [label]) : 'Preparing your model') : job.kind === 'load' ? 'Loading model' : job.kind === 'autotune' || job.kind === 'recheck' ? 'Tuning your model' : 'Engine operation')}</h2>{tr(job.kind === 'install' || job.kind === 'load' || job.kind === 'autotune' || job.kind === 'recheck' ? <div className="onboard-progress-actions">{tr(eta ? <span className="small muted">{eta}</span> : null)}<Btn size="sm" disabled={stopping} onClick={() => void stop()}>{tr(stopping ? 'Stopping…' : 'Stop')}</Btn></div> : null)}</div><p className="small">{tr(job.kind === 'install' ? (job.result?.install?.label ?? (download && /^5\//.test(job.result?.install?.step ?? '') ? stageText(installStage(job.result?.install)) : installStepLabel(job.result?.install?.step))) : job.summary)}</p>{tr(download ? <><progress max={100} value={download.percent ?? 0}/><p className="xs muted">{tr(download.file === 'model files' ? 'Model files' : download.file === 'picture reader' ? 'Picture reader' : download.file === 'checking files' ? 'Checking files' : download.file === 'MTP draft layer' ? 'Draft layer' : download.file)} · {tr(download.percent ?? 0)}%{tr(Number.isFinite(download.done_gb) && Number.isFinite(download.total_gb) ? ` · ${download.done_gb!.toFixed(1)} / ${download.total_gb!.toFixed(1)} GB` : '')}</p></> : null)}<DownloadParts parts={job.result?.install?.parts}/>{tr(detailsText ? <details><summary className="small">{tr("Details")}</summary><pre className="small muted error-detail-text">{detailsText}</pre></details> : null)}{tr(error ? <ErrorNotice message={error}/> : null)}</section>; }
|
||||
@@ -6,6 +6,7 @@ import { pickerGroups, pickerVersionState } from '../lib/model-picker';
|
||||
import { sizeGb } from '../lib/format';
|
||||
import { useNative } from '../lib/native';
|
||||
import { NativeDeleteDialog } from './NativeDelete';
|
||||
import { ModelsFolderCard } from './ModelsFolderDialog';
|
||||
import { nativeShortName } from '../lib/native-names';
|
||||
export function SettingsStorage() {
|
||||
const state = useStore(s => s);
|
||||
@@ -77,7 +78,9 @@ export function SettingsStorage() {
|
||||
setPending(false);
|
||||
}
|
||||
}
|
||||
const downloading = !!native?.jobs.some(j => j.state === 'running' && j.kind !== 'models-move') || !!library?.downloads.active || !!state.imageModels?.variants.some(v => v.state === 'downloading');
|
||||
return <>
|
||||
{native?.available && native.downloadDirectory ? <ModelsFolderCard blocked={downloading}/> : null}
|
||||
<section className="storage-directory" aria-labelledby="storage-directory-title">
|
||||
<div className="storage-directory__heading"><span><Icon name="folder" size={18}/><strong id="storage-directory-title">{tr("Model directory")}</strong></span><Btn size="sm" disabled={!currentDirectory || directoryPending || pending} onClick={() => { setDirectory(currentDirectory); setEditing(!editing); setDirectoryError(''); setNotice(''); }}>{tr(editing ? 'Cancel' : 'Change directory')}</Btn></div>
|
||||
<p className="small muted">{tr("Your models are saved here. Use an existing folder to reuse supported downloads.")}</p>
|
||||
@@ -95,6 +98,7 @@ export function SettingsStorage() {
|
||||
<div className="row"><Btn variant="primary" type="submit" disabled={directoryPending || directoryBlocked || !directory.trim()}>{tr(directoryPending ? 'Checking files…' : changing && loadedCount > 0 ? 'Unload and change directory' : changing ? 'Use this directory' : 'Check folder')}</Btn></div>
|
||||
</form> : <Btn size="sm" icon="refresh" disabled={directoryPending || directoryBlocked || !currentDirectory} onClick={() => void saveDirectory(currentDirectory)}>{tr(directoryPending ? 'Checking files…' : 'Check folder')}</Btn>)}
|
||||
{tr(directoryBlocked && !directoryPending ? <p className="small muted">{tr(!lifecycle.allowed ? lifecycle.reason : 'Finish or cancel downloads and loading to change the directory.')}</p> : null)}
|
||||
{tr(state.setup?.app.modelsDirNotice && !notice ? <p className="small storage-directory__notice" role="status"><Icon name="info" size={15}/>{tr(state.setup.app.modelsDirNotice)}</p> : null)}
|
||||
{tr(notice ? <p className="small storage-directory__notice" role="status"><Icon name="check" size={15}/>{tr(notice)}</p> : null)}
|
||||
{tr(directoryError ? <ErrorNotice message={directoryError}/> : null)}
|
||||
</section>
|
||||
|
||||
@@ -94,7 +94,7 @@ export function SettingsUpdates() {
|
||||
</>;
|
||||
}
|
||||
|
||||
/** "Strata 0.2.1 · Engine 0.1.46": the app's own version first, the engine's beside it (About). */
|
||||
/** "Strata 0.2.2 · Engine 0.1.47": the app's own version first, the engine's beside it (About). */
|
||||
export function AboutVersions() {
|
||||
const native = useNative();
|
||||
const app = appVersionLabel(useStore(s => s.setup?.app.version), native?.version);
|
||||
|
||||
@@ -50,7 +50,7 @@ export function ChatTelemetry({ session }: {
|
||||
const approx = m.estimated ? tr('≈ ') : tr('');
|
||||
const dur = (n: number) => elapsed(n);
|
||||
return <div className="telemetry xs muted" aria-label={tr("Chat statistics")}>
|
||||
<Chip icon="gauge" label={tr("{0}{1} tok/s{2}", [approx, speed === null ? tr('—') : Math.round(speed), m.streaming ? tr('') : tr(' · last reply')])} live={around(m.streaming ? "{0}{1} tok/s" : "{0}{1} tok/s · last reply", approx, <Num value={speed} format={int}/>)} title={tr("Session statistics")}>{tr(m.estimated ? <p className="xs muted">{tr("Live estimate from response length and elapsed time. Final usage comes from the engine.")}</p> : null)}<Row label={tr("Thinking time")} value={<Num value={m.thinkMs} format={dur}/>}/><Row label={tr("Writing time")} value={<Num value={m.writeMs} format={dur}/>}/><Row label={tr("Tool time")} value={<Num value={m.toolMs} format={dur}/>}/><Row label={tr("Average time to first token")} value={m.ttft === null ? tr('Not reported') : <Num value={m.ttft} format={dur}/>}/><Row label={tr("Latest response speed")} value={speed === null ? tr('Not reported') : <>{<Num value={speed} format={int}/>}{tr(' tokens/s')}</>}/><Row label={tr("Turns / tool calls")} value={<><Num value={m.turns} format={int}/> / <Num value={m.steps} format={int}/></>}/></Chip>
|
||||
<Chip icon="gauge" label={tr("{0}{1} tok/s{2}", [approx, speed === null ? tr('—') : Math.round(speed), m.streaming ? tr('') : tr(' · last reply')])} live={around(m.streaming ? "{0}{1} tok/s" : "{0}{1} tok/s · last reply", approx, <Num value={speed} format={int}/>)} title={tr("Session statistics")}>{tr(m.estimated ? <p className="xs muted">{tr("Live estimate from response length and elapsed time. Final usage comes from the engine.")}</p> : null)}<Row label={tr("Thinking time")} value={<Num value={m.thinkMs} format={dur}/>}/><Row label={tr("Writing time")} value={<Num value={m.writeMs} format={dur}/>}/><Row label={tr("Tool time")} value={<Num value={m.toolMs} format={dur}/>}/><Row label={tr("Average time to first token")} value={m.ttft === null ? tr('Not reported') : <Num value={m.ttft} format={dur}/>}/><Row label={tr("Latest response speed")} value={speed === null ? tr('Not reported') : <>{<Num value={speed} format={int}/>}{tr(' tokens/s')}</>}/><Row label={tr("Prompt reading")} value={m.promptSpeed === null ? tr('Not reported') : <>{<Num value={m.promptSpeed} format={int}/>}{tr(' tokens/s')}</>}/><Row label={tr("Turns / tool calls")} value={<><Num value={m.turns} format={int}/> / <Num value={m.steps} format={int}/></>}/></Chip>
|
||||
<Chip icon="memory" label={tr("{0}{1} total tokens", [approx, compactTokens(m.total)])} live={around("{0}{1} total tokens", approx, <Num value={m.total} format={compactTokens}/>)} title={tr("Token usage")}><Row label={tr("Total in this chat")} value={<Num value={m.total} format={compactTokens}/>}/><Row label={tr("Input tokens")} value={<Num value={m.input} format={compactTokens}/>}/><Row label={tr("Output tokens")} value={<Num value={m.output} format={compactTokens}/>}/></Chip>
|
||||
<Chip visual={<ContextRing percent={m.pct}/>} label={tr(m.pct === null ? '—' : `${Math.round(m.pct)}% of context`)} live={m.pct === null ? undefined : <><Num value={m.pct} format={int}/>{tr('% of context')}</>} title={tr("Context length")}><Row label={tr("Context used")} value={m.pct === null ? tr('Not reported') : <><Num value={m.pct} format={int}/>%</>}/><div className="context-meter" role="meter" aria-label={tr("Context window used")} aria-valuemin={0} aria-valuemax={100} aria-valuenow={m.pct ?? undefined}><span style={{ width: `${m.pct ?? 0}%` }}/></div><Row label={tr("Current context")} value={<Num value={m.used} format={compactTokens}/>}/><Row label={tr("Context limit")} value={<Num value={m.max} format={compactTokens}/>}/><Row label={tr("Space remaining")} value={m.used !== null && m.max !== null ? <Num value={Math.max(0, m.max - m.used)} format={compactTokens}/> : '—'}/></Chip>
|
||||
</div>;
|
||||
|
||||
@@ -384,7 +384,7 @@ export function Select<T extends string>({ value, valueSuffix, options, onChange
|
||||
</>);
|
||||
}
|
||||
/** One modal surface for confirmations and narrow-screen inspectors. */
|
||||
export function Dialog({ open, title, onClose, children, actions, headerActions, compact = false, sheet = false, wide = false, hideTitle = false, className }: {
|
||||
export function Dialog({ open, title, onClose, children, actions, headerActions, compact = false, sheet = false, wide = false, hideTitle = false, className, noClose = false }: {
|
||||
open: boolean;
|
||||
title: ReactNode;
|
||||
onClose: () => void;
|
||||
@@ -396,6 +396,8 @@ export function Dialog({ open, title, onClose, children, actions, headerActions,
|
||||
wide?: boolean;
|
||||
hideTitle?: boolean;
|
||||
className?: string;
|
||||
/** No close button: the person must use the dialog's own buttons (a move that is running). */
|
||||
noClose?: boolean;
|
||||
}) {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const id = useId();
|
||||
@@ -418,7 +420,7 @@ export function Dialog({ open, title, onClose, children, actions, headerActions,
|
||||
return createPortal(<div className="dialog-layer">
|
||||
<div className="dialog-scrim" onClick={onClose} aria-hidden="true"/>
|
||||
<section ref={ref} className={cx(`${sheet ? 'dialog-surface dialog-surface--sheet' : 'dialog-surface'}${wide ? ' dialog-surface--wide' : ''}${compact ? ' dialog-surface--compact' : ''}`, className)} role="dialog" aria-modal="true" aria-labelledby={id} tabIndex={-1}>
|
||||
<header className="dialog-head"><h2 className={hideTitle ? 'sr-only' : 'h3'} id={id}>{tr(title)}</h2><div className="dialog-head__actions">{tr(headerActions)}<IconBtn name="x" title={typeof title === 'string' ? tr("Close {0}", [title]) : tr("Close")} onClick={onClose}/></div></header>
|
||||
<header className="dialog-head"><h2 className={hideTitle ? 'sr-only' : 'h3'} id={id}>{tr(title)}</h2><div className="dialog-head__actions">{tr(headerActions)}{noClose ? null : <IconBtn name="x" title={typeof title === 'string' ? tr("Close {0}", [title]) : tr("Close")} onClick={onClose}/>}</div></header>
|
||||
<div className="dialog-body">{tr(children)}</div>
|
||||
{tr(actions ? <footer className="dialog-actions">{tr(actions)}</footer> : null)}
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* The version the person sees as "Strata". It is the DESKTOP APP's version (0.2.1), not that of Strata Control, the tools the app runs
|
||||
* (0.1.45, the version of the Terminal). Showing Control's number as "Strata" made a tester report "Strata 0.1.45" for the 0.2.1 app.
|
||||
* The version the person sees as "Strata". It is the DESKTOP APP's version (0.2.2), not that of Strata Control, the tools the app runs
|
||||
* (0.1.45, the version of the Terminal). Showing Control's number as "Strata" made a tester report "Strata 0.1.45" for the 0.2.2 app.
|
||||
* Without a real app version (a source checkout reports 'dev') the other number is the best there is.
|
||||
*/
|
||||
export function appVersionLabel(app: string | null | undefined, control?: string | null): string | undefined {
|
||||
|
||||
@@ -16,12 +16,15 @@ export function chatMetrics(session:Session|null, monitor:MonitorState|null, con
|
||||
const used=live?(liveUsed??finished):(finished??liveUsed);
|
||||
const max=remote?(replies.at(-1)?.contextMax||null):configuredContext??(active?monitor!.context.maxTokens:latest?.contextMax??null);
|
||||
const streaming=replies.find(m=>m.streaming);
|
||||
const speed=streaming?.tps??[...replies].reverse().find(m=>m.tps!==undefined)?.tps??null;
|
||||
// Answer speed: everything the model generated over its decode time. While a reply streams it is that reply's; after it, the LAST reply's own
|
||||
// (never an older reply's number under the words "last reply").
|
||||
const lastReply=streaming??replies.at(-1);
|
||||
const speed=lastReply?.tps??null,promptSpeed=lastReply?.promptTps??null;
|
||||
const ttfts=replies.flatMap(m=>m.ttftMs===undefined?[]:[m.ttftMs]);
|
||||
const hasUsage=session?.statsAvailable===true;
|
||||
const llmTotal=replies.reduce((n,m)=>n+(m.durationMs??(m.streaming?Math.max(0,now-(Date.parse(m.at)||now)):0)),0),thinkTotal=replies.reduce((n,m)=>n+(m.reasoningMs??(m.streaming&&m.reasoningStartedAt?Math.max(0,now-m.reasoningStartedAt):0)),0);
|
||||
return {streaming:!!streaming,writeMs:Math.max(0,llmTotal-thinkTotal),estimated:streaming?.usageSource==='estimated',hasReplies:replies.length>0,example:!!session?.showcase||replies.some(m=>m.usageSource==='demo'),
|
||||
speed:active&&monitor?.phase==='generating'?monitor.outputTps:speed,
|
||||
speed:active&&['generating','thinking'].includes(monitor?.phase??'')&&monitor!.outputTps>0?monitor!.outputTps:speed,promptSpeed,
|
||||
total:session?.statsAvailable===false?null:hasUsage?session.tokensIn+session.tokensOut+(streaming?.tokens??0)+(streaming?.inputTokens??0):replies.every(m=>m.inputTokens!==undefined&&m.tokens!==undefined)?replies.reduce((n,m)=>n+m.inputTokens!+m.tokens!,0):null,
|
||||
input:hasUsage?session.tokensIn:null,output:hasUsage?session.tokensOut:null,
|
||||
llmMs:replies.reduce((n,m)=>n+(m.durationMs??(m.streaming?Math.max(0,now-(Date.parse(m.at)||now)):0)),0),
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* Main / Helper / Off for every graphics card. The same rules as strata_control/gpu_roles.py (the Terminal, setup and the server); both run
|
||||
* tests/data/gpu_roles_vectors.json.
|
||||
*
|
||||
* - Main: the card that runs the LAST stage of the layer split (the output head, the draft layer and the check of its guesses). Exactly one.
|
||||
* - Helper: holds a share of the model, so more of it fits on graphics cards instead of being read from the drive.
|
||||
* - Off: Strata does not touch it.
|
||||
*
|
||||
* The engine takes the cards in stage order (`gpu: [helper, ..., main]`, `gpu_order: "as_given"`), so Main is the LAST one.
|
||||
*/
|
||||
export type Role = 'main' | 'helper' | 'off';
|
||||
export const ROLES: Role[] = ['main', 'helper', 'off'];
|
||||
export type Roles = Record<number, Role>;
|
||||
|
||||
export interface RoleCard {
|
||||
index: number;
|
||||
name: string;
|
||||
vram_gb: number;
|
||||
/** SMs x clock (or the AMD equivalent): only the order between cards matters. */
|
||||
speed?: number;
|
||||
usable?: boolean;
|
||||
problem?: string | null;
|
||||
vendor?: string;
|
||||
uma?: boolean;
|
||||
integrated?: boolean;
|
||||
unified_gb?: number;
|
||||
/** PCI bus id when known: used to notice a renumbering. */
|
||||
bus?: string;
|
||||
}
|
||||
|
||||
export const OFF_AT_OR_BELOW_GB = 8;
|
||||
const SAME_SPEED = 1.05;
|
||||
const MEMORY_GUARD_SPEED = 0.5;
|
||||
const MIN_CARD_GB = 4;
|
||||
const SPLIT_CARD_OVERHEAD_GB = 2;
|
||||
export const CPU_ONLY_LINE = 'Performance is degraded on CPU only.';
|
||||
export const NO_CARD_LINE = 'This engine needs a graphics card to start. Turn one on.';
|
||||
|
||||
const mem = (c: RoleCard) => Number(c.vram_gb) || 0;
|
||||
const speed = (c: RoleCard) => (typeof c.speed === 'number' && c.speed > 0 ? c.speed : 0);
|
||||
const shares = (c: RoleCard) => !!(c.integrated || c.uma || c.unified_gb || c.vendor === 'apple');
|
||||
const canRun = (c: RoleCard) => c.usable !== false;
|
||||
const rnd = (n: number) => Math.round(n);
|
||||
|
||||
export function shortName(c: RoleCard): string {
|
||||
let n = String(c.name || `GPU ${c.index + 1}`);
|
||||
for (const junk of ['NVIDIA GeForce ', 'NVIDIA ', 'GeForce ', 'AMD Radeon ', 'AMD ', 'Intel(R) ', 'Intel ']) if (n.startsWith(junk)) { n = n.slice(junk.length); break; }
|
||||
return n.trim();
|
||||
}
|
||||
/** The number the screens show: GPU 1 is card 0. */
|
||||
export const gpuLabel = (c: RoleCard) => `GPU ${c.index + 1}`;
|
||||
|
||||
/** The cards shown as rows: every card the engine's make can name, in card order. */
|
||||
export function rows<T extends RoleCard>(cards: T[] | null | undefined, vendor?: string | null): T[] {
|
||||
const list = cards ?? [];
|
||||
const make = vendor || pickVendor(list);
|
||||
const mine = make ? list.filter(c => !c.vendor || c.vendor === make) : list;
|
||||
return [...mine].sort((a, b) => a.index - b.index);
|
||||
}
|
||||
function pickVendor(cards: RoleCard[]): string | null {
|
||||
const makes = new Set(cards.filter(c => canRun(c) && c.vendor).map(c => c.vendor as string));
|
||||
if (makes.size <= 1) return [...makes][0] ?? null;
|
||||
return makes.has('nvidia') ? 'nvidia' : cards[0]?.vendor ?? null;
|
||||
}
|
||||
|
||||
function eligible(cs: RoleCard[]): RoleCard[] {
|
||||
const real = cs.filter(c => canRun(c) && !shares(c) && mem(c) >= MIN_CARD_GB);
|
||||
return real.length ? real : cs.filter(canRun);
|
||||
}
|
||||
|
||||
export type MainWhy = 'only' | 'faster' | 'same' | 'memory' | 'none';
|
||||
|
||||
export function pickMain(cs: RoleCard[]): { main: RoleCard | null; why: MainWhy } {
|
||||
const pool = eligible(cs);
|
||||
if (!pool.length) return { main: null, why: 'none' };
|
||||
if (pool.length === 1) return { main: pool[0], why: 'only' };
|
||||
const top = Math.max(...pool.map(speed));
|
||||
const group = top > 0 ? pool.filter(c => speed(c) * SAME_SPEED >= top) : [...pool];
|
||||
const byMem = (a: RoleCard, b: RoleCard) => rnd(mem(b)) - rnd(mem(a)) || a.index - b.index;
|
||||
let main = [...group].sort(byMem)[0];
|
||||
let why: MainWhy = group.length === 1 ? 'faster' : 'same';
|
||||
if (group.length > 1 && new Set(group.map(c => `${c.name}|${rnd(mem(c))}`)).size === 1) main = [...group].sort((a, b) => b.index - a.index)[0]; // identical cards: the HIGHEST number is Main
|
||||
if (top <= 0 && new Set(pool.map(c => rnd(mem(c)))).size > 1) why = 'memory';
|
||||
const bigger = pool.filter(c => rnd(mem(c)) > rnd(mem(main)) && (top <= 0 || speed(c) >= MEMORY_GUARD_SPEED * top));
|
||||
if (bigger.length) { main = [...bigger].sort(byMem)[0]; why = 'memory'; }
|
||||
return { main, why };
|
||||
}
|
||||
|
||||
export type OffWhy = 'cannot_run' | 'igpu' | 'small' | 'helper';
|
||||
export interface Recommendation { roles: Roles; main: number | null; why: MainWhy; reasons: Record<number, MainWhy | OffWhy> }
|
||||
|
||||
/** The best card is Main; every other card starts as Helper, EXCEPT a card with 8 GB or less, built-in graphics beside a real card, and a card the engine cannot run: those start Off. */
|
||||
export function recommend(cards: RoleCard[] | null | undefined, vendor?: string | null): Recommendation {
|
||||
const cs = rows(cards, vendor);
|
||||
const { main, why } = pickMain(cs);
|
||||
const roles: Roles = {}, reasons: Recommendation['reasons'] = {};
|
||||
for (const c of cs) {
|
||||
const i = c.index;
|
||||
if (main && c === main) { roles[i] = 'main'; reasons[i] = why; }
|
||||
else if (!canRun(c)) { roles[i] = 'off'; reasons[i] = 'cannot_run'; }
|
||||
else if (shares(c) && main) { roles[i] = 'off'; reasons[i] = 'igpu'; }
|
||||
else if (rnd(mem(c)) <= OFF_AT_OR_BELOW_GB) { roles[i] = 'off'; reasons[i] = 'small'; }
|
||||
else { roles[i] = 'helper'; reasons[i] = 'helper'; }
|
||||
}
|
||||
return { roles, main: main ? main.index : null, why, reasons };
|
||||
}
|
||||
|
||||
const on = (roles: Roles) => Object.keys(roles).map(Number).filter(i => roles[i] !== 'off');
|
||||
const byNumber = (roles: Roles) => Object.keys(roles).map(Number).sort((a, b) => a - b);
|
||||
|
||||
/** Exactly one Main among the cards that are on; a lone card that is on is Main. */
|
||||
export function normalize(roles: Roles, cards: RoleCard[]): Roles {
|
||||
const cs = new Map(cards.map(c => [c.index, c]));
|
||||
const out: Roles = {};
|
||||
for (const i of byNumber(roles)) if (cs.has(i)) out[i] = ROLES.includes(roles[i]) ? roles[i] : 'off';
|
||||
const onList = on(out), mains = onList.filter(i => out[i] === 'main');
|
||||
if (onList.length && !mains.length) {
|
||||
const best = [...onList].sort((a, b) => speed(cs.get(b)!) - speed(cs.get(a)!) || rnd(mem(cs.get(b)!)) - rnd(mem(cs.get(a)!)) || a - b)[0];
|
||||
out[best] = 'main';
|
||||
}
|
||||
for (const extra of mains.slice(1)) out[extra] = 'helper';
|
||||
if (onList.length === 1) out[onList[0]] = 'main';
|
||||
return out;
|
||||
}
|
||||
|
||||
export interface RoleChange { roles: Roles; note: string | null }
|
||||
|
||||
/** One click: the new roles and a code for the quiet line (old_main_helper:<i>, promoted:<i>, only_main). */
|
||||
export function setRole(roles: Roles, cards: RoleCard[], idx: number, role: Role): RoleChange {
|
||||
const cs = new Map(cards.map(c => [c.index, c]));
|
||||
const out: Roles = { ...roles };
|
||||
let note: string | null = null;
|
||||
if (!cs.has(idx) || !ROLES.includes(role)) return { roles: out, note };
|
||||
const ids = byNumber(out);
|
||||
const fastest = (list: number[]) => [...list].sort((a, b) => speed(cs.get(b)!) - speed(cs.get(a)!) || a - b);
|
||||
if (role === 'main') {
|
||||
const old = ids.find(i => i !== idx && out[i] === 'main');
|
||||
out[idx] = 'main';
|
||||
if (old !== undefined) { out[old] = 'helper'; note = `old_main_helper:${old}`; }
|
||||
} else if (role === 'helper') {
|
||||
if (out[idx] === 'main') {
|
||||
const others = fastest(ids.filter(i => i !== idx && out[i] === 'helper'));
|
||||
if (!others.length) return { roles: out, note: 'only_main' };
|
||||
out[idx] = 'helper'; out[others[0]] = 'main'; note = `promoted:${others[0]}`;
|
||||
} else out[idx] = 'helper';
|
||||
} else {
|
||||
const wasMain = out[idx] === 'main';
|
||||
out[idx] = 'off';
|
||||
if (wasMain) {
|
||||
const next = fastest(ids.filter(i => out[i] === 'helper'));
|
||||
if (next.length) { out[next[0]] = 'main'; note = `promoted:${next[0]}`; }
|
||||
}
|
||||
}
|
||||
const onList = on(out);
|
||||
if (onList.length === 1) out[onList[0]] = 'main';
|
||||
else if (onList.length && !onList.some(i => out[i] === 'main')) out[onList[0]] = 'main';
|
||||
return { roles: out, note };
|
||||
}
|
||||
|
||||
/** The engine's card list: Helpers (slowest first, ties by number), then the Main card LAST; Off cards left out. */
|
||||
export function order(roles: Roles, cards: RoleCard[]): number[] {
|
||||
const cs = new Map(cards.map(c => [c.index, c]));
|
||||
const r = normalize(roles, cards);
|
||||
const helpers = byNumber(r).filter(i => r[i] === 'helper').sort((a, b) => speed(cs.get(a)!) - speed(cs.get(b)!) || a - b);
|
||||
return [...helpers, ...byNumber(r).filter(i => r[i] === 'main')];
|
||||
}
|
||||
|
||||
export interface RoleMirror { gpu_roles: Record<string, Role>; gpu: number | number[]; gpu_order?: 'as_given'; gpu_none?: true; gpu_cards: Record<string, string> }
|
||||
/** What a writer stores: "gpu_roles" plus the legacy mirror older engines and the Monitor read. Nothing on: "gpu": [] and "gpu_none": true. */
|
||||
export function mirror(roles: Roles, cards: RoleCard[]): RoleMirror {
|
||||
const r = normalize(roles, cards), lst = order(r, cards);
|
||||
const out: RoleMirror = { gpu_roles: Object.fromEntries(byNumber(r).map(i => [String(i), r[i]])), gpu: [], gpu_cards: {} };
|
||||
if (lst.length) { out.gpu = lst.length === 1 ? lst[0] : lst; out.gpu_order = 'as_given'; } else out.gpu_none = true;
|
||||
for (const c of cards) if (c.index in r) out.gpu_cards[String(c.index)] = [shortName(c), c.bus ?? ''].filter(Boolean).join('|');
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Roles of an old "gpu" setting, for display: the LAST card of the engine's order is Main, earlier ones Helpers, unlisted cards Off. */
|
||||
export function fromLegacy(gpu: unknown, cards: RoleCard[], gpuOrder?: string | null): Roles {
|
||||
const cs = new Map(cards.map(c => [c.index, c]));
|
||||
let items: number[] = Array.isArray(gpu) ? gpu.map(Number) : gpu === null || gpu === undefined || gpu === '' ? [] : String(gpu).split(',').filter(x => x.trim() !== '').map(Number);
|
||||
items = items.filter(i => cs.has(i));
|
||||
if (!items.length) return { ...recommend(cards).roles };
|
||||
if (items.length > 1 && gpuOrder !== 'as_given') {
|
||||
const sp = items.map(i => speed(cs.get(i)!));
|
||||
if (sp.every(x => x > 0) && Math.max(...sp) >= SAME_SPEED * Math.min(...sp)) items = [...items].sort((a, b) => speed(cs.get(a)!) - speed(cs.get(b)!));
|
||||
}
|
||||
const roles: Roles = {};
|
||||
for (const c of cards) roles[c.index] = 'off';
|
||||
for (const i of items.slice(0, -1)) roles[i] = 'helper';
|
||||
roles[items[items.length - 1]] = 'main';
|
||||
return roles;
|
||||
}
|
||||
|
||||
export const sameRoles = (a: Roles, b: Roles) => { const ka = byNumber(a), kb = byNumber(b); return ka.length === kb.length && ka.every((k, n) => k === kb[n] && a[k] === b[k]); };
|
||||
|
||||
export interface RoleWarning { code: 'igpu_main' | 'cannot_run' | 'low_memory' | 'cpu_only'; card: number | null; text: string; fix: { card: number; role: Role } | null }
|
||||
|
||||
/** One short line each, only what a person could not know. Never blocking. */
|
||||
export function warnings(roles: Roles, cards: RoleCard[], opts: { spillGb?: number; engineRunsCpuOnly?: boolean } = {}): RoleWarning[] {
|
||||
const cs = new Map(cards.map(c => [c.index, c]));
|
||||
const out: RoleWarning[] = [];
|
||||
for (const i of byNumber(roles)) {
|
||||
const c = cs.get(i), role = roles[i];
|
||||
if (!c || role === 'off') continue;
|
||||
if (!canRun(c)) out.push({ code: 'cannot_run', card: i, text: `${shortName(c)} cannot run Strata.`, fix: { card: i, role: 'off' } });
|
||||
else if (role === 'main' && shares(c) && cards.some(x => !shares(x) && canRun(x))) {
|
||||
const real = cards.filter(x => !shares(x) && canRun(x)).sort((a, b) => speed(b) - speed(a) || mem(b) - mem(a) || a.index - b.index)[0];
|
||||
out.push({ code: 'igpu_main', card: i, text: `${shortName(c)} is built-in graphics; it is slow as Main.`, fix: { card: real.index, role: 'main' } });
|
||||
}
|
||||
}
|
||||
if (!on(roles).length) out.push({ code: 'cpu_only', card: null, text: opts.engineRunsCpuOnly ? CPU_ONLY_LINE : NO_CARD_LINE, fix: null });
|
||||
else if ((opts.spillGb ?? 0) >= 1) out.push({ code: 'low_memory', card: null, text: `Not enough memory for this setup: about ${Math.round(opts.spillGb!)} GB is read from the drive.`, fix: null });
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The graphics memory the cards that are on give for the model (an extra card costs its own dense layers); 0 when all are off. */
|
||||
export function vramOn(roles: Roles, cards: RoleCard[]): number {
|
||||
const cs = new Map(cards.map(c => [c.index, c]));
|
||||
const onCards = on(roles).map(i => cs.get(i)).filter((c): c is RoleCard => !!c && !shares(c));
|
||||
return Math.max(0, onCards.reduce((n, c) => n + mem(c), 0) - SPLIT_CARD_OVERHEAD_GB * Math.max(0, onCards.length - 1));
|
||||
}
|
||||
|
||||
export function noteText(code: string | null, cards: RoleCard[]): string {
|
||||
if (!code) return '';
|
||||
const [kind, arg] = code.split(':');
|
||||
const c = arg !== undefined ? cards.find(x => x.index === Number(arg)) : undefined;
|
||||
const name = c ? shortName(c) : 'That card';
|
||||
if (kind === 'old_main_helper') return `${name} is now a Helper.`;
|
||||
if (kind === 'promoted') return `${name} is now Main.`;
|
||||
if (kind === 'only_main') return 'The only card that is on has to be Main.';
|
||||
return '';
|
||||
}
|
||||
|
||||
/** The one plain line above the cards. */
|
||||
export function sentence(cards: RoleCard[] | null | undefined, vendor?: string | null): string {
|
||||
const cs = rows(cards, vendor), rec = recommend(cards, vendor);
|
||||
if (rec.main === null) return 'No graphics card that Strata can use was found.';
|
||||
const byi = new Map(cs.map(c => [c.index, c]));
|
||||
const m = byi.get(rec.main)!;
|
||||
if (cs.length === 1) return `Strata will use your ${shortName(m)}.`;
|
||||
const why = { same: 'the cards are alike', memory: 'it has more memory', faster: 'it is faster', only: 'it is the only card', none: '' }[rec.why];
|
||||
let s = `${shortName(m)} is Main: ${why}.`;
|
||||
const helpers = byNumber(rec.roles).filter(i => rec.roles[i] === 'helper').map(i => shortName(byi.get(i)!));
|
||||
if (helpers.length) s += ` ${helpers.join(' and ')} ${helpers.length > 1 ? 'help' : 'helps'}.`;
|
||||
for (const i of byNumber(rec.roles)) if (rec.roles[i] === 'off') {
|
||||
const code = rec.reasons[i], c = byi.get(i)!;
|
||||
s += ` ${shortName(c)} starts Off: ${code === 'cannot_run' ? 'Strata cannot run it.' : code === 'igpu' ? 'built-in graphics.' : `${OFF_AT_OR_BELOW_GB} GB or less.`}`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/** `Main: GPU 2 RTX 4070 Ti. Helper: GPU 1 RTX 3060.` */
|
||||
export function describe(roles: Roles, cards: RoleCard[]): string {
|
||||
const cs = new Map(cards.map(c => [c.index, c]));
|
||||
const parts: string[] = [];
|
||||
for (const [role, word] of [['main', 'Main'], ['helper', 'Helper'], ['off', 'Off']] as const) {
|
||||
const names = byNumber(roles).filter(i => roles[i] === role).map(i => (cs.has(i) ? `GPU ${i + 1} ${shortName(cs.get(i)!)}` : `GPU ${i + 1}`));
|
||||
if (names.length) parts.push(`${word}: ${names.join(', ')}.`);
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
/** A saved "gpu_roles" object -> roles; null when it is not one. */
|
||||
export function parseSaved(value: unknown): Roles | null {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
||||
const out: Roles = {};
|
||||
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
||||
if (!/^\d+$/.test(k.trim()) || !ROLES.includes(String(v).toLowerCase() as Role)) return null;
|
||||
out[Number(k)] = String(v).toLowerCase() as Role;
|
||||
}
|
||||
if (!Object.keys(out).length || Object.values(out).filter(r => r === 'main').length > 1) return null;
|
||||
return out;
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export interface NativeState {
|
||||
/** The picture reader (strata-vision) is installed with the engine, so models marked for pictures can read them here. */
|
||||
/** Strata Control's view of the engine for this computer (the app): usable false = none found or recognised; `notice` says why in plain words. */
|
||||
engine?:{usable:boolean;notice?:string|null;wrongMake?:boolean;version?:string|null};
|
||||
visionReader?: boolean;available:boolean;repo:string;downloadDirectory?:string;version?:string|null;summary:string;error:string|null;catalog:{families:NativeFamily[];contexts:number[];recommendation:{family:string|null;model:string|null;context?:number;why?:string};context_note:string}|null;installed:{models:{model:string;config:string;model_name:string;ready:boolean;/** The picture-reading files are on this computer (notes-split-agent8.md, item 8). */visionPrepared?:boolean;context?:number;kv?:string;images?:string;visionEnabled?:boolean;visionDevice?:string;lowRamMode?:string;resourceProfile?:'work'|'performance';dataFolder?:string;diskGb?:number;diskBytes?:number;/** Files this model shares with another (the picture reader, the draft layer): counted once, in `usage`. */sharedBytes?:number;/** What Delete frees: files in Strata's own folders that no other model uses. */removableGb?:number;missing_files?:string[]}[];/** What all models take on disk, from their real files, shared files counted once. */usage?:{total_bytes:number;total_gb:number;shared_bytes:number;shared_gb:number;models:Record<string,{bytes:number;gb:number}>}|null;data_folder:string;engine:{version:string|null}}|null;hardware:{ram_gb:number;engine_vendor?:string;gpus:{index:number;name:string;vram_gb:number;usable:boolean;vendor?:string;uma?:boolean;integrated?:boolean;unified_gb?:number;problem?:string|null}[]}|null;fields:NativeField[];instances:NativeInstance[];defaultId:string|null;jobs:NativeJob[];capabilities:{responses?:boolean;updates:boolean;rollback:boolean;calibration:boolean;componentUpdates?:boolean}}
|
||||
visionReader?: boolean;available:boolean;repo:string;downloadDirectory?:string;/** Free bytes on the drive that holds the models folder. */downloadFreeBytes?:number|null;downloadTotalBytes?:number|null;version?:string|null;summary:string;error:string|null;catalog:{families:NativeFamily[];contexts:number[];recommendation:{family:string|null;model:string|null;context?:number;why?:string};context_note:string}|null;installed:{models:{model:string;config:string;model_name:string;ready:boolean;/** The picture-reading files are on this computer (notes-split-agent8.md, item 8). */visionPrepared?:boolean;context?:number;kv?:string;images?:string;visionEnabled?:boolean;visionDevice?:string;lowRamMode?:string;resourceProfile?:'work'|'performance';dataFolder?:string;diskGb?:number;diskBytes?:number;/** Files this model shares with another (the picture reader, the draft layer): counted once, in `usage`. */sharedBytes?:number;/** What Delete frees: files in Strata's own folders that no other model uses. */removableGb?:number;missing_files?:string[]}[];/** What all models take on disk, from their real files, shared files counted once. */usage?:{total_bytes:number;total_gb:number;shared_bytes:number;shared_gb:number;models:Record<string,{bytes:number;gb:number}>}|null;data_folder:string;engine:{version:string|null}}|null;hardware:{ram_gb:number;engine_vendor?:string;gpus:{index:number;name:string;vram_gb:number;usable:boolean;/** SMs x clock: only the order between cards matters (strata_control/gpu_roles.py). */speed?:number;vendor?:string;uma?:boolean;integrated?:boolean;unified_gb?:number;problem?:string|null}[]}|null;fields:NativeField[];instances:NativeInstance[];defaultId:string|null;jobs:NativeJob[];capabilities:{responses?:boolean;updates:boolean;rollback:boolean;calibration:boolean;componentUpdates?:boolean}}
|
||||
let state:NativeState|null=null;
|
||||
const listeners=new Set<()=>void>();
|
||||
function put(value:NativeState){if(state&&JSON.stringify(state)===JSON.stringify(value))return;state=value;listeners.forEach(fn=>fn());}
|
||||
|
||||
@@ -1731,6 +1731,7 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
|
||||
...m,
|
||||
tokens: typeof frame.tokensOut === 'number' ? frame.tokensOut : m.tokens,
|
||||
tps: typeof frame.tps === 'number' ? frame.tps : m.tps,
|
||||
promptTps: typeof frame.promptTps === 'number' ? frame.promptTps : m.promptTps,
|
||||
}));
|
||||
} else if (type === 'error') {
|
||||
clearStatus(sessionId);
|
||||
@@ -1752,7 +1753,8 @@ export async function sendMessage(text: string, options: { fromLoop?: boolean; g
|
||||
inputTokens: typeof frame.tokensIn === 'number' ? frame.tokensIn : undefined,
|
||||
contextUsed: typeof frame.contextTokens === 'number' ? frame.contextTokens : undefined,
|
||||
usageSource: frame.usageSource === 'reported' || frame.usageSource === 'demo' ? frame.usageSource : 'unavailable',
|
||||
tps: typeof frame.tps === 'number' ? frame.tps : undefined,
|
||||
tps: typeof frame.tps === 'number' ? frame.tps : m.tps,
|
||||
promptTps: typeof frame.promptTps === 'number' ? frame.promptTps : m.promptTps,
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -223,11 +223,11 @@ export interface ServeRequest {
|
||||
finishReason?: string | null;
|
||||
}
|
||||
|
||||
export interface TunnelState { status: 'idle' | 'starting' | 'connected' | 'stopping' | 'error'; mode: 'quick' | 'domain' | null; url: string | null; error: string | null; installed: boolean; originUrl: string; connector?: { path: string; version: string | null; source: string; label: string; summary: string } | null; external?: { kind: 'service' | 'process'; others: number[]; own: number[]; message: string } | null; }
|
||||
export interface TunnelState { status: 'idle' | 'starting' | 'connected' | 'stopping' | 'error'; mode: 'quick' | 'domain' | null; url: string | null; error: string | null; warning?: string | null; installed: boolean; originUrl: string; connector?: { path: string; version: string | null; source: string; label: string; summary: string } | null; external?: { kind: 'service' | 'process'; others: number[]; own: number[]; message: string } | null; }
|
||||
|
||||
export interface ServingState {
|
||||
tunnel?: TunnelState;
|
||||
sharing?: { active: boolean; url: string | null };
|
||||
sharing?: { active: boolean; url: string | null; warning?: string | null };
|
||||
revision?: number;
|
||||
running: boolean;
|
||||
baseUrl: string;
|
||||
@@ -355,6 +355,7 @@ export interface ChatMessage {
|
||||
reasoningOpen?: boolean;
|
||||
tokens?: number;
|
||||
tps?: number;
|
||||
promptTps?: number;
|
||||
durationMs?: number;
|
||||
error?: string;
|
||||
tools?: ToolCall[];
|
||||
@@ -494,7 +495,7 @@ export interface SetupState {
|
||||
step: number;
|
||||
completedAt?: string;
|
||||
history: { step: number; at: string }[];
|
||||
app: { version: string; modelsDir: string; dataDir: string };
|
||||
app: { version: string; modelsDir: string; dataDir: string; modelsDirNotice?: string };
|
||||
}
|
||||
|
||||
export interface EngineEvent {
|
||||
|
||||
@@ -7,8 +7,8 @@ import type { UpdateCheckResult } from './types';
|
||||
*/
|
||||
const KEY = 'strata.update-check.v1';
|
||||
|
||||
interface Stored { result: UpdateCheckResult; appVersion: string | null; engineVersion: string | null }
|
||||
export interface InstalledVersions { appVersion?: string | null; engineVersion?: string | null }
|
||||
interface Stored { result: UpdateCheckResult; appVersion: string | null; engineVersion: string | null; controlVersion?: string | null }
|
||||
export interface InstalledVersions { appVersion?: string | null; engineVersion?: string | null; controlVersion?: string | null }
|
||||
|
||||
const same = (a: string | null | undefined, b: string | null | undefined) => (a ?? null) === (b ?? null);
|
||||
|
||||
@@ -16,13 +16,15 @@ export function readUpdateCache(now: InstalledVersions): UpdateCheckResult | nul
|
||||
try {
|
||||
const stored = JSON.parse(localStorage.getItem(KEY) ?? 'null') as Stored | null;
|
||||
if (!stored?.result?.strata?.status || !stored.result.engine?.status || !stored.result.checkedAt) return null;
|
||||
if (!same(stored.appVersion, now.appVersion) || !same(stored.engineVersion, now.engineVersion)) return null;
|
||||
// Still loading (no app version yet): show nothing rather than a result that may belong to the version before an update.
|
||||
if (!now.appVersion) return null;
|
||||
if (!same(stored.appVersion, now.appVersion) || !same(stored.engineVersion, now.engineVersion) || !same(stored.controlVersion, now.controlVersion)) return null;
|
||||
return stored.result;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export function saveUpdateCache(result: UpdateCheckResult, now: InstalledVersions) {
|
||||
try { localStorage.setItem(KEY, JSON.stringify({ result, appVersion: now.appVersion ?? null, engineVersion: now.engineVersion ?? null } satisfies Stored)); } catch { /* Not remembering a result is harmless. */ }
|
||||
try { localStorage.setItem(KEY, JSON.stringify({ result, appVersion: now.appVersion ?? null, engineVersion: now.engineVersion ?? null, controlVersion: now.controlVersion ?? null } satisfies Stored)); } catch { /* Not remembering a result is harmless. */ }
|
||||
}
|
||||
|
||||
export function clearUpdateCache() {
|
||||
|
||||
@@ -10,7 +10,7 @@ export function useInstalledVersions(): InstalledVersions {
|
||||
const native = useNative();
|
||||
const engineVersion = useStore(s => s.engine?.engine.version);
|
||||
const appVersion = appVersionLabel(useStore(s => s.setup?.app.version), native?.version);
|
||||
return { appVersion: appVersion ?? null, engineVersion: (native?.available ? native.installed?.engine?.version : engineVersion) ?? null };
|
||||
return { appVersion: appVersion ?? null, engineVersion: (native?.available ? native.installed?.engine?.version : engineVersion) ?? null, controlVersion: native?.version ?? null };
|
||||
}
|
||||
|
||||
/** The one place the app asks the host to check for updates. Called only from a button press. */
|
||||
|
||||
@@ -3346,5 +3346,148 @@
|
||||
"s8257": "alle {0} Min.",
|
||||
"s8258": "Diese Strata-Engine hat keinen Code für die Grafikkarte, die sie verwenden wollte.",
|
||||
"s8259": "Wähle unter „Ausführen auf“ eine andere Karte, oder öffne die Details: Dort steht, welche Karten gefunden wurden und wofür die Engine gebaut ist.",
|
||||
"s8260": "Dieser Schalter bewirkt in der App nichts. Webseiten beliebiger Seiten können die unter „API-Server“ gezeigte API schon aufrufen. Um nur bestimmte Seiten zuzulassen oder den eigenen Server der Engine (Port 8080) zu nutzen, tragen Sie \"cors_origins\": [\"https://ihre.seite\"] in die Konfigurationsdatei des Modells ein."
|
||||
"s8260": "Dieser Schalter bewirkt in der App nichts. Webseiten beliebiger Seiten können die unter „API-Server“ gezeigte API schon aufrufen. Um nur bestimmte Seiten zuzulassen oder den eigenen Server der Engine (Port 8080) zu nutzen, tragen Sie \"cors_origins\": [\"https://ihre.seite\"] in die Konfigurationsdatei des Modells ein.",
|
||||
"s8301": "API-Schlüssel erstellen (empfohlen)",
|
||||
"s8302": "Ohne API-Schlüssel kann jedes Gerät in diesem Netzwerk Ihr Modell nutzen.",
|
||||
"s8303": "Es wird nur die Inferenz-API freigegeben. Clients brauchen Ihren API-Schlüssel, sofern einer gesetzt ist. Nutzen Sie ein vertrauenswürdiges lokales Netzwerk; diese Verbindung verwendet HTTP.",
|
||||
"s8304": "Erstellen Sie einen Schlüssel, um Ihre API zu schützen. Danach kehren Sie hierher zurück. Sie können auch ohne Schlüssel fortfahren, dann kann aber jeder, der den Link findet, Ihr Modell nutzen.",
|
||||
"s8305": "Sie haben schon ein unterstütztes Modell auf diesem Computer? Wählen Sie den Ordner mit den Dateien oder eine der .gguf-Dateien. Strata prüft sie, bereitet das Modell vor und lädt es. Es wird nichts kopiert oder heruntergeladen.",
|
||||
"s8306": "Ordner mit den GGUF-Dateien oder eine .gguf-Datei",
|
||||
"s8307": "Vollständiger Pfad zum Ordner oder zur Datei",
|
||||
"s8308": "Ohne API-Schlüssel kann jeder, der diese Adresse erreicht, Ihr Modell nutzen.",
|
||||
"s8309": "Strata konnte {0} nicht für Modelle verwenden ({1}); die Modelle werden daher in {2} abgelegt. Sie können in den Einstellungen einen anderen Ordner wählen.",
|
||||
"s8310": "Port {0} wird bereits verwendet. Strata läuft vielleicht schon; schließen Sie es oder das andere Programm, oder starten Sie Strata mit einem anderen Port (STRATA_PORT).",
|
||||
"s8311": "Windows oder eine Firewall erlaubt Strata nicht, Port {0} zu verwenden. Hyper-V, WSL und Docker reservieren Portbereiche für sich (siehe \"netsh int ipv4 show excludedportrange protocol=tcp\"). Starten Sie Strata mit einem anderen Port (STRATA_PORT), zum Beispiel 8801.",
|
||||
"s8312": "{0} ist keine Adresse dieses Computers, daher kann Strata dort nicht lauschen. Prüfen Sie STRATA_BIND.",
|
||||
"s8313": "Strata kann auf {0}:{1} nicht lauschen ({2}).",
|
||||
"s8339": "Modellordner",
|
||||
"s8340": "Strata speichert die großen Modelldateien hier.",
|
||||
"s8341": "Ordner ändern",
|
||||
"s8342": "Warte auf den Ordner …",
|
||||
"s8343": "Speicherplatz belegt",
|
||||
"s8344": "{0} frei",
|
||||
"s8345": "auf diesem Laufwerk",
|
||||
"s8346": "Ihre Modelle belegen {0}",
|
||||
"s8347": "Beenden oder stoppen Sie den Download, um den Ordner zu ändern.",
|
||||
"s8348": "Diesen Ordner gibt es nicht",
|
||||
"s8349": "Dieser Ordner kann nicht verwendet werden",
|
||||
"s8350": "Strata kann in {0} keine Dateien speichern.",
|
||||
"s8351": "Strata kann {0} nicht finden.",
|
||||
"s8352": "Wählen Sie einen Ordner außerhalb des aktuellen.",
|
||||
"s8353": "Wählen Sie einen Ordner, der Ihnen gehört, zum Beispiel in Dokumente oder auf einem anderen Laufwerk.",
|
||||
"s8354": "Prüfen Sie, ob das Laufwerk angeschlossen ist, und wählen Sie den Ordner erneut. Ihre Modelle wurden nicht verändert.",
|
||||
"s8355": "Diesen Ordner für Ihre Modelle verwenden?",
|
||||
"s8356": "Neuer Ordner",
|
||||
"s8357": "{0} frei auf diesem Laufwerk",
|
||||
"s8358": "Sie haben das ganze Laufwerk {0} gewählt. Strata legt seine Dateien in einem eigenen Ordner ab, Ihre Modelle liegen daher in {1}.",
|
||||
"s8359": "Als Nächstes folgen die drei Möglichkeiten.",
|
||||
"s8360": "Meine Modelle verschieben",
|
||||
"s8361": "Beide Ordner verwenden",
|
||||
"s8362": "Leer beginnen",
|
||||
"s8363": "Ihre Modelle werden verschoben",
|
||||
"s8364": "Verschieben stoppen",
|
||||
"s8365": "{0} von {1} GB",
|
||||
"s8366": "Weniger als eine Minute übrig",
|
||||
"s8367": "Etwa eine Minute übrig",
|
||||
"s8368": "Etwa {0} Minuten übrig",
|
||||
"s8369": "Etwa {0} Stunden übrig",
|
||||
"s8370": "Lassen Sie Strata geöffnet. Andere Apps können Sie weiter benutzen. Ihre Modelle lassen sich erst laden, wenn das Verschieben fertig ist.",
|
||||
"s8371": "Kopiert gerade: {0}, Datei {1} von {2}",
|
||||
"s8372": "Von {0} nach {1}",
|
||||
"s8373": "Meine Modelle dorthin verschieben",
|
||||
"s8374": "Sie haben keine Modelle zum Verschieben.",
|
||||
"s8375": "Strata verschiebt Ihre {0} Modelle in den neuen Ordner. Es ist dasselbe Laufwerk, das geht also schnell.",
|
||||
"s8376": "Strata kopiert Ihre {0} Modelle in den neuen Ordner, prüft sie und entfernt dann die alten Kopien.",
|
||||
"s8377": "Braucht keinen zusätzlichen Platz.",
|
||||
"s8378": "Benötigt {0} freien Platz im neuen Ordner. Verfügbar: {1}.",
|
||||
"s8379": "Am bisherigen Ort lassen und zusätzlich den neuen Ordner verwenden",
|
||||
"s8380": "Es wird nichts kopiert. Ihre aktuellen Modelle funktionieren weiter, neue Downloads landen im neuen Ordner.",
|
||||
"s8381": "Braucht jetzt keinen Platz. Neue Downloads müssen in {1} passen.",
|
||||
"s8382": "Braucht jetzt keinen Platz.",
|
||||
"s8383": "Strata verwendet ab jetzt den neuen Ordner und zeigt Ihre aktuellen Modelle nicht mehr an. Ihre alten Dateien werden nicht gelöscht.",
|
||||
"s8384": "Braucht jetzt keinen Platz. Jedes später geladene Modell braucht eigenen Platz.",
|
||||
"s8385": "Ihre Modelle brauchen {0}, auf diesem Laufwerk sind {1} frei.",
|
||||
"s8386": "Geben Sie dort etwa {0} frei, wählen Sie einen anderen Ordner oder nehmen Sie eine der anderen beiden Möglichkeiten.",
|
||||
"s8387": "Was soll mit Ihren Modellen geschehen",
|
||||
"s8388": "Ein laufendes Modell wird zuerst gestoppt; laden Sie es nach dem Verschieben erneut.",
|
||||
"s8389": "{0} würden verschoben.",
|
||||
"s8390": "Der freie Platz wird auf dem Laufwerk gemessen, auf dem {0} liegt.",
|
||||
"s8391": "Alter Ordner: {0}",
|
||||
"s8392": "Strata kopiert eine Datei nach der anderen und prüft ihre Größe, bevor die alte entfernt wird.",
|
||||
"s8393": "Sie können jederzeit stoppen; bereits verschobene Dateien bleiben nutzbar.",
|
||||
"s8394": "Auf dem Laufwerk ist der Platz ausgegangen",
|
||||
"s8395": "Das Verschieben wurde gestoppt",
|
||||
"s8396": "{0} war nach {1} voll, deshalb wurde das Verschieben gestoppt.",
|
||||
"s8397": "Die Modelle konnten nicht verschoben werden.",
|
||||
"s8398": "Es ging nichts verloren. Bereits verschobene Modelle liegen im neuen Ordner, die übrigen noch in {0}, und die halb kopierte Datei wurde entfernt. Schaffen Sie Platz auf {1} oder wählen Sie einen anderen Ordner und versuchen Sie es erneut.",
|
||||
"s8399": "Es ging nichts verloren. Alle Ihre Modelle liegen noch in {0}, und die halb kopierte Datei wurde entfernt. Schaffen Sie Platz auf {1} oder wählen Sie einen anderen Ordner und versuchen Sie es erneut.",
|
||||
"s8400": "Es ging nichts verloren. Die nicht verschobenen Modelle liegen noch in {0}. Wählen Sie einen anderen Ordner, um es erneut zu versuchen.",
|
||||
"s8401": "Verschieben gestoppt",
|
||||
"s8402": "Es ging nichts verloren.",
|
||||
"s8403": "{0} liegt im neuen Ordner.",
|
||||
"s8404": "{0} liegt noch im alten Ordner.",
|
||||
"s8405": "Beide funktionieren.",
|
||||
"s8406": "Modelle in beiden Ordnern werden wie gewohnt angezeigt. Das Verschieben können Sie später mit „Ordner ändern“ beenden.",
|
||||
"s8407": "Ihre Modelle wurden verschoben",
|
||||
"s8408": "{0} liegen jetzt in {1}",
|
||||
"s8409": "Die alten Kopien wurden entfernt, auf {1} sind wieder {0} frei.",
|
||||
"s8410": "Strata verwendet jetzt auch {0}. Neue Downloads landen dort, Ihre aktuellen Modelle bleiben, wo sie sind.",
|
||||
"s8411": "Strata verwendet jetzt {0}. Ihre alten Modelldateien liegen noch in {1}; Strata löscht sie nicht.",
|
||||
"s8412": "Das Ändern des Modellordners ist Teil der Strata-App.",
|
||||
"s8413": "Ein Modell läuft. Stoppen Sie es zuerst und ändern Sie dann den Ordner.",
|
||||
"s8414": "Wählen Sie einen Modellordner.",
|
||||
"s8415": "Wählen Sie, was mit Ihren Modellen geschehen soll.",
|
||||
"s8416": "Diese Möglichkeit gibt es hier nicht.",
|
||||
"s8417": "Ein Verschieben wurde unterbrochen. Beenden Sie es oder lassen Sie es sein.",
|
||||
"s8418": "Strata hat Ihre Modelle von {0} nach {1} verschoben, als es angehalten wurde. Es ist nichts verloren.",
|
||||
"s8419": "Verschieben beenden",
|
||||
"s8420": "Belassen",
|
||||
"s8421": "Beenden legt alle Ihre Modelle in {1} ab. Belassen lässt sie in {0} und entfernt die unvollständigen Kopien aus {1}.",
|
||||
"s8422": "Die unvollständigen Kopien wurden entfernt. Ihre Modelle liegen in {0}.",
|
||||
"s8601": "Grafikkarten",
|
||||
"s8602": "Haupt",
|
||||
"s8603": "Helfer",
|
||||
"s8604": "Ihre Wahl.",
|
||||
"s8605": "Empfohlen.",
|
||||
"s8606": "Sie haben die Rollen geändert. Strata verwendet genau diese.",
|
||||
"s8607": "Empfohlen: {0}",
|
||||
"s8608": "Rolle von {0}",
|
||||
"s8609": "{0} GB Speicher",
|
||||
"s8610": "Nutzt den Arbeitsspeicher Ihres Computers (integrierte Grafik)",
|
||||
"s8611": "Macht die Hauptarbeit",
|
||||
"s8612": "Hält einen Teil des Modells",
|
||||
"s8613": "Nicht genutzt",
|
||||
"s8614": "Wird von {0} verwendet",
|
||||
"s8615": "Ausschalten",
|
||||
"s8616": "{0} zur Hauptkarte machen",
|
||||
"s8617": "{0} kann Strata nicht ausführen.",
|
||||
"s8618": "{0} ist integrierte Grafik; als Hauptkarte ist sie langsam.",
|
||||
"s8619": "Nur mit der CPU ist die Leistung eingeschränkt.",
|
||||
"s8620": "Diese Engine braucht zum Start eine Grafikkarte. Schalten Sie eine ein.",
|
||||
"s8621": "Nicht genug Speicher für diese Einstellung: etwa {0} GB werden vom Laufwerk gelesen.",
|
||||
"s8622": "{0} ist jetzt ein Helfer.",
|
||||
"s8623": "{0} ist jetzt die Hauptkarte.",
|
||||
"s8624": "Die einzige eingeschaltete Karte muss die Hauptkarte sein.",
|
||||
"s8625": "Es wurde keine Grafikkarte gefunden, die Strata nutzen kann.",
|
||||
"s8626": "Strata verwendet Ihre {0}.",
|
||||
"s8627": "{0} ist die Hauptkarte: {1}.",
|
||||
"s8628": "die Karten sind gleich",
|
||||
"s8629": "sie hat mehr Speicher",
|
||||
"s8630": "sie ist schneller",
|
||||
"s8631": "sie ist die einzige Karte",
|
||||
"s8632": "{0} helfen.",
|
||||
"s8633": "{0} hilft.",
|
||||
"s8634": "{0} startet ausgeschaltet: {1}",
|
||||
"s8635": "Strata kann sie nicht ausführen.",
|
||||
"s8636": "integrierte Grafik.",
|
||||
"s8637": "{0} GB oder weniger.",
|
||||
"s8638": "Die Karte, die die letzten Schritte jeder Antwort übernimmt: Sie schreibt die nächsten Wörter, führt den Vorausschau-Schritt aus, prüft ihn und hält Ihr Gespräch. Wählen Sie Ihre schnellste Karte mit viel Speicher. Es gibt immer nur eine.",
|
||||
"s8639": "Hält einen Teil des Modells und seines schnellen Speichers, sodass mehr davon auf den Grafikkarten Platz findet statt vom Laufwerk gelesen zu werden. Prompts werden etwa 20 % schneller gelesen.",
|
||||
"s8640": "Strata rührt diese Karte nicht an. Gut für eine Karte, die Sie anderweitig nutzen, etwa für ein Spiel oder Ihren Bildschirm.",
|
||||
"s8641": "Das Modell braucht etwa {0} GB. Diese Karten und Ihr Arbeitsspeicher bieten etwa {1} GB. Es passt.",
|
||||
"s8642": "Das Modell braucht etwa {0} GB. Diese Karten und Ihr Arbeitsspeicher bieten etwa {1} GB, daher würden etwa {2} GB vom Laufwerk gelesen.",
|
||||
"s8643": "Nur Ihr Prozessor und Arbeitsspeicher werden verwendet.",
|
||||
"s8644": "Für Experten: Die Engine startet auf den Karten {0} (Hauptkarte zuletzt).",
|
||||
"s8645": "Wird nicht gespeichert, solange Sie die Empfehlung behalten: Strata wählt neu, wenn Sie eine Karte hinzufügen oder entfernen.",
|
||||
"s8646": "Nicht genutzt: {0} (anderer Grafikkarten-Hersteller als der, den diese Engine ansteuert)."
|
||||
}
|
||||
@@ -3346,5 +3346,148 @@
|
||||
"s8257": "every {0} min",
|
||||
"s8258": "This Strata engine has no code for the graphics card it tried to use.",
|
||||
"s8259": "Pick another card under Run on, or open Details to see which cards were found and what the engine is built for.",
|
||||
"s8260": "This switch does nothing in the app. Web pages from any site can already call the API shown under Serving. To allow only some sites, or to use the engine’s own server (port 8080), add \"cors_origins\": [\"https://your.site\"] to the model’s config file."
|
||||
"s8260": "This switch does nothing in the app. Web pages from any site can already call the API shown under Serving. To allow only some sites, or to use the engine’s own server (port 8080), add \"cors_origins\": [\"https://your.site\"] to the model’s config file.",
|
||||
"s8301": "Create an API key (recommended)",
|
||||
"s8302": "Without an API key, any device on this network can use your model.",
|
||||
"s8303": "Only the inference API is shared. Clients need your API key when one is set. Use a trusted local network; this connection uses HTTP.",
|
||||
"s8304": "Create a key to protect your API. You’ll return here when it’s ready. You can also continue without one, but anyone who finds the link can then use your model.",
|
||||
"s8305": "Already have a supported model on this computer? Choose the folder that holds its files, or one of its .gguf files. Strata checks them, prepares the model and loads it. Nothing is copied or downloaded.",
|
||||
"s8306": "Folder with the GGUF files, or one .gguf file",
|
||||
"s8307": "Full path to the folder or the file",
|
||||
"s8308": "Without an API key, anyone who can reach this address can use your model.",
|
||||
"s8309": "Strata could not use {0} for models ({1}), so models go to {2}. You can pick another folder in Settings.",
|
||||
"s8310": "Port {0} is already in use. Strata may already be running; close it or the other program, or start Strata with another port (STRATA_PORT).",
|
||||
"s8311": "Windows or a firewall does not let Strata use port {0}. Hyper-V, WSL and Docker keep ranges of ports for themselves (see \"netsh int ipv4 show excludedportrange protocol=tcp\"). Start Strata with another port (STRATA_PORT), for example 8801.",
|
||||
"s8312": "{0} is not an address of this computer, so Strata cannot listen on it. Check STRATA_BIND.",
|
||||
"s8313": "Strata cannot listen on {0}:{1} ({2}).",
|
||||
"s8339": "Models folder",
|
||||
"s8340": "Strata saves the big model files here.",
|
||||
"s8341": "Change folder",
|
||||
"s8342": "Waiting for the folder…",
|
||||
"s8343": "Disk use",
|
||||
"s8344": "{0} free",
|
||||
"s8345": "on this drive",
|
||||
"s8346": "Your models use {0}",
|
||||
"s8347": "Finish or stop the download to change the folder.",
|
||||
"s8348": "That folder isn't there",
|
||||
"s8349": "That folder can't be used",
|
||||
"s8350": "Strata can't save files in {0}.",
|
||||
"s8351": "Strata can't find {0}.",
|
||||
"s8352": "Pick a folder outside the current one.",
|
||||
"s8353": "Choose a folder you own, for example one in Documents or on another drive.",
|
||||
"s8354": "Check that the drive is plugged in, then pick the folder again. Your models were not touched.",
|
||||
"s8355": "Use this folder for your models?",
|
||||
"s8356": "New folder",
|
||||
"s8357": "{0} free on this drive",
|
||||
"s8358": "You picked the whole drive {0}. Strata keeps its files in a folder of their own, so your models go in {1}.",
|
||||
"s8359": "The three choices come next.",
|
||||
"s8360": "Move my models",
|
||||
"s8361": "Use both folders",
|
||||
"s8362": "Start empty",
|
||||
"s8363": "Moving your models",
|
||||
"s8364": "Stop moving",
|
||||
"s8365": "{0} of {1} GB",
|
||||
"s8366": "Less than a minute left",
|
||||
"s8367": "About a minute left",
|
||||
"s8368": "About {0} minutes left",
|
||||
"s8369": "About {0} hours left",
|
||||
"s8370": "Keep Strata open. You can keep using other apps. Your models cannot be loaded until the move is done.",
|
||||
"s8371": "Now copying: {0}, file {1} of {2}",
|
||||
"s8372": "From {0} to {1}",
|
||||
"s8373": "Move my models there",
|
||||
"s8374": "You have no models to move.",
|
||||
"s8375": "Strata moves your {0} of models to the new folder. This is the same drive, so it takes a moment.",
|
||||
"s8376": "Strata copies your {0} of models to the new folder, checks them, then removes the old copies.",
|
||||
"s8377": "Needs no extra space.",
|
||||
"s8378": "Needs {0} free in the new folder. You have {1}.",
|
||||
"s8379": "Keep them where they are and also use the new folder",
|
||||
"s8380": "Nothing is copied. Your current models keep working, and new downloads go to the new folder.",
|
||||
"s8381": "Needs no space now. New downloads must fit in {1}.",
|
||||
"s8382": "Needs no space now.",
|
||||
"s8383": "Strata uses the new folder from now on and does not list your current models. Your old files are not deleted.",
|
||||
"s8384": "Needs no space now. Each model you download later needs its own space.",
|
||||
"s8385": "Your models need {0}, this drive has {1} free.",
|
||||
"s8386": "Free up about {0} there, pick another folder, or choose one of the other two options.",
|
||||
"s8387": "What should happen to your models",
|
||||
"s8388": "A running model is stopped first; load it again when the move is done.",
|
||||
"s8389": "{0} would move.",
|
||||
"s8390": "Free space is measured on the drive that holds {0}.",
|
||||
"s8391": "Old folder: {0}",
|
||||
"s8392": "Strata copies one file at a time and checks its size before it removes the old one.",
|
||||
"s8393": "You can stop at any time; files already moved stay usable.",
|
||||
"s8394": "The drive ran out of space",
|
||||
"s8395": "The move stopped",
|
||||
"s8396": "{0} filled up after {1}, so the move stopped.",
|
||||
"s8397": "The models could not be moved.",
|
||||
"s8398": "Nothing was lost. Models already moved are in the new folder, the rest are still in {0}, and the half-copied file was removed. Free up space on {1} or choose another folder, then try again.",
|
||||
"s8399": "Nothing was lost. All your models are still in {0}, and the half-copied file was removed. Free up space on {1} or choose another folder, then try again.",
|
||||
"s8400": "Nothing was lost. Your models that were not moved are still in {0}. Choose another folder to try again.",
|
||||
"s8401": "Moving stopped",
|
||||
"s8402": "Nothing was lost.",
|
||||
"s8403": "{0} is in the new folder.",
|
||||
"s8404": "{0} is still in the old folder.",
|
||||
"s8405": "Both work.",
|
||||
"s8406": "Models in both folders are listed as normal. You can finish moving later with Change folder.",
|
||||
"s8407": "Your models are moved",
|
||||
"s8408": "{0} now live in {1}",
|
||||
"s8409": "The old copies were removed and {0} is free again on {1}.",
|
||||
"s8410": "Strata now also uses {0}. New downloads go there, and your current models stay where they are.",
|
||||
"s8411": "Strata now uses {0}. Your old model files are still in {1}; Strata does not delete them.",
|
||||
"s8412": "Changing the models folder is part of the Strata app.",
|
||||
"s8413": "A model is running. Stop it first, then change the folder.",
|
||||
"s8414": "Choose a models folder.",
|
||||
"s8415": "Choose what to do with your models.",
|
||||
"s8416": "That choice is not possible here.",
|
||||
"s8417": "A move was interrupted. Finish it or leave it.",
|
||||
"s8418": "Strata was moving your models from {0} to {1} when it stopped. Nothing is lost.",
|
||||
"s8419": "Finish the move",
|
||||
"s8420": "Leave it",
|
||||
"s8421": "Finishing puts all your models in {1}. Leaving it keeps them in {0} and removes the partial copies from {1}.",
|
||||
"s8422": "The partial copies were removed. Your models are in {0}.",
|
||||
"s8601": "Graphics cards",
|
||||
"s8602": "Main",
|
||||
"s8603": "Helper",
|
||||
"s8604": "Your choice.",
|
||||
"s8605": "Recommended.",
|
||||
"s8606": "You changed the roles. Strata will use exactly these.",
|
||||
"s8607": "Recommended: {0}",
|
||||
"s8608": "Role of {0}",
|
||||
"s8609": "{0} GB memory",
|
||||
"s8610": "Uses your computer's memory (built-in graphics)",
|
||||
"s8611": "Does the main work",
|
||||
"s8612": "Holds part of the model",
|
||||
"s8613": "Not used",
|
||||
"s8614": "In use by {0}",
|
||||
"s8615": "Turn it off",
|
||||
"s8616": "Make {0} Main",
|
||||
"s8617": "{0} cannot run Strata.",
|
||||
"s8618": "{0} is built-in graphics; it is slow as Main.",
|
||||
"s8619": "Performance is degraded on CPU only.",
|
||||
"s8620": "This engine needs a graphics card to start. Turn one on.",
|
||||
"s8621": "Not enough memory for this setup: about {0} GB is read from the drive.",
|
||||
"s8622": "{0} is now a Helper.",
|
||||
"s8623": "{0} is now Main.",
|
||||
"s8624": "The only card that is on has to be Main.",
|
||||
"s8625": "No graphics card that Strata can use was found.",
|
||||
"s8626": "Strata will use your {0}.",
|
||||
"s8627": "{0} is Main: {1}.",
|
||||
"s8628": "the cards are alike",
|
||||
"s8629": "it has more memory",
|
||||
"s8630": "it is faster",
|
||||
"s8631": "it is the only card",
|
||||
"s8632": "{0} help.",
|
||||
"s8633": "{0} helps.",
|
||||
"s8634": "{0} starts Off: {1}",
|
||||
"s8635": "Strata cannot run it.",
|
||||
"s8636": "built-in graphics.",
|
||||
"s8637": "{0} GB or less.",
|
||||
"s8638": "The card that does the final steps of every answer: it writes the next words, runs the draft-ahead step and checks it, and keeps your conversation. Pick your fastest card with plenty of memory. There is only ever one.",
|
||||
"s8639": "Holds a share of the model and its fast memory, so more of it fits on graphics cards instead of being read from your drive. Prompts are read about 20% faster.",
|
||||
"s8640": "Strata does not touch this card. Good for a card you use for something else, such as a game or your screen.",
|
||||
"s8641": "The model needs about {0} GB. These cards and your memory give about {1} GB. It fits.",
|
||||
"s8642": "The model needs about {0} GB. These cards and your memory give about {1} GB, so about {2} GB would be read from your drive.",
|
||||
"s8643": "Only your processor and memory are used.",
|
||||
"s8644": "For experts: the engine starts on cards {0} (Main last).",
|
||||
"s8645": "Not saved while you keep the recommendation: Strata picks again if you add or remove a card.",
|
||||
"s8646": "Not used: {0} (another make of graphics card than this engine drives)."
|
||||
}
|
||||
@@ -3346,5 +3346,148 @@
|
||||
"s8257": "cada {0} min",
|
||||
"s8258": "Este motor de Strata no tiene código para la tarjeta gráfica que intentó usar.",
|
||||
"s8259": "Elige otra tarjeta en «Ejecutar en», o abre los detalles para ver qué tarjetas se encontraron y para cuáles está compilado el motor.",
|
||||
"s8260": "Este interruptor no hace nada en la app. Las páginas web de cualquier sitio ya pueden llamar a la API que se muestra en Servicio. Para permitir solo algunos sitios, o para usar el servidor propio del motor (puerto 8080), añada \"cors_origins\": [\"https://su.sitio\"] al archivo de configuración del modelo."
|
||||
"s8260": "Este interruptor no hace nada en la app. Las páginas web de cualquier sitio ya pueden llamar a la API que se muestra en Servicio. Para permitir solo algunos sitios, o para usar el servidor propio del motor (puerto 8080), añada \"cors_origins\": [\"https://su.sitio\"] al archivo de configuración del modelo.",
|
||||
"s8301": "Crear una clave de API (recomendado)",
|
||||
"s8302": "Sin una clave de API, cualquier dispositivo de esta red puede usar tu modelo.",
|
||||
"s8303": "Solo se comparte la API de inferencia. Los clientes necesitan tu clave de API si hay una. Usa una red local de confianza; esta conexión usa HTTP.",
|
||||
"s8304": "Crea una clave para proteger tu API. Volverás aquí cuando esté lista. También puedes continuar sin ella, pero cualquiera que encuentre el enlace podrá usar tu modelo.",
|
||||
"s8305": "¿Ya tienes un modelo compatible en este equipo? Elige la carpeta con sus archivos o uno de sus archivos .gguf. Strata los comprueba, prepara el modelo y lo carga. No se copia ni se descarga nada.",
|
||||
"s8306": "Carpeta con los archivos GGUF, o un archivo .gguf",
|
||||
"s8307": "Ruta completa de la carpeta o del archivo",
|
||||
"s8308": "Sin una clave de API, cualquiera que pueda llegar a esta dirección podrá usar tu modelo.",
|
||||
"s8309": "Strata no pudo usar {0} para los modelos ({1}), así que los modelos van a {2}. Puedes elegir otra carpeta en Ajustes.",
|
||||
"s8310": "El puerto {0} ya está en uso. Puede que Strata ya se esté ejecutando; ciérralo o cierra el otro programa, o inicia Strata con otro puerto (STRATA_PORT).",
|
||||
"s8311": "Windows o un cortafuegos no deja que Strata use el puerto {0}. Hyper-V, WSL y Docker reservan rangos de puertos para sí mismos (consulta \"netsh int ipv4 show excludedportrange protocol=tcp\"). Inicia Strata con otro puerto (STRATA_PORT), por ejemplo 8801.",
|
||||
"s8312": "{0} no es una dirección de este equipo, así que Strata no puede escuchar en ella. Revisa STRATA_BIND.",
|
||||
"s8313": "Strata no puede escuchar en {0}:{1} ({2}).",
|
||||
"s8339": "Carpeta de modelos",
|
||||
"s8340": "Strata guarda aquí los archivos grandes de los modelos.",
|
||||
"s8341": "Cambiar carpeta",
|
||||
"s8342": "Esperando la carpeta…",
|
||||
"s8343": "Uso del disco",
|
||||
"s8344": "{0} libres",
|
||||
"s8345": "en esta unidad",
|
||||
"s8346": "Tus modelos ocupan {0}",
|
||||
"s8347": "Termina o detén la descarga para cambiar la carpeta.",
|
||||
"s8348": "Esa carpeta no existe",
|
||||
"s8349": "No se puede usar esa carpeta",
|
||||
"s8350": "Strata no puede guardar archivos en {0}.",
|
||||
"s8351": "Strata no encuentra {0}.",
|
||||
"s8352": "Elige una carpeta fuera de la actual.",
|
||||
"s8353": "Elige una carpeta tuya, por ejemplo una en Documentos o en otra unidad.",
|
||||
"s8354": "Comprueba que la unidad esté conectada y vuelve a elegir la carpeta. Tus modelos no se han tocado.",
|
||||
"s8355": "¿Usar esta carpeta para tus modelos?",
|
||||
"s8356": "Carpeta nueva",
|
||||
"s8357": "{0} libres en esta unidad",
|
||||
"s8358": "Has elegido toda la unidad {0}. Strata guarda sus archivos en una carpeta propia, así que tus modelos irán en {1}.",
|
||||
"s8359": "A continuación verás las tres opciones.",
|
||||
"s8360": "Mover mis modelos",
|
||||
"s8361": "Usar ambas carpetas",
|
||||
"s8362": "Empezar vacío",
|
||||
"s8363": "Moviendo tus modelos",
|
||||
"s8364": "Detener el traslado",
|
||||
"s8365": "{0} de {1} GB",
|
||||
"s8366": "Queda menos de un minuto",
|
||||
"s8367": "Queda aproximadamente un minuto",
|
||||
"s8368": "Quedan aproximadamente {0} minutos",
|
||||
"s8369": "Quedan aproximadamente {0} horas",
|
||||
"s8370": "Mantén Strata abierto. Puedes seguir usando otras aplicaciones. Tus modelos no se podrán cargar hasta que termine el traslado.",
|
||||
"s8371": "Copiando ahora: {0}, archivo {1} de {2}",
|
||||
"s8372": "De {0} a {1}",
|
||||
"s8373": "Mover mis modelos allí",
|
||||
"s8374": "No tienes modelos que mover.",
|
||||
"s8375": "Strata mueve tus {0} de modelos a la carpeta nueva. Es la misma unidad, así que tarda un instante.",
|
||||
"s8376": "Strata copia tus {0} de modelos a la carpeta nueva, los comprueba y después elimina las copias antiguas.",
|
||||
"s8377": "No necesita espacio adicional.",
|
||||
"s8378": "Necesita {0} libres en la carpeta nueva. Tienes {1}.",
|
||||
"s8379": "Dejarlos donde están y usar también la carpeta nueva",
|
||||
"s8380": "No se copia nada. Tus modelos actuales siguen funcionando y las descargas nuevas van a la carpeta nueva.",
|
||||
"s8381": "No necesita espacio ahora. Las descargas nuevas deben caber en {1}.",
|
||||
"s8382": "No necesita espacio ahora.",
|
||||
"s8383": "Strata usará la carpeta nueva a partir de ahora y no mostrará tus modelos actuales. Tus archivos antiguos no se borran.",
|
||||
"s8384": "No necesita espacio ahora. Cada modelo que descargues después necesitará su propio espacio.",
|
||||
"s8385": "Tus modelos necesitan {0} y esta unidad tiene {1} libres.",
|
||||
"s8386": "Libera unos {0} allí, elige otra carpeta o escoge una de las otras dos opciones.",
|
||||
"s8387": "Qué debe pasar con tus modelos",
|
||||
"s8388": "Un modelo en ejecución se detiene antes; cárgalo de nuevo cuando termine el traslado.",
|
||||
"s8389": "Se moverían {0}.",
|
||||
"s8390": "El espacio libre se mide en la unidad que contiene {0}.",
|
||||
"s8391": "Carpeta anterior: {0}",
|
||||
"s8392": "Strata copia un archivo cada vez y comprueba su tamaño antes de eliminar el antiguo.",
|
||||
"s8393": "Puedes detenerlo en cualquier momento; los archivos ya movidos siguen siendo utilizables.",
|
||||
"s8394": "La unidad se quedó sin espacio",
|
||||
"s8395": "El traslado se detuvo",
|
||||
"s8396": "{0} se llenó tras {1}, así que el traslado se detuvo.",
|
||||
"s8397": "No se pudieron mover los modelos.",
|
||||
"s8398": "No se perdió nada. Los modelos ya movidos están en la carpeta nueva, el resto sigue en {0} y se eliminó el archivo copiado a medias. Libera espacio en {1} o elige otra carpeta y vuelve a intentarlo.",
|
||||
"s8399": "No se perdió nada. Todos tus modelos siguen en {0} y se eliminó el archivo copiado a medias. Libera espacio en {1} o elige otra carpeta y vuelve a intentarlo.",
|
||||
"s8400": "No se perdió nada. Los modelos que no se movieron siguen en {0}. Elige otra carpeta para volver a intentarlo.",
|
||||
"s8401": "Traslado detenido",
|
||||
"s8402": "No se perdió nada.",
|
||||
"s8403": "{0} está en la carpeta nueva.",
|
||||
"s8404": "{0} sigue en la carpeta anterior.",
|
||||
"s8405": "Ambos funcionan.",
|
||||
"s8406": "Los modelos de ambas carpetas aparecen como siempre. Puedes terminar el traslado más tarde con «Cambiar carpeta».",
|
||||
"s8407": "Tus modelos se han movido",
|
||||
"s8408": "{0} están ahora en {1}",
|
||||
"s8409": "Se eliminaron las copias antiguas y vuelven a estar libres {0} en {1}.",
|
||||
"s8410": "Strata ahora también usa {0}. Las descargas nuevas van allí y tus modelos actuales se quedan donde están.",
|
||||
"s8411": "Strata ahora usa {0}. Tus archivos de modelos antiguos siguen en {1}; Strata no los borra.",
|
||||
"s8412": "Cambiar la carpeta de modelos forma parte de la aplicación Strata.",
|
||||
"s8413": "Hay un modelo en ejecución. Deténlo primero y después cambia la carpeta.",
|
||||
"s8414": "Elige una carpeta de modelos.",
|
||||
"s8415": "Elige qué hacer con tus modelos.",
|
||||
"s8416": "Esa opción no es posible aquí.",
|
||||
"s8417": "Se interrumpió un traslado. Termínelo o déjelo como estaba.",
|
||||
"s8418": "Strata estaba trasladando sus modelos de {0} a {1} cuando se detuvo. No se ha perdido nada.",
|
||||
"s8419": "Terminar el traslado",
|
||||
"s8420": "Dejarlo",
|
||||
"s8421": "Terminar pone todos sus modelos en {1}. Dejarlo los mantiene en {0} y elimina las copias parciales de {1}.",
|
||||
"s8422": "Se eliminaron las copias parciales. Sus modelos están en {0}.",
|
||||
"s8601": "Tarjetas gráficas",
|
||||
"s8602": "Principal",
|
||||
"s8603": "Ayudante",
|
||||
"s8604": "Tu elección.",
|
||||
"s8605": "Recomendado.",
|
||||
"s8606": "Cambiaste los roles. Strata usará exactamente estos.",
|
||||
"s8607": "Recomendado: {0}",
|
||||
"s8608": "Rol de {0}",
|
||||
"s8609": "{0} GB de memoria",
|
||||
"s8610": "Usa la memoria de tu equipo (gráficos integrados)",
|
||||
"s8611": "Hace el trabajo principal",
|
||||
"s8612": "Guarda parte del modelo",
|
||||
"s8613": "No se usa",
|
||||
"s8614": "En uso por {0}",
|
||||
"s8615": "Apagarla",
|
||||
"s8616": "Hacer principal a {0}",
|
||||
"s8617": "{0} no puede ejecutar Strata.",
|
||||
"s8618": "{0} son gráficos integrados; como principal es lenta.",
|
||||
"s8619": "El rendimiento se reduce al usar solo la CPU.",
|
||||
"s8620": "Este motor necesita una tarjeta gráfica para arrancar. Activa una.",
|
||||
"s8621": "Memoria insuficiente para esta configuración: unos {0} GB se leen del disco.",
|
||||
"s8622": "{0} ahora es ayudante.",
|
||||
"s8623": "{0} ahora es la principal.",
|
||||
"s8624": "La única tarjeta activada tiene que ser la principal.",
|
||||
"s8625": "No se encontró ninguna tarjeta gráfica que Strata pueda usar.",
|
||||
"s8626": "Strata usará tu {0}.",
|
||||
"s8627": "{0} es la principal: {1}.",
|
||||
"s8628": "las tarjetas son iguales",
|
||||
"s8629": "tiene más memoria",
|
||||
"s8630": "es más rápida",
|
||||
"s8631": "es la única tarjeta",
|
||||
"s8632": "{0} ayudan.",
|
||||
"s8633": "{0} ayuda.",
|
||||
"s8634": "{0} empieza apagada: {1}",
|
||||
"s8635": "Strata no puede ejecutarla.",
|
||||
"s8636": "gráficos integrados.",
|
||||
"s8637": "{0} GB o menos.",
|
||||
"s8638": "La tarjeta que hace los pasos finales de cada respuesta: escribe las siguientes palabras, ejecuta el paso de adelanto y lo comprueba, y guarda tu conversación. Elige tu tarjeta más rápida con mucha memoria. Solo hay una.",
|
||||
"s8639": "Guarda una parte del modelo y de su memoria rápida, así cabe más en las tarjetas gráficas en lugar de leerse del disco. Los prompts se leen un 20 % más rápido.",
|
||||
"s8640": "Strata no toca esta tarjeta. Útil para una tarjeta que usas para otra cosa, como un juego o tu pantalla.",
|
||||
"s8641": "El modelo necesita unos {0} GB. Estas tarjetas y tu memoria suman unos {1} GB. Cabe.",
|
||||
"s8642": "El modelo necesita unos {0} GB. Estas tarjetas y tu memoria suman unos {1} GB, así que unos {2} GB se leerían del disco.",
|
||||
"s8643": "Solo se usan tu procesador y tu memoria.",
|
||||
"s8644": "Para expertos: el motor arranca en las tarjetas {0} (la principal al final).",
|
||||
"s8645": "No se guarda mientras mantengas la recomendación: Strata elige de nuevo si añades o quitas una tarjeta.",
|
||||
"s8646": "No se usa: {0} (otra marca de tarjeta gráfica distinta de la que maneja este motor)."
|
||||
}
|
||||
@@ -3346,5 +3346,148 @@
|
||||
"s8257": "{0}分ごと",
|
||||
"s8258": "この Strata エンジンには、使おうとしたグラフィックスカード用のコードが含まれていません。",
|
||||
"s8259": "「実行先」で別のカードを選ぶか、「詳細」を開いて、見つかったカードとエンジンの対応状況を確認してください。",
|
||||
"s8260": "このスイッチはアプリでは何もしません。どのサイトのWebページも、「配信」に表示されるAPIをすでに呼び出せます。一部のサイトだけを許可したい場合や、エンジン自身のサーバー(ポート8080)を使う場合は、モデルの設定ファイルに \"cors_origins\": [\"https://your.site\"] を追加してください。"
|
||||
"s8260": "このスイッチはアプリでは何もしません。どのサイトのWebページも、「配信」に表示されるAPIをすでに呼び出せます。一部のサイトだけを許可したい場合や、エンジン自身のサーバー(ポート8080)を使う場合は、モデルの設定ファイルに \"cors_origins\": [\"https://your.site\"] を追加してください。",
|
||||
"s8301": "APIキーを作成する(推奨)",
|
||||
"s8302": "APIキーがないと、このネットワーク上のどの機器でもモデルを使えます。",
|
||||
"s8303": "共有されるのは推論APIだけです。キーを設定している場合、クライアントにはAPIキーが必要です。信頼できるローカルネットワークで使ってください。この接続はHTTPを使います。",
|
||||
"s8304": "APIを守るキーを作成します。準備ができたらここに戻ります。キーなしで続けることもできますが、その場合はリンクを見つけた人なら誰でもモデルを使えます。",
|
||||
"s8305": "対応モデルがすでにこのコンピューターにありますか?ファイルがあるフォルダー、または.ggufファイルを1つ選んでください。Strataがファイルを確認し、モデルを準備して読み込みます。コピーやダウンロードは行いません。",
|
||||
"s8306": "GGUFファイルのフォルダー、または.ggufファイル1つ",
|
||||
"s8307": "フォルダーまたはファイルのフルパス",
|
||||
"s8308": "APIキーがないと、このアドレスに接続できる人なら誰でもモデルを使えます。",
|
||||
"s8309": "Strataは{0}をモデルの保存先として使えなかったため({1})、モデルは{2}に保存されます。別のフォルダーは設定で選べます。",
|
||||
"s8310": "ポート{0}はすでに使われています。Strataがすでに起動しているかもしれません。それか別のプログラムを閉じるか、別のポート(STRATA_PORT)でStrataを起動してください。",
|
||||
"s8311": "Windowsまたはファイアウォールが、Strataによるポート{0}の使用を許可していません。Hyper-V、WSL、Dockerは一部のポート範囲を自分用に確保します(\"netsh int ipv4 show excludedportrange protocol=tcp\"で確認できます)。別のポート(STRATA_PORT)、たとえば8801でStrataを起動してください。",
|
||||
"s8312": "{0}はこのコンピューターのアドレスではないため、Strataはそこで待ち受けできません。STRATA_BINDを確認してください。",
|
||||
"s8313": "Strataは{0}:{1}で待ち受けできません({2})。",
|
||||
"s8339": "モデルフォルダ",
|
||||
"s8340": "Strata は大きなモデルファイルをここに保存します。",
|
||||
"s8341": "フォルダを変更",
|
||||
"s8342": "フォルダを待っています…",
|
||||
"s8343": "ディスクの使用量",
|
||||
"s8344": "空き {0}",
|
||||
"s8345": "(このドライブ)",
|
||||
"s8346": "モデルの使用量: {0}",
|
||||
"s8347": "フォルダを変更するには、ダウンロードを完了するか停止してください。",
|
||||
"s8348": "そのフォルダは見つかりません",
|
||||
"s8349": "そのフォルダは使えません",
|
||||
"s8350": "Strata は {0} にファイルを保存できません。",
|
||||
"s8351": "Strata は {0} を見つけられません。",
|
||||
"s8352": "現在のフォルダの外にあるフォルダを選んでください。",
|
||||
"s8353": "ご自分のフォルダ(ドキュメント内や別のドライブ上など)を選んでください。",
|
||||
"s8354": "ドライブが接続されているか確認してから、もう一度フォルダを選んでください。モデルには手を加えていません。",
|
||||
"s8355": "このフォルダをモデル用に使いますか?",
|
||||
"s8356": "新しいフォルダ",
|
||||
"s8357": "このドライブの空き: {0}",
|
||||
"s8358": "ドライブ全体 {0} を選びました。Strata は専用のフォルダにファイルを置くため、モデルは {1} に保存されます。",
|
||||
"s8359": "次に 3 つの選択肢が表示されます。",
|
||||
"s8360": "モデルを移動する",
|
||||
"s8361": "両方のフォルダを使う",
|
||||
"s8362": "空の状態で始める",
|
||||
"s8363": "モデルを移動しています",
|
||||
"s8364": "移動を停止",
|
||||
"s8365": "{0} / {1} GB",
|
||||
"s8366": "残り 1 分未満",
|
||||
"s8367": "残り約 1 分",
|
||||
"s8368": "残り約 {0} 分",
|
||||
"s8369": "残り約 {0} 時間",
|
||||
"s8370": "Strata を開いたままにしてください。ほかのアプリは使い続けられます。移動が終わるまでモデルは読み込めません。",
|
||||
"s8371": "コピー中: {0}、ファイル {1} / {2}",
|
||||
"s8372": "{0} から {1} へ",
|
||||
"s8373": "モデルをそこへ移動する",
|
||||
"s8374": "移動するモデルがありません。",
|
||||
"s8375": "Strata はモデル {0} を新しいフォルダへ移動します。同じドライブなので、すぐに終わります。",
|
||||
"s8376": "Strata はモデル {0} を新しいフォルダへコピーし、確認してから古いコピーを削除します。",
|
||||
"s8377": "追加の空き容量は不要です。",
|
||||
"s8378": "新しいフォルダに {0} の空きが必要です。現在の空き: {1}。",
|
||||
"s8379": "今の場所に残し、新しいフォルダも使う",
|
||||
"s8380": "コピーは行いません。今あるモデルはそのまま使え、新しいダウンロードは新しいフォルダに保存されます。",
|
||||
"s8381": "今は空き容量は不要です。新しいダウンロードは {1} に収まる必要があります。",
|
||||
"s8382": "今は空き容量は不要です。",
|
||||
"s8383": "今後は新しいフォルダを使い、今のモデルは一覧に表示しません。古いファイルは削除されません。",
|
||||
"s8384": "今は空き容量は不要です。あとでダウンロードするモデルごとに、その分の空きが必要です。",
|
||||
"s8385": "モデルには {0} が必要ですが、このドライブの空きは {1} です。",
|
||||
"s8386": "そこで約 {0} を空けるか、別のフォルダを選ぶか、ほかの 2 つの選択肢のどちらかを選んでください。",
|
||||
"s8387": "モデルをどう扱うか",
|
||||
"s8388": "実行中のモデルは先に停止されます。移動が終わったらもう一度読み込んでください。",
|
||||
"s8389": "移動対象: {0}。",
|
||||
"s8390": "空き容量は {0} があるドライブで測定しています。",
|
||||
"s8391": "元のフォルダ: {0}",
|
||||
"s8392": "Strata は 1 ファイルずつコピーし、サイズを確認してから古いファイルを削除します。",
|
||||
"s8393": "いつでも停止できます。移動済みのファイルはそのまま使えます。",
|
||||
"s8394": "ドライブの空き容量が足りなくなりました",
|
||||
"s8395": "移動が停止しました",
|
||||
"s8396": "{1} を移動したところで {0} がいっぱいになったため、移動を停止しました。",
|
||||
"s8397": "モデルを移動できませんでした。",
|
||||
"s8398": "失われたものはありません。移動済みのモデルは新しいフォルダにあり、残りは {0} にあります。途中までコピーされたファイルは削除しました。{1} の空きを増やすか別のフォルダを選んで、もう一度お試しください。",
|
||||
"s8399": "失われたものはありません。モデルはすべて {0} に残っており、途中までコピーされたファイルは削除しました。{1} の空きを増やすか別のフォルダを選んで、もう一度お試しください。",
|
||||
"s8400": "失われたものはありません。移動されなかったモデルは {0} に残っています。別のフォルダを選んで、もう一度お試しください。",
|
||||
"s8401": "移動を停止しました",
|
||||
"s8402": "失われたものはありません。",
|
||||
"s8403": "{0} は新しいフォルダにあります。",
|
||||
"s8404": "{0} は元のフォルダに残っています。",
|
||||
"s8405": "どちらも使えます。",
|
||||
"s8406": "どちらのフォルダのモデルも通常どおり一覧に表示されます。移動の続きは、あとで「フォルダを変更」から行えます。",
|
||||
"s8407": "モデルを移動しました",
|
||||
"s8408": "{0} が {1} に移動しました",
|
||||
"s8409": "古いコピーを削除し、{1} の空きが {0} 増えました。",
|
||||
"s8410": "Strata は {0} も使うようになりました。新しいダウンロードはそこに保存され、今のモデルはそのままの場所に残ります。",
|
||||
"s8411": "Strata は {0} を使うようになりました。古いモデルファイルは {1} に残っており、Strata が削除することはありません。",
|
||||
"s8412": "モデルフォルダの変更は Strata アプリの機能です。",
|
||||
"s8413": "モデルが実行中です。先に停止してから、フォルダを変更してください。",
|
||||
"s8414": "モデルフォルダを選んでください。",
|
||||
"s8415": "モデルをどうするか選んでください。",
|
||||
"s8416": "この選択肢はここでは使えません。",
|
||||
"s8417": "移動が中断されました。完了するか、中止してください。",
|
||||
"s8418": "Strata がモデルを {0} から {1} へ移動している途中で止まりました。失われたものはありません。",
|
||||
"s8419": "移動を完了する",
|
||||
"s8420": "中止する",
|
||||
"s8421": "完了すると、すべてのモデルが {1} に入ります。中止すると、モデルは {0} に残り、{1} の不完全なコピーが削除されます。",
|
||||
"s8422": "不完全なコピーを削除しました。モデルは {0} にあります。",
|
||||
"s8601": "グラフィックカード",
|
||||
"s8602": "メイン",
|
||||
"s8603": "ヘルパー",
|
||||
"s8604": "あなたの選択です。",
|
||||
"s8605": "おすすめです。",
|
||||
"s8606": "役割を変更しました。Strata はこのとおりに使います。",
|
||||
"s8607": "おすすめ:{0}",
|
||||
"s8608": "{0} の役割",
|
||||
"s8609": "メモリ {0} GB",
|
||||
"s8610": "コンピューター本体のメモリを使います(内蔵グラフィックス)",
|
||||
"s8611": "主な処理を担当",
|
||||
"s8612": "モデルの一部を保持",
|
||||
"s8613": "使用しません",
|
||||
"s8614": "{0} が使用中",
|
||||
"s8615": "オフにする",
|
||||
"s8616": "{0} をメインにする",
|
||||
"s8617": "{0} では Strata を実行できません。",
|
||||
"s8618": "{0} は内蔵グラフィックスで、メインにすると遅くなります。",
|
||||
"s8619": "CPU のみの場合、性能が低下します。",
|
||||
"s8620": "このエンジンの起動にはグラフィックカードが必要です。1 枚オンにしてください。",
|
||||
"s8621": "この設定ではメモリが足りません。約 {0} GB をドライブから読み込みます。",
|
||||
"s8622": "{0} はヘルパーになりました。",
|
||||
"s8623": "{0} がメインになりました。",
|
||||
"s8624": "オンになっている唯一のカードはメインでなければなりません。",
|
||||
"s8625": "Strata が使えるグラフィックカードが見つかりません。",
|
||||
"s8626": "Strata は {0} を使います。",
|
||||
"s8627": "{0} がメインです:{1}。",
|
||||
"s8628": "カードが同じため",
|
||||
"s8629": "メモリが多いため",
|
||||
"s8630": "より高速なため",
|
||||
"s8631": "唯一のカードのため",
|
||||
"s8632": "{0} がサポートします。",
|
||||
"s8633": "{0} がサポートします。",
|
||||
"s8634": "{0} は最初オフです:{1}",
|
||||
"s8635": "Strata では実行できません。",
|
||||
"s8636": "内蔵グラフィックスです。",
|
||||
"s8637": "{0} GB 以下です。",
|
||||
"s8638": "回答の最後の工程を担当するカードです。次の単語を書き、先読みの工程を実行して確認し、会話を保持します。メモリに余裕のある一番速いカードを選んでください。常に 1 枚だけです。",
|
||||
"s8639": "モデルの一部と高速メモリを保持し、ドライブから読み込む代わりにより多くをグラフィックカード上に置けます。プロンプトの読み込みが約 20% 速くなります。",
|
||||
"s8640": "Strata はこのカードに触れません。ゲームや画面表示など、別の用途に使うカードに向いています。",
|
||||
"s8641": "モデルには約 {0} GB が必要です。これらのカードとメモリで約 {1} GB あり、収まります。",
|
||||
"s8642": "モデルには約 {0} GB が必要です。これらのカードとメモリは約 {1} GB のため、約 {2} GB がドライブから読み込まれます。",
|
||||
"s8643": "プロセッサとメモリのみを使います。",
|
||||
"s8644": "上級者向け:エンジンはカード {0} で起動します(メインが最後)。",
|
||||
"s8645": "おすすめのままの間は保存されません。カードを追加・取り外すと Strata が選び直します。",
|
||||
"s8646": "未使用:{0}(このエンジンが対応するのとは別メーカーのカードです)。"
|
||||
}
|
||||
@@ -3346,5 +3346,148 @@
|
||||
"s8257": "{0}분마다",
|
||||
"s8258": "이 Strata 엔진에는 사용하려던 그래픽 카드용 코드가 없습니다.",
|
||||
"s8259": "“실행 장치”에서 다른 카드를 고르거나, “세부 정보”에서 찾은 카드와 엔진이 지원하는 카드를 확인하세요.",
|
||||
"s8260": "이 스위치는 앱에서는 아무 일도 하지 않습니다. 어느 사이트의 웹 페이지든 ‘API 서비스’에 표시된 API를 이미 호출할 수 있습니다. 일부 사이트만 허용하거나 엔진의 자체 서버(포트 8080)를 쓰려면 모델의 설정 파일에 \"cors_origins\": [\"https://your.site\"] 를 추가하세요."
|
||||
"s8260": "이 스위치는 앱에서는 아무 일도 하지 않습니다. 어느 사이트의 웹 페이지든 ‘API 서비스’에 표시된 API를 이미 호출할 수 있습니다. 일부 사이트만 허용하거나 엔진의 자체 서버(포트 8080)를 쓰려면 모델의 설정 파일에 \"cors_origins\": [\"https://your.site\"] 를 추가하세요.",
|
||||
"s8301": "API 키 만들기(권장)",
|
||||
"s8302": "API 키가 없으면 이 네트워크의 모든 기기가 모델을 사용할 수 있습니다.",
|
||||
"s8303": "추론 API만 공유됩니다. 키를 설정한 경우 클라이언트에 API 키가 필요합니다. 신뢰할 수 있는 로컬 네트워크에서 사용하세요. 이 연결은 HTTP를 사용합니다.",
|
||||
"s8304": "API를 보호할 키를 만드세요. 준비되면 이 화면으로 돌아옵니다. 키 없이 계속할 수도 있지만, 그러면 링크를 찾은 누구나 모델을 사용할 수 있습니다.",
|
||||
"s8305": "이 컴퓨터에 이미 지원되는 모델이 있나요? 파일이 있는 폴더나 .gguf 파일 하나를 선택하세요. Strata가 파일을 확인하고 모델을 준비해 불러옵니다. 복사하거나 다운로드하지 않습니다.",
|
||||
"s8306": "GGUF 파일이 있는 폴더 또는 .gguf 파일 하나",
|
||||
"s8307": "폴더 또는 파일의 전체 경로",
|
||||
"s8308": "API 키가 없으면 이 주소에 접속할 수 있는 누구나 모델을 사용할 수 있습니다.",
|
||||
"s8309": "Strata가 {0}을(를) 모델 폴더로 사용할 수 없어({1}) 모델을 {2}에 저장합니다. 설정에서 다른 폴더를 고를 수 있습니다.",
|
||||
"s8310": "포트 {0}을(를) 이미 사용 중입니다. Strata가 이미 실행 중일 수 있습니다. 해당 프로그램을 닫거나 다른 포트(STRATA_PORT)로 Strata를 시작하세요.",
|
||||
"s8311": "Windows 또는 방화벽이 Strata의 포트 {0} 사용을 허용하지 않습니다. Hyper-V, WSL, Docker는 일부 포트 범위를 자체적으로 예약합니다(\"netsh int ipv4 show excludedportrange protocol=tcp\" 참고). 다른 포트(STRATA_PORT), 예를 들어 8801로 Strata를 시작하세요.",
|
||||
"s8312": "{0}은(는) 이 컴퓨터의 주소가 아니어서 Strata가 해당 주소에서 연결을 받을 수 없습니다. STRATA_BIND를 확인하세요.",
|
||||
"s8313": "Strata가 {0}:{1}에서 연결을 받을 수 없습니다({2}).",
|
||||
"s8339": "모델 폴더",
|
||||
"s8340": "Strata는 큰 모델 파일을 여기에 저장합니다.",
|
||||
"s8341": "폴더 변경",
|
||||
"s8342": "폴더를 기다리는 중…",
|
||||
"s8343": "디스크 사용량",
|
||||
"s8344": "{0} 여유",
|
||||
"s8345": "(이 드라이브)",
|
||||
"s8346": "모델이 차지하는 공간: {0}",
|
||||
"s8347": "폴더를 바꾸려면 다운로드를 끝내거나 중지하세요.",
|
||||
"s8348": "해당 폴더가 없습니다",
|
||||
"s8349": "해당 폴더는 사용할 수 없습니다",
|
||||
"s8350": "Strata가 {0}에 파일을 저장할 수 없습니다.",
|
||||
"s8351": "Strata가 {0}을(를) 찾을 수 없습니다.",
|
||||
"s8352": "현재 폴더 밖에 있는 폴더를 선택하세요.",
|
||||
"s8353": "내 소유의 폴더(예: 문서 폴더나 다른 드라이브)를 선택하세요.",
|
||||
"s8354": "드라이브가 연결되어 있는지 확인한 뒤 폴더를 다시 선택하세요. 모델은 그대로입니다.",
|
||||
"s8355": "이 폴더를 모델용으로 사용할까요?",
|
||||
"s8356": "새 폴더",
|
||||
"s8357": "이 드라이브의 여유 공간: {0}",
|
||||
"s8358": "드라이브 전체 {0}을(를) 선택했습니다. Strata는 자체 폴더에 파일을 보관하므로 모델은 {1}에 저장됩니다.",
|
||||
"s8359": "다음으로 세 가지 선택지가 나옵니다.",
|
||||
"s8360": "내 모델 이동",
|
||||
"s8361": "두 폴더 모두 사용",
|
||||
"s8362": "빈 상태로 시작",
|
||||
"s8363": "모델을 이동하는 중",
|
||||
"s8364": "이동 중지",
|
||||
"s8365": "{0} / {1} GB",
|
||||
"s8366": "1분 미만 남음",
|
||||
"s8367": "약 1분 남음",
|
||||
"s8368": "약 {0}분 남음",
|
||||
"s8369": "약 {0}시간 남음",
|
||||
"s8370": "Strata를 열어 두세요. 다른 앱은 계속 사용할 수 있습니다. 이동이 끝날 때까지 모델을 불러올 수 없습니다.",
|
||||
"s8371": "복사 중: {0}, 파일 {1}/{2}",
|
||||
"s8372": "{0}에서 {1}(으)로",
|
||||
"s8373": "내 모델을 그쪽으로 이동",
|
||||
"s8374": "이동할 모델이 없습니다.",
|
||||
"s8375": "Strata가 모델 {0}을(를) 새 폴더로 옮깁니다. 같은 드라이브라 금방 끝납니다.",
|
||||
"s8376": "Strata가 모델 {0}을(를) 새 폴더로 복사하고 확인한 뒤 기존 사본을 삭제합니다.",
|
||||
"s8377": "추가 공간이 필요 없습니다.",
|
||||
"s8378": "새 폴더에 {0}의 여유 공간이 필요합니다. 현재 {1}.",
|
||||
"s8379": "지금 위치에 두고 새 폴더도 함께 사용",
|
||||
"s8380": "아무것도 복사하지 않습니다. 현재 모델은 그대로 동작하고, 새로 받는 모델은 새 폴더에 저장됩니다.",
|
||||
"s8381": "지금은 공간이 필요 없습니다. 새 다운로드는 {1} 안에 들어가야 합니다.",
|
||||
"s8382": "지금은 공간이 필요 없습니다.",
|
||||
"s8383": "앞으로 새 폴더를 사용하며 현재 모델은 목록에 표시하지 않습니다. 기존 파일은 삭제되지 않습니다.",
|
||||
"s8384": "지금은 공간이 필요 없습니다. 나중에 받는 모델마다 각자의 공간이 필요합니다.",
|
||||
"s8385": "모델에 {0}이(가) 필요한데 이 드라이브의 여유 공간은 {1}입니다.",
|
||||
"s8386": "그곳에서 약 {0}을(를) 비우거나, 다른 폴더를 선택하거나, 나머지 두 가지 중 하나를 고르세요.",
|
||||
"s8387": "모델을 어떻게 할까요",
|
||||
"s8388": "실행 중인 모델은 먼저 중지됩니다. 이동이 끝나면 다시 불러오세요.",
|
||||
"s8389": "{0}이(가) 이동됩니다.",
|
||||
"s8390": "여유 공간은 {0}이(가) 있는 드라이브를 기준으로 측정합니다.",
|
||||
"s8391": "기존 폴더: {0}",
|
||||
"s8392": "Strata는 파일을 하나씩 복사하고, 크기를 확인한 뒤에 기존 파일을 삭제합니다.",
|
||||
"s8393": "언제든 중지할 수 있으며, 이미 이동한 파일은 계속 사용할 수 있습니다.",
|
||||
"s8394": "드라이브의 공간이 부족합니다",
|
||||
"s8395": "이동이 중지되었습니다",
|
||||
"s8396": "{1}을(를) 옮긴 뒤 {0}이(가) 가득 차서 이동을 중지했습니다.",
|
||||
"s8397": "모델을 이동하지 못했습니다.",
|
||||
"s8398": "잃어버린 것은 없습니다. 이미 이동한 모델은 새 폴더에 있고 나머지는 {0}에 그대로 있으며, 반쯤 복사된 파일은 삭제했습니다. {1}의 공간을 확보하거나 다른 폴더를 선택한 뒤 다시 시도하세요.",
|
||||
"s8399": "잃어버린 것은 없습니다. 모든 모델이 {0}에 그대로 있으며, 반쯤 복사된 파일은 삭제했습니다. {1}의 공간을 확보하거나 다른 폴더를 선택한 뒤 다시 시도하세요.",
|
||||
"s8400": "잃어버린 것은 없습니다. 이동하지 못한 모델은 {0}에 그대로 있습니다. 다른 폴더를 선택해 다시 시도하세요.",
|
||||
"s8401": "이동 중지됨",
|
||||
"s8402": "잃어버린 것은 없습니다.",
|
||||
"s8403": "{0}은(는) 새 폴더에 있습니다.",
|
||||
"s8404": "{0}은(는) 아직 기존 폴더에 있습니다.",
|
||||
"s8405": "둘 다 사용할 수 있습니다.",
|
||||
"s8406": "두 폴더의 모델 모두 평소처럼 목록에 표시됩니다. 나머지 이동은 나중에 '폴더 변경'으로 마칠 수 있습니다.",
|
||||
"s8407": "모델 이동이 끝났습니다",
|
||||
"s8408": "{0}이(가) 이제 {1}에 있습니다",
|
||||
"s8409": "기존 사본을 삭제했으며 {1}에서 {0}이(가) 다시 확보되었습니다.",
|
||||
"s8410": "이제 Strata가 {0}도 사용합니다. 새 다운로드는 그곳에 저장되고 현재 모델은 그대로 있습니다.",
|
||||
"s8411": "이제 Strata가 {0}을(를) 사용합니다. 기존 모델 파일은 {1}에 그대로 있으며 Strata가 삭제하지 않습니다.",
|
||||
"s8412": "모델 폴더 변경은 Strata 앱의 기능입니다.",
|
||||
"s8413": "모델이 실행 중입니다. 먼저 중지한 뒤 폴더를 변경하세요.",
|
||||
"s8414": "모델 폴더를 선택하세요.",
|
||||
"s8415": "모델을 어떻게 할지 선택하세요.",
|
||||
"s8416": "여기서는 해당 선택지를 사용할 수 없습니다.",
|
||||
"s8417": "이동이 중단되었습니다. 마무리하거나 그대로 두세요.",
|
||||
"s8418": "Strata가 모델을 {0}에서 {1}(으)로 옮기던 중 멈췄습니다. 잃어버린 것은 없습니다.",
|
||||
"s8419": "이동 마무리",
|
||||
"s8420": "그대로 두기",
|
||||
"s8421": "마무리하면 모든 모델이 {1}에 들어갑니다. 그대로 두면 모델은 {0}에 남고 {1}의 불완전한 복사본이 삭제됩니다.",
|
||||
"s8422": "불완전한 복사본을 삭제했습니다. 모델은 {0}에 있습니다.",
|
||||
"s8601": "그래픽 카드",
|
||||
"s8602": "메인",
|
||||
"s8603": "헬퍼",
|
||||
"s8604": "직접 고른 설정입니다.",
|
||||
"s8605": "추천 설정입니다.",
|
||||
"s8606": "역할을 변경했습니다. Strata는 이 설정을 그대로 사용합니다.",
|
||||
"s8607": "추천: {0}",
|
||||
"s8608": "{0}의 역할",
|
||||
"s8609": "메모리 {0} GB",
|
||||
"s8610": "컴퓨터 본체 메모리를 사용합니다 (내장 그래픽)",
|
||||
"s8611": "주요 작업을 처리합니다",
|
||||
"s8612": "모델의 일부를 담당합니다",
|
||||
"s8613": "사용 안 함",
|
||||
"s8614": "{0}에서 사용 중",
|
||||
"s8615": "끄기",
|
||||
"s8616": "{0}을(를) 메인으로 지정",
|
||||
"s8617": "{0}에서는 Strata를 실행할 수 없습니다.",
|
||||
"s8618": "{0}은(는) 내장 그래픽이라 메인으로 쓰면 느립니다.",
|
||||
"s8619": "CPU만 사용하면 성능이 떨어집니다.",
|
||||
"s8620": "이 엔진은 시작하려면 그래픽 카드가 필요합니다. 하나를 켜세요.",
|
||||
"s8621": "이 설정에는 메모리가 부족합니다. 약 {0} GB를 드라이브에서 읽습니다.",
|
||||
"s8622": "{0}이(가) 이제 헬퍼입니다.",
|
||||
"s8623": "{0}이(가) 이제 메인입니다.",
|
||||
"s8624": "켜져 있는 유일한 카드는 메인이어야 합니다.",
|
||||
"s8625": "Strata가 사용할 수 있는 그래픽 카드를 찾지 못했습니다.",
|
||||
"s8626": "Strata가 {0}을(를) 사용합니다.",
|
||||
"s8627": "{0}이(가) 메인입니다: {1}.",
|
||||
"s8628": "카드가 같습니다",
|
||||
"s8629": "메모리가 더 많습니다",
|
||||
"s8630": "더 빠릅니다",
|
||||
"s8631": "유일한 카드입니다",
|
||||
"s8632": "{0}이(가) 돕습니다.",
|
||||
"s8633": "{0}이(가) 돕습니다.",
|
||||
"s8634": "{0}은(는) 기본적으로 꺼집니다: {1}",
|
||||
"s8635": "Strata에서 실행할 수 없습니다.",
|
||||
"s8636": "내장 그래픽입니다.",
|
||||
"s8637": "{0} GB 이하입니다.",
|
||||
"s8638": "모든 답변의 마지막 단계를 맡는 카드입니다. 다음 단어를 쓰고, 미리 계산하는 단계를 실행해 확인하며, 대화를 보관합니다. 메모리가 넉넉한 가장 빠른 카드를 고르세요. 항상 하나뿐입니다.",
|
||||
"s8639": "모델의 일부와 빠른 메모리를 담당해, 더 많은 부분이 드라이브가 아니라 그래픽 카드에 올라갑니다. 프롬프트를 약 20% 더 빠르게 읽습니다.",
|
||||
"s8640": "Strata가 이 카드를 건드리지 않습니다. 게임이나 화면 출력 등 다른 용도로 쓰는 카드에 알맞습니다.",
|
||||
"s8641": "모델에는 약 {0} GB가 필요합니다. 이 카드들과 메모리는 약 {1} GB를 제공하므로 충분합니다.",
|
||||
"s8642": "모델에는 약 {0} GB가 필요합니다. 이 카드들과 메모리는 약 {1} GB를 제공하므로 약 {2} GB를 드라이브에서 읽게 됩니다.",
|
||||
"s8643": "프로세서와 메모리만 사용합니다.",
|
||||
"s8644": "전문가용: 엔진은 카드 {0}에서 시작합니다 (메인이 마지막).",
|
||||
"s8645": "추천 설정을 유지하는 동안은 저장되지 않습니다. 카드를 추가하거나 빼면 Strata가 다시 고릅니다.",
|
||||
"s8646": "사용 안 함: {0} (이 엔진이 다루는 것과 다른 제조사의 그래픽 카드입니다)."
|
||||
}
|
||||
@@ -3346,5 +3346,148 @@
|
||||
"s8257": "a cada {0} min",
|
||||
"s8258": "Este motor do Strata não tem código para a placa de vídeo que tentou usar.",
|
||||
"s8259": "Escolha outra placa em «Executar em» ou abra os detalhes para ver quais placas foram encontradas e para quais o motor foi compilado.",
|
||||
"s8260": "Este botão não faz nada no app. Páginas web de qualquer site já podem chamar a API mostrada em API. Para permitir só alguns sites, ou para usar o servidor próprio do motor (porta 8080), adicione \"cors_origins\": [\"https://seu.site\"] ao arquivo de configuração do modelo."
|
||||
"s8260": "Este botão não faz nada no app. Páginas web de qualquer site já podem chamar a API mostrada em API. Para permitir só alguns sites, ou para usar o servidor próprio do motor (porta 8080), adicione \"cors_origins\": [\"https://seu.site\"] ao arquivo de configuração do modelo.",
|
||||
"s8301": "Criar uma chave de API (recomendado)",
|
||||
"s8302": "Sem uma chave de API, qualquer dispositivo desta rede pode usar o seu modelo.",
|
||||
"s8303": "Apenas a API de inferência é compartilhada. Os clientes precisam da sua chave de API quando houver uma. Use uma rede local confiável; esta conexão usa HTTP.",
|
||||
"s8304": "Crie uma chave para proteger a sua API. Você voltará aqui quando ela estiver pronta. Também pode continuar sem ela, mas qualquer pessoa que encontrar o link poderá usar o seu modelo.",
|
||||
"s8305": "Já tem um modelo compatível neste computador? Escolha a pasta com os arquivos ou um dos arquivos .gguf. O Strata verifica os arquivos, prepara o modelo e o carrega. Nada é copiado nem baixado.",
|
||||
"s8306": "Pasta com os arquivos GGUF ou um arquivo .gguf",
|
||||
"s8307": "Caminho completo da pasta ou do arquivo",
|
||||
"s8308": "Sem uma chave de API, qualquer pessoa que consiga acessar este endereço pode usar o seu modelo.",
|
||||
"s8309": "O Strata não conseguiu usar {0} para os modelos ({1}), então os modelos vão para {2}. Você pode escolher outra pasta em Configurações.",
|
||||
"s8310": "A porta {0} já está em uso. O Strata pode já estar em execução; feche-o ou o outro programa, ou inicie o Strata com outra porta (STRATA_PORT).",
|
||||
"s8311": "O Windows ou um firewall não deixa o Strata usar a porta {0}. Hyper-V, WSL e Docker reservam faixas de portas para si (veja \"netsh int ipv4 show excludedportrange protocol=tcp\"). Inicie o Strata com outra porta (STRATA_PORT), por exemplo 8801.",
|
||||
"s8312": "{0} não é um endereço deste computador, então o Strata não pode escutar nele. Verifique STRATA_BIND.",
|
||||
"s8313": "O Strata não consegue escutar em {0}:{1} ({2}).",
|
||||
"s8339": "Pasta de modelos",
|
||||
"s8340": "O Strata guarda aqui os arquivos grandes dos modelos.",
|
||||
"s8341": "Alterar pasta",
|
||||
"s8342": "Aguardando a pasta…",
|
||||
"s8343": "Uso do disco",
|
||||
"s8344": "{0} livres",
|
||||
"s8345": "nesta unidade",
|
||||
"s8346": "Seus modelos ocupam {0}",
|
||||
"s8347": "Conclua ou pare o download para alterar a pasta.",
|
||||
"s8348": "Essa pasta não existe",
|
||||
"s8349": "Não é possível usar essa pasta",
|
||||
"s8350": "O Strata não consegue salvar arquivos em {0}.",
|
||||
"s8351": "O Strata não encontra {0}.",
|
||||
"s8352": "Escolha uma pasta fora da atual.",
|
||||
"s8353": "Escolha uma pasta sua, por exemplo em Documentos ou em outra unidade.",
|
||||
"s8354": "Verifique se a unidade está conectada e escolha a pasta de novo. Seus modelos não foram alterados.",
|
||||
"s8355": "Usar esta pasta para seus modelos?",
|
||||
"s8356": "Pasta nova",
|
||||
"s8357": "{0} livres nesta unidade",
|
||||
"s8358": "Você escolheu a unidade inteira {0}. O Strata guarda seus arquivos em uma pasta própria, então seus modelos ficam em {1}.",
|
||||
"s8359": "Em seguida, você verá as três opções.",
|
||||
"s8360": "Mover meus modelos",
|
||||
"s8361": "Usar as duas pastas",
|
||||
"s8362": "Começar vazio",
|
||||
"s8363": "Movendo seus modelos",
|
||||
"s8364": "Parar a movimentação",
|
||||
"s8365": "{0} de {1} GB",
|
||||
"s8366": "Menos de um minuto restante",
|
||||
"s8367": "Cerca de um minuto restante",
|
||||
"s8368": "Cerca de {0} minutos restantes",
|
||||
"s8369": "Cerca de {0} horas restantes",
|
||||
"s8370": "Mantenha o Strata aberto. Você pode continuar usando outros aplicativos. Seus modelos só poderão ser carregados quando a movimentação terminar.",
|
||||
"s8371": "Copiando agora: {0}, arquivo {1} de {2}",
|
||||
"s8372": "De {0} para {1}",
|
||||
"s8373": "Mover meus modelos para lá",
|
||||
"s8374": "Você não tem modelos para mover.",
|
||||
"s8375": "O Strata move seus {0} de modelos para a pasta nova. É a mesma unidade, então leva só um instante.",
|
||||
"s8376": "O Strata copia seus {0} de modelos para a pasta nova, confere os arquivos e depois remove as cópias antigas.",
|
||||
"s8377": "Não precisa de espaço extra.",
|
||||
"s8378": "Precisa de {0} livres na pasta nova. Você tem {1}.",
|
||||
"s8379": "Mantê-los onde estão e usar também a pasta nova",
|
||||
"s8380": "Nada é copiado. Seus modelos atuais continuam funcionando e os novos downloads vão para a pasta nova.",
|
||||
"s8381": "Não precisa de espaço agora. Os novos downloads precisam caber em {1}.",
|
||||
"s8382": "Não precisa de espaço agora.",
|
||||
"s8383": "O Strata passa a usar a pasta nova e não lista seus modelos atuais. Seus arquivos antigos não são apagados.",
|
||||
"s8384": "Não precisa de espaço agora. Cada modelo baixado depois precisa do próprio espaço.",
|
||||
"s8385": "Seus modelos precisam de {0}, e esta unidade tem {1} livres.",
|
||||
"s8386": "Libere cerca de {0} lá, escolha outra pasta ou use uma das outras duas opções.",
|
||||
"s8387": "O que deve acontecer com seus modelos",
|
||||
"s8388": "Um modelo em execução é parado antes; carregue-o de novo quando a movimentação terminar.",
|
||||
"s8389": "{0} seriam movidos.",
|
||||
"s8390": "O espaço livre é medido na unidade que contém {0}.",
|
||||
"s8391": "Pasta antiga: {0}",
|
||||
"s8392": "O Strata copia um arquivo por vez e confere o tamanho antes de remover o antigo.",
|
||||
"s8393": "Você pode parar a qualquer momento; os arquivos já movidos continuam utilizáveis.",
|
||||
"s8394": "A unidade ficou sem espaço",
|
||||
"s8395": "A movimentação parou",
|
||||
"s8396": "{0} ficou cheia após {1}, então a movimentação parou.",
|
||||
"s8397": "Não foi possível mover os modelos.",
|
||||
"s8398": "Nada foi perdido. Os modelos já movidos estão na pasta nova, o restante continua em {0} e o arquivo copiado pela metade foi removido. Libere espaço em {1} ou escolha outra pasta e tente de novo.",
|
||||
"s8399": "Nada foi perdido. Todos os seus modelos continuam em {0} e o arquivo copiado pela metade foi removido. Libere espaço em {1} ou escolha outra pasta e tente de novo.",
|
||||
"s8400": "Nada foi perdido. Os modelos que não foram movidos continuam em {0}. Escolha outra pasta para tentar de novo.",
|
||||
"s8401": "Movimentação interrompida",
|
||||
"s8402": "Nada foi perdido.",
|
||||
"s8403": "{0} está na pasta nova.",
|
||||
"s8404": "{0} ainda está na pasta antiga.",
|
||||
"s8405": "Ambos funcionam.",
|
||||
"s8406": "Os modelos das duas pastas aparecem normalmente. Você pode concluir a movimentação depois, em “Alterar pasta”.",
|
||||
"s8407": "Seus modelos foram movidos",
|
||||
"s8408": "{0} agora estão em {1}",
|
||||
"s8409": "As cópias antigas foram removidas e {0} voltaram a ficar livres em {1}.",
|
||||
"s8410": "O Strata agora também usa {0}. Os novos downloads vão para lá, e seus modelos atuais ficam onde estão.",
|
||||
"s8411": "O Strata agora usa {0}. Seus arquivos de modelo antigos continuam em {1}; o Strata não os apaga.",
|
||||
"s8412": "Alterar a pasta de modelos faz parte do aplicativo Strata.",
|
||||
"s8413": "Há um modelo em execução. Pare-o primeiro e depois altere a pasta.",
|
||||
"s8414": "Escolha uma pasta de modelos.",
|
||||
"s8415": "Escolha o que fazer com seus modelos.",
|
||||
"s8416": "Essa opção não é possível aqui.",
|
||||
"s8417": "Uma mudança foi interrompida. Termine-a ou deixe como estava.",
|
||||
"s8418": "O Strata estava mudando seus modelos de {0} para {1} quando parou. Nada foi perdido.",
|
||||
"s8419": "Terminar a mudança",
|
||||
"s8420": "Deixar como está",
|
||||
"s8421": "Terminar coloca todos os seus modelos em {1}. Deixar como está mantém os modelos em {0} e remove as cópias parciais de {1}.",
|
||||
"s8422": "As cópias parciais foram removidas. Seus modelos estão em {0}.",
|
||||
"s8601": "Placas de vídeo",
|
||||
"s8602": "Principal",
|
||||
"s8603": "Auxiliar",
|
||||
"s8604": "Sua escolha.",
|
||||
"s8605": "Recomendado.",
|
||||
"s8606": "Você mudou as funções. O Strata usará exatamente estas.",
|
||||
"s8607": "Recomendado: {0}",
|
||||
"s8608": "Função de {0}",
|
||||
"s8609": "{0} GB de memória",
|
||||
"s8610": "Usa a memória do seu computador (gráficos integrados)",
|
||||
"s8611": "Faz o trabalho principal",
|
||||
"s8612": "Guarda parte do modelo",
|
||||
"s8613": "Não usada",
|
||||
"s8614": "Em uso por {0}",
|
||||
"s8615": "Desligar",
|
||||
"s8616": "Tornar {0} a principal",
|
||||
"s8617": "{0} não consegue executar o Strata.",
|
||||
"s8618": "{0} é um gráfico integrado; como principal é lenta.",
|
||||
"s8619": "O desempenho fica reduzido usando apenas a CPU.",
|
||||
"s8620": "Este motor precisa de uma placa de vídeo para iniciar. Ative uma.",
|
||||
"s8621": "Memória insuficiente para esta configuração: cerca de {0} GB são lidos do disco.",
|
||||
"s8622": "{0} agora é auxiliar.",
|
||||
"s8623": "{0} agora é a principal.",
|
||||
"s8624": "A única placa ligada tem de ser a principal.",
|
||||
"s8625": "Nenhuma placa de vídeo que o Strata possa usar foi encontrada.",
|
||||
"s8626": "O Strata usará a sua {0}.",
|
||||
"s8627": "{0} é a principal: {1}.",
|
||||
"s8628": "as placas são iguais",
|
||||
"s8629": "tem mais memória",
|
||||
"s8630": "é mais rápida",
|
||||
"s8631": "é a única placa",
|
||||
"s8632": "{0} ajudam.",
|
||||
"s8633": "{0} ajuda.",
|
||||
"s8634": "{0} começa desligada: {1}",
|
||||
"s8635": "O Strata não consegue executá-la.",
|
||||
"s8636": "gráficos integrados.",
|
||||
"s8637": "{0} GB ou menos.",
|
||||
"s8638": "A placa que faz os passos finais de cada resposta: escreve as próximas palavras, executa o passo de antecipação e o verifica, e guarda a sua conversa. Escolha a placa mais rápida com bastante memória. Há sempre só uma.",
|
||||
"s8639": "Guarda uma parte do modelo e da sua memória rápida, para que mais caiba nas placas de vídeo em vez de ser lido do disco. Os prompts são lidos cerca de 20% mais rápido.",
|
||||
"s8640": "O Strata não mexe nesta placa. Bom para uma placa que você usa para outra coisa, como um jogo ou a tela.",
|
||||
"s8641": "O modelo precisa de cerca de {0} GB. Estas placas e a sua memória dão cerca de {1} GB. Cabe.",
|
||||
"s8642": "O modelo precisa de cerca de {0} GB. Estas placas e a sua memória dão cerca de {1} GB, por isso cerca de {2} GB seriam lidos do disco.",
|
||||
"s8643": "Apenas o processador e a memória são usados.",
|
||||
"s8644": "Para especialistas: o motor inicia nas placas {0} (a principal por último).",
|
||||
"s8645": "Não é guardado enquanto mantiver a recomendação: o Strata escolhe de novo se adicionar ou remover uma placa.",
|
||||
"s8646": "Não usada: {0} (outra marca de placa de vídeo diferente da que este motor controla)."
|
||||
}
|
||||
@@ -3346,5 +3346,148 @@
|
||||
"s8257": "каждые {0} мин",
|
||||
"s8258": "В этом движке Strata нет кода для видеокарты, которую он попытался использовать.",
|
||||
"s8259": "Выберите другую карту в разделе «Запускать на» или откройте «Подробности»: там указано, какие карты найдены и для каких собран движок.",
|
||||
"s8260": "Этот переключатель в приложении ничего не делает. Веб-страницы любого сайта уже могут обращаться к API, который показан в «API». Чтобы разрешить только некоторые сайты или использовать собственный сервер движка (порт 8080), добавьте \"cors_origins\": [\"https://your.site\"] в файл настроек модели."
|
||||
"s8260": "Этот переключатель в приложении ничего не делает. Веб-страницы любого сайта уже могут обращаться к API, который показан в «API». Чтобы разрешить только некоторые сайты или использовать собственный сервер движка (порт 8080), добавьте \"cors_origins\": [\"https://your.site\"] в файл настроек модели.",
|
||||
"s8301": "Создать ключ API (рекомендуется)",
|
||||
"s8302": "Без ключа API любое устройство в этой сети сможет пользоваться вашей моделью.",
|
||||
"s8303": "Доступен только API инференса. Если ключ задан, клиентам нужен ваш ключ API. Используйте доверенную локальную сеть: это соединение работает по HTTP.",
|
||||
"s8304": "Создайте ключ, чтобы защитить API. Когда он будет готов, вы вернётесь сюда. Можно продолжить и без ключа, но тогда любой, кто найдёт ссылку, сможет пользоваться вашей моделью.",
|
||||
"s8305": "Уже есть поддерживаемая модель на этом компьютере? Выберите папку с её файлами или один из файлов .gguf. Strata проверит их, подготовит модель и загрузит её. Ничего не копируется и не скачивается.",
|
||||
"s8306": "Папка с файлами GGUF или один файл .gguf",
|
||||
"s8307": "Полный путь к папке или файлу",
|
||||
"s8308": "Без ключа API любой, кто может обратиться к этому адресу, сможет пользоваться вашей моделью.",
|
||||
"s8309": "Strata не смогла использовать {0} для моделей ({1}), поэтому модели сохраняются в {2}. Другую папку можно выбрать в настройках.",
|
||||
"s8310": "Порт {0} уже занят. Возможно, Strata уже запущена; закройте её или другую программу либо запустите Strata с другим портом (STRATA_PORT).",
|
||||
"s8311": "Windows или брандмауэр не разрешает Strata использовать порт {0}. Hyper-V, WSL и Docker резервируют для себя диапазоны портов (см. \"netsh int ipv4 show excludedportrange protocol=tcp\"). Запустите Strata с другим портом (STRATA_PORT), например 8801.",
|
||||
"s8312": "{0} не является адресом этого компьютера, поэтому Strata не может его прослушивать. Проверьте STRATA_BIND.",
|
||||
"s8313": "Strata не может прослушивать {0}:{1} ({2}).",
|
||||
"s8339": "Папка моделей",
|
||||
"s8340": "Strata хранит здесь большие файлы моделей.",
|
||||
"s8341": "Сменить папку",
|
||||
"s8342": "Ждём выбор папки…",
|
||||
"s8343": "Занято на диске",
|
||||
"s8344": "свободно {0}",
|
||||
"s8345": "на этом диске",
|
||||
"s8346": "Ваши модели занимают {0}",
|
||||
"s8347": "Чтобы сменить папку, дождитесь конца загрузки или остановите её.",
|
||||
"s8348": "Такой папки нет",
|
||||
"s8349": "Эту папку использовать нельзя",
|
||||
"s8350": "Strata не может сохранять файлы в {0}.",
|
||||
"s8351": "Strata не находит {0}.",
|
||||
"s8352": "Выберите папку вне текущей.",
|
||||
"s8353": "Выберите свою папку, например в «Документах» или на другом диске.",
|
||||
"s8354": "Проверьте, что диск подключён, и выберите папку снова. Ваши модели не затронуты.",
|
||||
"s8355": "Использовать эту папку для моделей?",
|
||||
"s8356": "Новая папка",
|
||||
"s8357": "Свободно на этом диске: {0}",
|
||||
"s8358": "Вы выбрали весь диск {0}. Strata хранит свои файлы в отдельной папке, поэтому модели будут в {1}.",
|
||||
"s8359": "Дальше будут три варианта.",
|
||||
"s8360": "Переместить мои модели",
|
||||
"s8361": "Использовать обе папки",
|
||||
"s8362": "Начать с пустой",
|
||||
"s8363": "Перемещение моделей",
|
||||
"s8364": "Остановить перемещение",
|
||||
"s8365": "{0} из {1} ГБ",
|
||||
"s8366": "Осталось меньше минуты",
|
||||
"s8367": "Осталось около минуты",
|
||||
"s8368": "Осталось около {0} мин.",
|
||||
"s8369": "Осталось около {0} ч.",
|
||||
"s8370": "Не закрывайте Strata. Другими приложениями можно пользоваться. Пока перемещение не закончится, модели загрузить нельзя.",
|
||||
"s8371": "Сейчас копируется: {0}, файл {1} из {2}",
|
||||
"s8372": "Из {0} в {1}",
|
||||
"s8373": "Переместить мои модели туда",
|
||||
"s8374": "Нет моделей для перемещения.",
|
||||
"s8375": "Strata переместит ваши модели ({0}) в новую папку. Это тот же диск, поэтому это займёт мгновение.",
|
||||
"s8376": "Strata скопирует ваши модели ({0}) в новую папку, проверит их и удалит старые копии.",
|
||||
"s8377": "Дополнительное место не нужно.",
|
||||
"s8378": "В новой папке нужно {0} свободного места. Сейчас доступно {1}.",
|
||||
"s8379": "Оставить на месте и дополнительно использовать новую папку",
|
||||
"s8380": "Ничего не копируется. Текущие модели продолжают работать, а новые загрузки идут в новую папку.",
|
||||
"s8381": "Сейчас место не нужно. Новые загрузки должны поместиться в {1}.",
|
||||
"s8382": "Сейчас место не нужно.",
|
||||
"s8383": "Отныне Strata использует новую папку и не показывает текущие модели. Старые файлы не удаляются.",
|
||||
"s8384": "Сейчас место не нужно. Каждой модели, которую вы загрузите позже, понадобится своё место.",
|
||||
"s8385": "Вашим моделям нужно {0}, а на этом диске свободно {1}.",
|
||||
"s8386": "Освободите там около {0}, выберите другую папку или один из двух других вариантов.",
|
||||
"s8387": "Что сделать с вашими моделями",
|
||||
"s8388": "Работающая модель сначала останавливается; загрузите её снова после перемещения.",
|
||||
"s8389": "Будет перемещено: {0}.",
|
||||
"s8390": "Свободное место измеряется на диске, где находится {0}.",
|
||||
"s8391": "Старая папка: {0}",
|
||||
"s8392": "Strata копирует файлы по одному и проверяет размер, прежде чем удалить старый.",
|
||||
"s8393": "Остановить можно в любой момент; уже перемещённые файлы остаются пригодными.",
|
||||
"s8394": "На диске закончилось место",
|
||||
"s8395": "Перемещение остановлено",
|
||||
"s8396": "Диск {0} заполнился после {1}, поэтому перемещение остановлено.",
|
||||
"s8397": "Не удалось переместить модели.",
|
||||
"s8398": "Ничего не потеряно. Уже перемещённые модели в новой папке, остальные по-прежнему в {0}, а наполовину скопированный файл удалён. Освободите место на {1} или выберите другую папку и повторите попытку.",
|
||||
"s8399": "Ничего не потеряно. Все ваши модели по-прежнему в {0}, а наполовину скопированный файл удалён. Освободите место на {1} или выберите другую папку и повторите попытку.",
|
||||
"s8400": "Ничего не потеряно. Неперемещённые модели по-прежнему в {0}. Выберите другую папку и повторите попытку.",
|
||||
"s8401": "Перемещение остановлено",
|
||||
"s8402": "Ничего не потеряно.",
|
||||
"s8403": "{0}: в новой папке.",
|
||||
"s8404": "{0}: всё ещё в старой папке.",
|
||||
"s8405": "Работают оба.",
|
||||
"s8406": "Модели из обеих папок отображаются как обычно. Закончить перемещение можно позже через «Сменить папку».",
|
||||
"s8407": "Модели перемещены",
|
||||
"s8408": "{0} теперь находятся в {1}",
|
||||
"s8409": "Старые копии удалены, на {1} снова свободно {0}.",
|
||||
"s8410": "Теперь Strata использует и {0}. Новые загрузки идут туда, а текущие модели остаются на месте.",
|
||||
"s8411": "Теперь Strata использует {0}. Старые файлы моделей остаются в {1}; Strata их не удаляет.",
|
||||
"s8412": "Смена папки моделей доступна в приложении Strata.",
|
||||
"s8413": "Модель запущена. Сначала остановите её, затем смените папку.",
|
||||
"s8414": "Выберите папку моделей.",
|
||||
"s8415": "Выберите, что сделать с моделями.",
|
||||
"s8416": "Этот вариант здесь невозможен.",
|
||||
"s8417": "Перенос был прерван. Завершите его или оставьте как есть.",
|
||||
"s8418": "Strata переносил ваши модели из {0} в {1}, когда остановился. Ничего не потеряно.",
|
||||
"s8419": "Завершить перенос",
|
||||
"s8420": "Оставить как есть",
|
||||
"s8421": "Если завершить, все модели окажутся в {1}. Если оставить, модели останутся в {0}, а неполные копии в {1} будут удалены.",
|
||||
"s8422": "Неполные копии удалены. Ваши модели находятся в {0}.",
|
||||
"s8601": "Видеокарты",
|
||||
"s8602": "Основная",
|
||||
"s8603": "Помощник",
|
||||
"s8604": "Ваш выбор.",
|
||||
"s8605": "Рекомендуется.",
|
||||
"s8606": "Вы изменили роли. Strata будет использовать именно их.",
|
||||
"s8607": "Рекомендуется: {0}",
|
||||
"s8608": "Роль {0}",
|
||||
"s8609": "{0} ГБ памяти",
|
||||
"s8610": "Использует память компьютера (встроенная графика)",
|
||||
"s8611": "Выполняет основную работу",
|
||||
"s8612": "Хранит часть модели",
|
||||
"s8613": "Не используется",
|
||||
"s8614": "Используется: {0}",
|
||||
"s8615": "Выключить",
|
||||
"s8616": "Сделать {0} основной",
|
||||
"s8617": "{0} не может запускать Strata.",
|
||||
"s8618": "{0} — встроенная графика; в роли основной она медленная.",
|
||||
"s8619": "Только на процессоре производительность снижена.",
|
||||
"s8620": "Для запуска этому движку нужна видеокарта. Включите одну.",
|
||||
"s8621": "Для этой настройки не хватает памяти: около {0} ГБ читается с диска.",
|
||||
"s8622": "{0} теперь помощник.",
|
||||
"s8623": "{0} теперь основная.",
|
||||
"s8624": "Единственная включённая карта должна быть основной.",
|
||||
"s8625": "Не найдено видеокарт, которые может использовать Strata.",
|
||||
"s8626": "Strata будет использовать ваш {0}.",
|
||||
"s8627": "{0} — основная: {1}.",
|
||||
"s8628": "карты одинаковые",
|
||||
"s8629": "у неё больше памяти",
|
||||
"s8630": "она быстрее",
|
||||
"s8631": "это единственная карта",
|
||||
"s8632": "{0} помогают.",
|
||||
"s8633": "{0} помогает.",
|
||||
"s8634": "{0} по умолчанию выключена: {1}",
|
||||
"s8635": "Strata не может её запустить.",
|
||||
"s8636": "встроенная графика.",
|
||||
"s8637": "{0} ГБ или меньше.",
|
||||
"s8638": "Карта, выполняющая последние шаги каждого ответа: пишет следующие слова, запускает шаг опережающего черновика и проверяет его, а также хранит вашу беседу. Выберите самую быструю карту с большим объёмом памяти. Основная карта всегда одна.",
|
||||
"s8639": "Хранит часть модели и её быстрой памяти, поэтому больше помещается на видеокартах и меньше читается с диска. Запросы читаются примерно на 20 % быстрее.",
|
||||
"s8640": "Strata не трогает эту карту. Подходит для карты, занятой другим, например игрой или выводом на экран.",
|
||||
"s8641": "Модели нужно около {0} ГБ. Эти карты и ваша память дают около {1} ГБ. Помещается.",
|
||||
"s8642": "Модели нужно около {0} ГБ. Эти карты и ваша память дают около {1} ГБ, поэтому около {2} ГБ придётся читать с диска.",
|
||||
"s8643": "Используются только процессор и память.",
|
||||
"s8644": "Для опытных: движок запускается на картах {0} (основная — последняя).",
|
||||
"s8645": "Не сохраняется, пока вы оставляете рекомендацию: Strata выберет заново, если вы добавите или уберёте карту.",
|
||||
"s8646": "Не используется: {0} (другой производитель видеокарт, чем тот, с которым работает этот движок)."
|
||||
}
|
||||
@@ -3346,5 +3346,148 @@
|
||||
"s8257": "每 {0} 分钟",
|
||||
"s8258": "此 Strata 引擎没有适用于它尝试使用的显卡的代码。",
|
||||
"s8259": "请在“运行于”中选择另一块显卡,或打开详情,查看找到了哪些显卡以及引擎支持哪些显卡。",
|
||||
"s8260": "这个开关在应用中不起作用。任何网站的网页都已经可以调用“API 服务”页面显示的 API。如果只想允许部分网站,或要使用引擎自带的服务器(端口 8080),请在模型的配置文件中添加 \"cors_origins\": [\"https://your.site\"]。"
|
||||
"s8260": "这个开关在应用中不起作用。任何网站的网页都已经可以调用“API 服务”页面显示的 API。如果只想允许部分网站,或要使用引擎自带的服务器(端口 8080),请在模型的配置文件中添加 \"cors_origins\": [\"https://your.site\"]。",
|
||||
"s8301": "创建 API 密钥(推荐)",
|
||||
"s8302": "没有 API 密钥时,此网络中的任何设备都可以使用你的模型。",
|
||||
"s8303": "只共享推理 API。如果设置了密钥,客户端需要你的 API 密钥。请在可信的本地网络中使用;此连接使用 HTTP。",
|
||||
"s8304": "创建一个密钥来保护你的 API。准备好后你会回到这里。你也可以不创建而继续,但这样任何找到链接的人都可以使用你的模型。",
|
||||
"s8305": "这台电脑上已经有受支持的模型了吗?请选择存放其文件的文件夹,或其中一个 .gguf 文件。Strata 会检查文件、准备模型并加载。不会复制或下载任何内容。",
|
||||
"s8306": "包含 GGUF 文件的文件夹,或一个 .gguf 文件",
|
||||
"s8307": "文件夹或文件的完整路径",
|
||||
"s8308": "没有 API 密钥时,任何能访问此地址的人都可以使用你的模型。",
|
||||
"s8309": "Strata 无法使用 {0} 存放模型({1}),因此模型会保存到 {2}。你可以在设置中选择其他文件夹。",
|
||||
"s8310": "端口 {0} 已被占用。Strata 可能已经在运行;请关闭它或另一个程序,或用其他端口(STRATA_PORT)启动 Strata。",
|
||||
"s8311": "Windows 或防火墙不允许 Strata 使用端口 {0}。Hyper-V、WSL 和 Docker 会为自己保留一些端口范围(请查看 \"netsh int ipv4 show excludedportrange protocol=tcp\")。请用其他端口(STRATA_PORT)启动 Strata,例如 8801。",
|
||||
"s8312": "{0} 不是这台电脑的地址,Strata 无法在其上监听。请检查 STRATA_BIND。",
|
||||
"s8313": "Strata 无法在 {0}:{1} 上监听({2})。",
|
||||
"s8339": "模型文件夹",
|
||||
"s8340": "Strata 会把体积较大的模型文件保存在这里。",
|
||||
"s8341": "更改文件夹",
|
||||
"s8342": "正在等待选择文件夹…",
|
||||
"s8343": "磁盘使用情况",
|
||||
"s8344": "可用 {0}",
|
||||
"s8345": "(此驱动器)",
|
||||
"s8346": "你的模型占用 {0}",
|
||||
"s8347": "请先完成或停止下载,再更改文件夹。",
|
||||
"s8348": "找不到该文件夹",
|
||||
"s8349": "无法使用该文件夹",
|
||||
"s8350": "Strata 无法在 {0} 中保存文件。",
|
||||
"s8351": "Strata 找不到 {0}。",
|
||||
"s8352": "请选择当前文件夹之外的文件夹。",
|
||||
"s8353": "请选择你自己的文件夹,例如“文档”中的文件夹或其他驱动器上的文件夹。",
|
||||
"s8354": "请检查驱动器是否已连接,然后重新选择文件夹。你的模型没有受到影响。",
|
||||
"s8355": "要用这个文件夹存放你的模型吗?",
|
||||
"s8356": "新文件夹",
|
||||
"s8357": "此驱动器可用 {0}",
|
||||
"s8358": "你选择了整个驱动器 {0}。Strata 会把文件放在自己的文件夹里,因此你的模型将存放在 {1}。",
|
||||
"s8359": "接下来会出现三个选项。",
|
||||
"s8360": "移动我的模型",
|
||||
"s8361": "同时使用两个文件夹",
|
||||
"s8362": "从空文件夹开始",
|
||||
"s8363": "正在移动你的模型",
|
||||
"s8364": "停止移动",
|
||||
"s8365": "{0} / {1} GB",
|
||||
"s8366": "剩余不到 1 分钟",
|
||||
"s8367": "大约剩余 1 分钟",
|
||||
"s8368": "大约剩余 {0} 分钟",
|
||||
"s8369": "大约剩余 {0} 小时",
|
||||
"s8370": "请保持 Strata 打开。你可以继续使用其他应用。移动完成之前无法加载你的模型。",
|
||||
"s8371": "正在复制:{0},第 {1} 个文件,共 {2} 个",
|
||||
"s8372": "从 {0} 到 {1}",
|
||||
"s8373": "把我的模型移到那里",
|
||||
"s8374": "没有可移动的模型。",
|
||||
"s8375": "Strata 会把你的 {0} 模型移到新文件夹。因为是同一个驱动器,只需片刻。",
|
||||
"s8376": "Strata 会把你的 {0} 模型复制到新文件夹,检查无误后再删除旧副本。",
|
||||
"s8377": "不需要额外空间。",
|
||||
"s8378": "新文件夹需要 {0} 可用空间。你现有 {1}。",
|
||||
"s8379": "保留在原处,同时使用新文件夹",
|
||||
"s8380": "不会复制任何文件。现有模型继续可用,新下载的模型会保存到新文件夹。",
|
||||
"s8381": "目前不需要空间。新下载的模型必须能放进 {1}。",
|
||||
"s8382": "目前不需要空间。",
|
||||
"s8383": "从现在起 Strata 使用新文件夹,不再列出你现有的模型。旧文件不会被删除。",
|
||||
"s8384": "目前不需要空间。之后下载的每个模型都需要各自的空间。",
|
||||
"s8385": "你的模型需要 {0},此驱动器只有 {1} 可用。",
|
||||
"s8386": "请在那里腾出约 {0},或选择其他文件夹,或选用另外两个选项之一。",
|
||||
"s8387": "如何处理你的模型",
|
||||
"s8388": "正在运行的模型会先被停止;移动完成后请重新加载。",
|
||||
"s8389": "将移动:{0}。",
|
||||
"s8390": "可用空间是按 {0} 所在的驱动器计算的。",
|
||||
"s8391": "旧文件夹:{0}",
|
||||
"s8392": "Strata 一次复制一个文件,检查大小后才会删除旧文件。",
|
||||
"s8393": "你可以随时停止;已经移动的文件仍可使用。",
|
||||
"s8394": "驱动器空间已用完",
|
||||
"s8395": "移动已停止",
|
||||
"s8396": "移动 {1} 后 {0} 已满,因此移动已停止。",
|
||||
"s8397": "无法移动模型。",
|
||||
"s8398": "没有任何内容丢失。已移动的模型在新文件夹中,其余的仍在 {0},复制到一半的文件已被删除。请在 {1} 上腾出空间或选择其他文件夹,然后重试。",
|
||||
"s8399": "没有任何内容丢失。你的所有模型仍在 {0},复制到一半的文件已被删除。请在 {1} 上腾出空间或选择其他文件夹,然后重试。",
|
||||
"s8400": "没有任何内容丢失。未移动的模型仍在 {0}。请选择其他文件夹后重试。",
|
||||
"s8401": "移动已停止",
|
||||
"s8402": "没有任何内容丢失。",
|
||||
"s8403": "{0} 在新文件夹中。",
|
||||
"s8404": "{0} 仍在旧文件夹中。",
|
||||
"s8405": "两处都能使用。",
|
||||
"s8406": "两个文件夹中的模型都会照常显示。你可以稍后通过“更改文件夹”继续移动。",
|
||||
"s8407": "你的模型已移动",
|
||||
"s8408": "{0} 现已位于 {1}",
|
||||
"s8409": "旧副本已删除,{1} 上重新空出了 {0}。",
|
||||
"s8410": "Strata 现在也会使用 {0}。新下载的模型会保存到那里,你现有的模型仍留在原处。",
|
||||
"s8411": "Strata 现在使用 {0}。旧的模型文件仍在 {1},Strata 不会删除它们。",
|
||||
"s8412": "更改模型文件夹是 Strata 应用的功能。",
|
||||
"s8413": "有模型正在运行。请先停止它,再更改文件夹。",
|
||||
"s8414": "请选择模型文件夹。",
|
||||
"s8415": "请选择如何处理你的模型。",
|
||||
"s8416": "此处无法使用该选项。",
|
||||
"s8417": "移动被中断了。请完成它,或保持原样。",
|
||||
"s8418": "Strata 正在把你的模型从 {0} 移到 {1} 时停止了。没有任何内容丢失。",
|
||||
"s8419": "完成移动",
|
||||
"s8420": "保持原样",
|
||||
"s8421": "完成后,所有模型都会放到 {1}。保持原样则模型留在 {0},并删除 {1} 中不完整的副本。",
|
||||
"s8422": "已删除不完整的副本。你的模型在 {0}。",
|
||||
"s8601": "显卡",
|
||||
"s8602": "主卡",
|
||||
"s8603": "辅助",
|
||||
"s8604": "你的选择。",
|
||||
"s8605": "推荐。",
|
||||
"s8606": "你更改了分工。Strata 将完全按此使用。",
|
||||
"s8607": "推荐:{0}",
|
||||
"s8608": "{0} 的分工",
|
||||
"s8609": "{0} GB 显存",
|
||||
"s8610": "使用电脑自身的内存(集成显卡)",
|
||||
"s8611": "承担主要工作",
|
||||
"s8612": "存放模型的一部分",
|
||||
"s8613": "不使用",
|
||||
"s8614": "正被 {0} 使用",
|
||||
"s8615": "关闭它",
|
||||
"s8616": "将 {0} 设为主卡",
|
||||
"s8617": "{0} 无法运行 Strata。",
|
||||
"s8618": "{0} 是集成显卡,作主卡会很慢。",
|
||||
"s8619": "仅使用 CPU 时性能会下降。",
|
||||
"s8620": "此引擎需要一张显卡才能启动。请开启一张。",
|
||||
"s8621": "此配置的内存不足:约 {0} GB 将从硬盘读取。",
|
||||
"s8622": "{0} 现在是辅助卡。",
|
||||
"s8623": "{0} 现在是主卡。",
|
||||
"s8624": "唯一开启的显卡必须是主卡。",
|
||||
"s8625": "未找到 Strata 可用的显卡。",
|
||||
"s8626": "Strata 将使用你的 {0}。",
|
||||
"s8627": "{0} 是主卡:{1}。",
|
||||
"s8628": "两张卡相同",
|
||||
"s8629": "它的显存更大",
|
||||
"s8630": "它更快",
|
||||
"s8631": "它是唯一的显卡",
|
||||
"s8632": "{0} 提供辅助。",
|
||||
"s8633": "{0} 提供辅助。",
|
||||
"s8634": "{0} 默认关闭:{1}",
|
||||
"s8635": "Strata 无法运行它。",
|
||||
"s8636": "集成显卡。",
|
||||
"s8637": "{0} GB 或更少。",
|
||||
"s8638": "负责每次回答最后步骤的显卡:写出下一个词,运行预测步骤并校验,同时保存你的对话。请选最快且显存充足的显卡。始终只有一张。",
|
||||
"s8639": "存放模型的一部分及其高速缓存,让更多内容放在显卡上而不是从硬盘读取。读取提示词约快 20%。",
|
||||
"s8640": "Strata 不会使用这张显卡。适合用于其他用途的显卡,例如游戏或显示器。",
|
||||
"s8641": "模型约需 {0} GB。这些显卡加上内存共约 {1} GB,可以容纳。",
|
||||
"s8642": "模型约需 {0} GB。这些显卡加上内存共约 {1} GB,因此约 {2} GB 将从硬盘读取。",
|
||||
"s8643": "只使用处理器和内存。",
|
||||
"s8644": "专家信息:引擎在显卡 {0} 上启动(主卡在最后)。",
|
||||
"s8645": "沿用推荐设置时不会保存:增减显卡后 Strata 会重新选择。",
|
||||
"s8646": "未使用:{0}(与此引擎驱动的显卡品牌不同)。"
|
||||
}
|
||||
@@ -26,3 +26,5 @@ import './styles/model-run.css';
|
||||
import './styles/onboarding.css';
|
||||
|
||||
import './styles/board.css';
|
||||
|
||||
import './styles/gpu-roles.css';
|
||||
@@ -9,6 +9,8 @@ import { api } from '../lib/client';
|
||||
import { RunningModels } from '../components/RunningModels';
|
||||
import { LocalModelImport } from '../components/LocalModelImport';
|
||||
import { gpuId } from '../lib/monitor-hardware';
|
||||
import { GpuRoles } from '../components/GpuRoles';
|
||||
import { normalize as normalizeRoles, order as roleOrder, recommend as recommendRoles, sameRoles, type RoleCard, type Roles } from '../lib/gpu-roles';
|
||||
import { RunImageModel } from '../components/RunImageModel';
|
||||
import { IMAGE_GENERATION_VISIBLE } from '../lib/features';
|
||||
import { FloatingMenu } from '../components/FloatingMenu';
|
||||
@@ -49,6 +51,8 @@ function LegacyModelRunScreen({ importFirst = false, initialVariant }: {
|
||||
const runtime = useStore(s => s.runtime);
|
||||
const [deviceChoice, setDeviceChoice] = useState('auto');
|
||||
const [customGpus, setCustomGpus] = useState<string[]>([]);
|
||||
// Two or more cards (Expert settings): a role for each card instead of the "Run on" list. null = the recommended roles.
|
||||
const [runRoles, setRunRoles] = useState<Roles | null>(null);
|
||||
const placementInitialized = useRef('');
|
||||
const imageModels = useStore(s => s.imageModels), imagePending = useStore(s => !!s.busy['image:select']);
|
||||
const imageChoice = imageModels?.selected ?? 'off';
|
||||
@@ -123,8 +127,11 @@ function LegacyModelRunScreen({ importFirst = false, initialVariant }: {
|
||||
const profileChanged = !!selectedProfile && !!config && (configIdentity(config) !== configIdentity(selectedProfile.config) || imageChoice !== (selectedProfile.imageModel ?? 'off'));
|
||||
const use = config?.headroom === 'full' ? 'full' : 'shared';
|
||||
const ctx = contextTokens(config?.beginner.contextLength ?? 'recommended');
|
||||
const roleCards: RoleCard[] = (hardware?.gpus ?? []).map((g, i) => ({ index: i, name: g.name, vram_gb: g.vramGb ?? 0, integrated: !!g.unified, vendor: g.vendor }));
|
||||
const rolesMode = expert && roleCards.length > 1;
|
||||
const recommendedRoles = recommendRoles(roleCards).roles, effectiveRoles: Roles = runRoles ? normalizeRoles(runRoles, roleCards) : recommendedRoles;
|
||||
const effectiveDevice = expert ? deviceChoice : 'auto';
|
||||
const gpuIds = hardware ? (effectiveDevice === 'auto' ? hardware.gpus.map(gpuId) : effectiveDevice === 'cpu' ? [] : effectiveDevice === 'custom' ? customGpus : [effectiveDevice]) : [];
|
||||
const gpuIds = hardware ? (rolesMode ? roleOrder(effectiveRoles, roleCards).map(i => gpuId(hardware.gpus[i])) : effectiveDevice === 'auto' ? hardware.gpus.map(gpuId) : effectiveDevice === 'cpu' ? [] : effectiveDevice === 'custom' ? customGpus : [effectiveDevice]) : [];
|
||||
const executionHardware = hardware ? { ...hardware, gpus: hardware.gpus.filter(g => gpuIds.includes(gpuId(g))), accelerator: { ...hardware.accelerator, backend: gpuIds.length ? hardware.accelerator.backend : 'cpu', unified: gpuIds.length ? hardware.accelerator.unified : false, usableVramGb: hardware.gpus.filter(g => gpuIds.includes(gpuId(g))).reduce((n, g) => n + g.vramGb, 0) } } : null;
|
||||
const cfg = execution({ ...config?.expert, ...(!expert ? { 'gpu.layers': gpuIds.length ? 'Automatic' : 'CPU only' } : !gpuIds.length ? { 'gpu.layers': 'CPU only' } : {}) }, ctx);
|
||||
const baseExperience = estimate(model, executionHardware, cfg, 'document', library?.mode === 'simulated' && !model?.local);
|
||||
@@ -213,7 +220,7 @@ function LegacyModelRunScreen({ importFirst = false, initialVariant }: {
|
||||
});
|
||||
}, 180);
|
||||
return () => { active = false; clearTimeout(timer); };
|
||||
}, [text, model?.id, deviceChoice, customGpus.join(','), runtime?.revision, connected, expert, executionMode]);
|
||||
}, [text, model?.id, deviceChoice, customGpus.join(','), JSON.stringify(runRoles), runtime?.revision, connected, expert, executionMode]);
|
||||
function loadPatch(doc: ConfigDocument) {
|
||||
const patch = configPatch(doc);
|
||||
if (!expert || !gpuIds.length)
|
||||
@@ -378,7 +385,7 @@ function LegacyModelRunScreen({ importFirst = false, initialVariant }: {
|
||||
else {
|
||||
if (!lifecycle.allowed)
|
||||
throw new Error(lifecycle.reason);
|
||||
if (expert && deviceChoice !== 'auto')
|
||||
if (expert && (rolesMode ? !!runRoles : deviceChoice !== 'auto'))
|
||||
throw new Error('GPU assignment requires a native engine adapter. Use Automatic with this host.');
|
||||
await saveEngine(patch, true, true);
|
||||
const ok = getState().library?.loaded === model.id ? await restartEngine() : await loadVariant(model.id);
|
||||
@@ -443,9 +450,11 @@ function LegacyModelRunScreen({ importFirst = false, initialVariant }: {
|
||||
{tr(expert ? <section className="run-card run-placement">
|
||||
<div className="run-card__head"><span><Icon name="gpu" size={19}/><h2>{tr("Model execution")}</h2></span></div>
|
||||
<label className="run-multiple-choice"><span><strong>{tr("Run multiple models")}</strong><small>{tr("Keep models loaded when you add another.")}</small></span><input type="checkbox" role="switch" aria-label={tr("Run multiple models")} checked={allowMultiple} disabled={pending || runtime?.capabilities.multipleModels === false} onChange={e => { setAllowMultiple(e.target.checked); setIssues([]); setStage('idle'); }}/></label>
|
||||
{rolesMode ? <GpuRoles cards={roleCards} roles={effectiveRoles} custom={!!runRoles && !sameRoles(effectiveRoles, recommendedRoles)} disabled={pending || runtime?.capabilities.deviceAssignment === false} onChange={next => { setRunRoles(sameRoles(next, recommendedRoles) ? null : next); setStage('idle'); setIssues([]); }} onBack={() => { setRunRoles(null); setStage('idle'); setIssues([]); }}/> : <>
|
||||
<span className="run-device-label">{tr("Run on")}</span>
|
||||
<Select ariaLabel={tr("Execution device")} value={deviceChoice} disabled={pending || runtime?.capabilities.deviceAssignment === false} options={[{ value: 'auto', label: hardware?.gpus.length ? hardware.gpus.length > 1 ? 'Automatic · all GPUs' : 'Automatic · GPU 1' : 'CPU only' }, { value: 'cpu', label: 'CPU only' }, ...(hardware?.gpus ?? []).map((g, i) => ({ value: gpuId(g), label: `GPU ${i + 1} · ${g.name}` })), ...((hardware?.gpus.length ?? 0) > 1 ? [{ value: 'custom', label: 'Choose GPUs…' }] : [])]} onChange={id => { setDeviceChoice(id); setStage('idle'); setIssues([]); }}/>
|
||||
{tr(deviceChoice === 'custom' ? <div className="run-gpu-options">{tr(hardware?.gpus.map((g, i) => <label key={gpuId(g)}><input type="checkbox" checked={customGpus.includes(gpuId(g))} onChange={e => { setCustomGpus(ids => e.target.checked ? [...ids, gpuId(g)] : ids.filter(id => id !== gpuId(g))); setStage('idle'); }}/><span>{tr("GPU ")}{tr(i + 1)} · {tr(g.name)}<small>{tr(g.unified ? 'Shared system memory' : `${sizeGb(g.vramGb)} VRAM`)}</small></span></label>))}</div> : null)}
|
||||
</>}
|
||||
<p className="xs muted">{tr(runtime?.capabilities.multipleModels === false ? 'This native host currently supports one model with automatic GPU placement.' : !allowMultiple ? 'One model at a time. Loading switches to the selected model.' : placementPlan?.reloads ? 'Loading applies these settings to this instance. Other models stay loaded.' : 'Loading adds a model. Existing models keep their settings and GPU assignments.')}</p>
|
||||
{tr(runtime?.instances.length ? <div className="run-memory-reservations"><span>{tr(placementPlan && !placementPlan.alreadyLoaded ? 'Shared RAM after load' : 'Shared RAM reserved')} <strong>{tr((placementPlan?.budget.ramGb ?? runtime.memory.ramGb).toFixed(1))} / {tr((placementPlan?.budget.budgetGb ?? runtime.memory.budgetGb).toFixed(1))}{tr(" GB")}</strong></span><small>{tr(runtime.memory.reserveGb.toFixed(0))}{tr(" GB RAM headroom \u00B7 estimates include all loaded models")}</small>{tr((placementPlan?.budget.devices ?? runtime.memory.devices).map(d => <span key={d.id}>{tr(d.name)}<strong>{tr(d.usedGb.toFixed(1))} / {tr(d.budgetGb.toFixed(1))}{tr(" GB")}</strong></span>))}</div> : null)}
|
||||
</section> : null)}
|
||||
|
||||
@@ -86,7 +86,7 @@ export function ServingScreen() {
|
||||
<section className="st-card serving-overview"><dl className="summary-list"><div><dt>{tr("API base URL")}</dt><dd className="row"><code style={{ overflowWrap: 'anywhere' }}>{serving.baseUrl}</code><IconBtn name="copy" title={tr("Copy API base URL")} onClick={() => void copyConnectionValue(serving.baseUrl, 'API base URL')}/></dd></div>{tr(!multipleModels ? <div><dt>{tr("Model ID")}</dt><dd className="row"><code>{loaded?.apiModelId ?? 'No model loaded'}</code><IconBtn name="copy" title={tr("Copy Model ID")} disabled={!loaded} onClick={() => {
|
||||
if (loaded)
|
||||
void copyConnectionValue(loaded.apiModelId, 'Model ID');
|
||||
}}/></dd></div> : null)}<div><dt>{tr("Status")}</dt><dd><span className="api-status" data-active={serving.running}><span className="api-status__dot"/>{tr(serving.running ? 'API listening' : 'API stopped')} · {tr(multipleModels ? `${runtime?.instances.length} models loaded` : loaded ? 'Model ready' : 'Model not ready')}</span></dd></div><div><dt>{tr("Local network")}</dt><dd className="row"><span className="request-status" data-state={serving.sharing?.active ? "shared" : "queued"}>{tr(serving.sharing?.active ? "Shared on local network" : "Off")}</span><Btn size="sm" onClick={() => { setError(''); setSharingOpen(true); }}>{tr(serving.sharing?.active ? "Manage / stop" : "Share")}</Btn></dd></div>{tr(serving.tunnel?.status === "connected" && serving.tunnel.url ? <div><dt>{tr("Cloudflare tunnel")}</dt><dd className="row"><code style={{ overflowWrap: "anywhere" }}>{serving.tunnel.url}</code><IconBtn name="copy" title={tr("Copy Cloudflare tunnel URL")} onClick={() => void navigator.clipboard.writeText(serving.tunnel!.url!)}/><IconBtn name="settings" title={tr("Manage Cloudflare tunnel")} onClick={() => setCloudflareOpen(true)}/></dd></div> : serving.tunnel?.status === "starting" ? <div><dt>{tr("Cloudflare tunnel")}</dt><dd role="status">{tr("Connecting\u2026")}</dd></div> : null)}<div className="serving-access-row"><dt>{tr("API key")}</dt><dd><div className="serving-access-status">{tr(serving.access.apiKeySet ? <span className="small muted">{tr("Required \u00B7 ")}{tr(serving.access.apiKeyHint)}</span> : null)}<div className="row"><Btn size="sm" onClick={() => { setError(''); generateKey(); setKeyOpen(true); }}>{tr(serving.access.apiKeySet ? 'Change key' : 'Set key')}</Btn>{tr(serving.access.apiKeySet ? <Btn size="sm" disabled={serving.access.otherDevices} onClick={() => { setError(''); setRemoveKeyOpen(true); }}>{tr("Remove key")}</Btn> : null)}</div></div>{tr(serving.access.otherDevices ? <span className="xs muted">{tr("Keep a key while the API is available to other devices.")}</span> : null)}</dd></div></dl></section>
|
||||
}}/></dd></div> : null)}<div><dt>{tr("Status")}</dt><dd><span className="api-status" data-active={serving.running}><span className="api-status__dot"/>{tr(serving.running ? 'API listening' : 'API stopped')} · {tr(multipleModels ? `${runtime?.instances.length} models loaded` : loaded ? 'Model ready' : 'Model not ready')}</span></dd></div><div><dt>{tr("Local network")}</dt><dd className="row"><span className="request-status" data-state={serving.sharing?.active ? "shared" : "queued"}>{tr(serving.sharing?.active ? "Shared on local network" : "Off")}</span><Btn size="sm" onClick={() => { setError(''); setSharingOpen(true); }}>{tr(serving.sharing?.active ? "Manage / stop" : "Share")}</Btn></dd></div>{tr(serving.tunnel?.status === "connected" && serving.tunnel.url ? <div><dt>{tr("Cloudflare tunnel")}</dt><dd className="row"><code style={{ overflowWrap: "anywhere" }}>{serving.tunnel.url}</code><IconBtn name="copy" title={tr("Copy Cloudflare tunnel URL")} onClick={() => void navigator.clipboard.writeText(serving.tunnel!.url!)}/><IconBtn name="settings" title={tr("Manage Cloudflare tunnel")} onClick={() => setCloudflareOpen(true)}/></dd></div> : serving.tunnel?.status === "starting" ? <div><dt>{tr("Cloudflare tunnel")}</dt><dd role="status">{tr("Connecting\u2026")}</dd></div> : null)}<div className="serving-access-row"><dt>{tr("API key")}</dt><dd><div className="serving-access-status">{tr(serving.access.apiKeySet ? <span className="small muted">{tr("Required \u00B7 ")}{tr(serving.access.apiKeyHint)}</span> : null)}<div className="row"><Btn size="sm" onClick={() => { setError(''); generateKey(); setKeyOpen(true); }}>{tr(serving.access.apiKeySet ? 'Change key' : 'Set key')}</Btn>{tr(serving.access.apiKeySet ? <Btn size="sm" onClick={() => { setError(''); setRemoveKeyOpen(true); }}>{tr("Remove key")}</Btn> : null)}</div></div>{tr(serving.access.otherDevices ? <span className="xs muted">{tr("Keep a key while the API is available to other devices.")}</span> : null)}</dd></div></dl></section>
|
||||
{tr(multipleModels ? <section className="st-card serving-models" aria-label={tr("Available API models")}><div className="section-toolbar"><h2 className="h4">{tr("Available models")}</h2><span className="xs muted">{tr("One API URL \u00B7 route by model ID")}</span></div>
|
||||
{tr(runtime?.instances.length ? <div className="serving-models__list">{tr(runtime.instances.map(i => <div key={i.id} className="serving-model"><div><strong>{tr(i.family)}</strong><small>{tr(i.name)} · {tr(i.memory.context / 1024)}{tr("K \u00B7 ")}{tr(deviceLabel(i.gpuIds, hardware))}</small></div><code>{i.apiModelId}</code><IconBtn name="copy" title={tr("Copy model ID {0}", [i.apiModelId])} onClick={() => void copyConnectionValue(i.apiModelId, 'Model ID')}/></div>))}</div> : <p className="small muted">{tr("Load a model in Models to make it available here.")}</p>)}
|
||||
<p className="xs muted">{tr("Set ")}<code>model</code>{tr(" in each request. Each model has its own queue.")}{tr(runtime?.mode === 'simulated' ? ' Design preview · responses are illustrative.' : '')}</p>
|
||||
@@ -97,7 +97,7 @@ export function ServingScreen() {
|
||||
{tr(error ? <ErrorNotice message={error}/> : null)}
|
||||
</div><Dialog open={guide} title={tr("Connect Strata to an app")} onClose={() => setGuide(false)} headerActions={<div className="protocol-switch"><Seg panelId="api-protocol-panel" value={protocol} ariaLabel={tr("API protocol")} options={[{ value: 'openai', label: 'Chat Completions' }, { value: 'responses', label: 'Responses' }, { value: 'anthropic', label: 'Anthropic' }]} onChange={setProtocol}/></div>} sheet><div className="stack">{tr(multipleModels ? <label className="input-field"><span>{tr("Model for this app")}</span><Select ariaLabel={tr("Connection guide model")} value={loaded?.apiModelId ?? ''} options={modelOptions} disabled={!modelOptions.length} onChange={setGuideModel}/></label> : null)}<ol><li>{tr("In your client, choose a compatible provider.")}</li><li>{tr("Set its base URL to ")}<code>{appBaseUrl(serving.baseUrl, protocol)}</code>.</li><li>{tr("Use model ID ")}<code>{loaded?.apiModelId ?? 'from the Models screen after loading'}</code>.</li><li>{tr(serving.access.apiKeySet ? 'Enter your existing API key in the client.' : 'This local API has no key configured.')}</li></ol>{tr(protocol === 'anthropic' ? <p className="small muted">{tr("Anthropic apps add /v1/messages themselves, so this address has no /v1 at the end. In Claude Code, set ANTHROPIC_BASE_URL to it.")}</p> : null)}{tr(protocol === 'responses' && (!native?.instances.some(i => i.status === 'ready') || native.capabilities.responses === false) ? <p className="small muted">{tr("Responses requires a running native model and a Strata version that supports this API. Update the native installation in Settings \u2192 About if needed.")}</p> : null)}<div id="api-protocol-panel" role="tabpanel" aria-labelledby={`api-protocol-panel-tab-${protocol}`} tabIndex={0}><div className="st-code"><div className="st-code__head"><span>{tr("curl")}</span><IconBtn name="copy" title={tr("Copy request template")} onClick={() => void navigator.clipboard.writeText(code)}/></div><pre>{code}</pre></div><p className="xs muted">{tr("Model list: ")}{tr(serving.baseUrl)}{tr(serving.endpoints.models)}{tr(". Network access depends on the host\u2019s listen address and your firewall.")}</p></div></div></Dialog>
|
||||
<Dialog open={!!selectedId} title={tr("Request details")} onClose={() => setSelectedId(null)} sheet>{tr(selected ? <><dl className="summary-list"><div><dt>{tr("ID")}</dt><dd className="mono">{tr(selected.id)}</dd></div><div><dt>{tr("State")}</dt><dd>{tr(selected.cancelRequested ? selected.cancellable ? 'Cancelling…' : 'Cancelled' : selected.state)}</dd></div><div><dt>{tr("Model")}</dt><dd>{tr(selected.model)}</dd></div><div><dt>{tr("Tokens in / out")}</dt><dd>{tr(tokens(selected.tokensIn))} / {tr(tokens(selected.tokensOut))}</dd></div><div><dt>{tr("Usage source")}</dt><dd>{tr(selected.usageSource === 'demo' ? 'Simulated' : selected.usageSource === 'reported' ? 'Reported by model' : 'Unavailable')}</dd></div><div><dt>{tr("Recorded duration")}</dt><dd>{tr(selected.durationMs ? duration(selected.durationMs) : 'Not yet available')}</dd></div></dl>{tr(selected.error ? <div ref={cancelResult} tabIndex={-1}><ErrorNotice message={selected.error} next={tr("Check the client connection and try the request again.")}/></div> : null)}<p className="small muted">{tr("Prompt content and historical queue position are not retained. This view updates while the request runs.")}</p>{tr(selected.cancellable !== false && ['queued', 'generating'].includes(selected.state) ? <Btn disabled={pending || selected.cancelRequested} onClick={() => { requestedCancel.current = selected.id; void operation(() => cancelRequest(selected.id)); }}>{tr(selected.cancelRequested ? 'Cancelling…' : 'Cancel request')}</Btn> : null)}{tr(error ? <ErrorNotice message={error}/> : null)}</> : <p>{tr("This request is no longer retained in the session log.")}</p>)}</Dialog>
|
||||
<Dialog open={sharingOpen} title={tr("Local sharing")} onClose={() => !pending && setSharingOpen(false)} actions={<><Btn disabled={pending} onClick={() => setSharingOpen(false)}>{tr("Close")}</Btn><Btn variant="primary" disabled={pending || !serving.access.apiKeySet} onClick={() => void operation(async () => { await setLocalSharing(!serving.sharing?.active); setSharingOpen(false); })}>{tr(pending ? 'Updating…' : serving.sharing?.active ? 'Stop sharing' : 'Start sharing')}</Btn></>}><div className="form-stack"><span className="dialog-feature-icon"><Icon name="link" size={28}/></span><p>{tr(serving.sharing?.active ? 'Your inference API is available to devices on this network.' : 'Let another device on your network use your loaded models.')}</p><p className="small muted">{tr("Only the inference API is shared. Clients need your API key. Use a trusted local network; this connection uses HTTP.")}</p>{tr(serving.sharing?.active ? <div className="row"><code className="key-url">{serving.sharing.url}</code><IconBtn name="copy" title={tr("Copy local network URL")} onClick={() => void navigator.clipboard.writeText(serving.sharing!.url!)}/></div> : null)}{tr(!serving.access.apiKeySet ? <Btn onClick={() => { generateKey(); setSharingOpen(false); setKeyOpen(true); }}>{tr("Create an API key first")}</Btn> : <p className="small muted"><Icon name="shield" size={16}/>{tr(" API key required")}</p>)}{tr(error ? <ErrorNotice message={error}/> : null)}</div></Dialog>
|
||||
<Dialog open={sharingOpen} title={tr("Local sharing")} onClose={() => !pending && setSharingOpen(false)} actions={<><Btn disabled={pending} onClick={() => setSharingOpen(false)}>{tr("Close")}</Btn><Btn variant="primary" disabled={pending} onClick={() => void operation(async () => { await setLocalSharing(!serving.sharing?.active); setSharingOpen(false); })}>{tr(pending ? 'Updating…' : serving.sharing?.active ? 'Stop sharing' : 'Start sharing')}</Btn></>}><div className="form-stack"><span className="dialog-feature-icon"><Icon name="link" size={28}/></span><p>{tr(serving.sharing?.active ? 'Your inference API is available to devices on this network.' : 'Let another device on your network use your loaded models.')}</p><p className="small muted">{tr("Only the inference API is shared. Clients need your API key when one is set. Use a trusted local network; this connection uses HTTP.")}</p>{tr(serving.sharing?.active ? <div className="row"><code className="key-url">{serving.sharing.url}</code><IconBtn name="copy" title={tr("Copy local network URL")} onClick={() => void navigator.clipboard.writeText(serving.sharing!.url!)}/></div> : null)}{tr(!serving.access.apiKeySet ? <><p className="small muted"><Icon name="warning" size={16}/>{tr(" Without an API key, any device on this network can use your model.")}</p><Btn onClick={() => { generateKey(); setSharingOpen(false); setKeyOpen(true); }}>{tr("Create an API key (recommended)")}</Btn></> : <p className="small muted"><Icon name="shield" size={16}/>{tr(" API key required")}</p>)}{tr(error ? <ErrorNotice message={error}/> : null)}</div></Dialog>
|
||||
<Dialog open={keyOpen} className="serving-key-dialog" title={tr(serving.access.apiKeySet ? 'Change API key' : 'Create an API key')} onClose={() => {
|
||||
if (!pending)
|
||||
closeKey();
|
||||
@@ -116,7 +116,7 @@ export function ServingScreen() {
|
||||
{tr(error ? <ErrorNotice message={error}/> : null)}
|
||||
</section></div>
|
||||
</Dialog>
|
||||
<Dialog open={removeKeyOpen} compact title={tr("Remove API key?")} onClose={() => !pending && setRemoveKeyOpen(false)} actions={<><Btn disabled={pending} onClick={() => setRemoveKeyOpen(false)}>{tr("Keep key")}</Btn><Btn variant="danger" disabled={pending} onClick={() => void operation(async () => { await setServingAccess({ apiKey: null }); setRemoveKeyOpen(false); })}>{tr(pending ? 'Removing…' : 'Remove key')}</Btn></>}><p>{tr("Apps on this computer will be able to send requests without a key. Existing clients can keep their current key field.")}</p>{tr(error ? <ErrorNotice message={error}/> : null)}</Dialog>
|
||||
<Dialog open={removeKeyOpen} compact title={tr("Remove API key?")} onClose={() => !pending && setRemoveKeyOpen(false)} actions={<><Btn disabled={pending} onClick={() => setRemoveKeyOpen(false)}>{tr("Keep key")}</Btn><Btn variant="danger" disabled={pending} onClick={() => void operation(async () => { await setServingAccess({ apiKey: null }); setRemoveKeyOpen(false); })}>{tr(pending ? 'Removing…' : 'Remove key')}</Btn></>}><p>{tr("Apps on this computer will be able to send requests without a key. Existing clients can keep their current key field.")}</p>{tr(serving.access.otherDevices ? <p className="small muted"><Icon name="warning" size={16}/>{tr(" Without an API key, anyone who can reach this address can use your model.")}</p> : null)}{tr(error ? <ErrorNotice message={error}/> : null)}</Dialog>
|
||||
<CloudflareDialog open={cloudflareOpen} onClose={() => setCloudflareOpen(false)} onCreateKey={() => { setReturnToTunnel(true); setCloudflareOpen(false); generateKey(); setKeyOpen(true); }}/>
|
||||
</Screen>;
|
||||
}
|
||||
@@ -746,3 +746,28 @@ html[data-motion='reduced'] .task-tree-row[data-leaving] { opacity: 0; }
|
||||
.native-cards__opt small { display:block; margin-top:4px; font-size:12px; line-height:1.55; color:var(--st-ink-muted); }
|
||||
.native-cards__opt small[data-warn="true"] { color:var(--st-ink-soft); }
|
||||
.native-cards__note { margin:2px 0 0; }
|
||||
|
||||
/* Settings > Storage > Models folder (0.2.2): free-space line, the three choices, notices, the result mark. */
|
||||
.models-free { display:grid; gap:6px; margin-top:10px; }
|
||||
.models-free__row { display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap; }
|
||||
.models-picked { display:grid; gap:4px; }
|
||||
.models-picked .storage-directory__path { margin:0; }
|
||||
.models-choices { display:grid; gap:10px; border:0; padding:0; margin:0; }
|
||||
.models-choice { display:grid; grid-template-columns:auto 1fr; gap:4px 12px; padding:14px 16px; border:1px solid var(--st-line); border-radius:var(--st-r-md); background:var(--st-surface); cursor:pointer; }
|
||||
.models-choice:has(input:checked) { border-color:var(--st-accent); background:var(--st-accent-tint); }
|
||||
.models-choice:has(input:disabled) { opacity:.55; cursor:not-allowed; background:var(--st-surface-2); }
|
||||
.models-choice input { margin:4px 0 0; accent-color:var(--st-accent); }
|
||||
.models-choice strong { font-weight:700; }
|
||||
.models-choice__need { grid-column:2; font-size:var(--st-fs-xs); color:var(--st-ink-muted); font-variant-numeric:tabular-nums; }
|
||||
.models-choice__tag { font-size:var(--st-fs-xs); font-weight:700; color:var(--st-accent-text); margin-left:8px; }
|
||||
.models-notice { display:flex; gap:10px; align-items:flex-start; padding:12px 14px; border-radius:12px; font-size:var(--st-fs-sm); }
|
||||
.models-notice--warn { background:var(--st-warn-tint); color:var(--st-warn-text); }
|
||||
.models-notice--info { background:var(--st-info-tint); color:var(--st-info-text); }
|
||||
.models-notice .st-icon { flex:none; }
|
||||
.models-details summary { cursor:pointer; color:var(--st-accent-text); font-weight:700; font-size:var(--st-fs-sm); }
|
||||
.models-details ul { margin:8px 0 0; padding-left:18px; color:var(--st-ink-soft); font-size:var(--st-fs-sm); }
|
||||
.models-ok { display:flex; align-items:center; gap:12px; }
|
||||
.models-ok__mark { display:grid; place-items:center; flex:none; width:44px; height:44px; border-radius:50%; background:var(--st-accent-tint); color:var(--st-accent-text); }
|
||||
.models-dialog .dialog-body { display:grid; gap:14px; }
|
||||
.models-path { font-family:var(--st-font-mono); font-size:12px; font-weight:400; background:var(--st-surface-2); padding:2px 6px; border-radius:6px; overflow-wrap:anywhere; }
|
||||
.models-dialog .storage-directory__path { margin:0; }
|
||||
@@ -0,0 +1,50 @@
|
||||
/* Main / Helper / Off for every graphics card (GpuRoles.tsx). Tokens are the app's (styles/kit/tokens.css). */
|
||||
.gpuroles { display:flex; flex-direction:column; gap:10px; margin-top:14px; }
|
||||
.gpuroles__head { display:flex; align-items:baseline; justify-content:space-between; gap:12px; flex-wrap:wrap; }
|
||||
.gpuroles__link { background:none; border:0; padding:0; color:var(--st-accent-text); font:600 var(--st-fs-sm)/1.4 var(--st-font); cursor:pointer; text-decoration:underline; text-underline-offset:2px; }
|
||||
.gpuroles__link:disabled { opacity:.5; cursor:default; }
|
||||
.gpuroles__reco { display:flex; gap:10px; align-items:flex-start; padding:12px 14px; border-radius:var(--st-r-md); background:var(--st-accent-tint); color:var(--st-ink); }
|
||||
.gpuroles__reco svg { flex:none; margin-top:2px; color:var(--st-accent-text); }
|
||||
.gpuroles__reco[data-custom="true"] { background:var(--st-surface-2); }
|
||||
.gpuroles__reco[data-custom="true"] svg { color:var(--st-ink-muted); }
|
||||
.gpuroles__reco p { margin:0; }
|
||||
.gpuroles__rows { display:grid; gap:10px; }
|
||||
.gpuroles__row { border:1px solid var(--st-line); border-radius:var(--st-r-md); padding:14px 16px; background:var(--st-surface); }
|
||||
.gpuroles__row[data-role="main"] { border-color:var(--st-accent); background:var(--st-accent-tint); }
|
||||
.gpuroles__row[data-role="off"] { background:var(--st-surface-2); }
|
||||
.gpuroles__top { display:flex; gap:14px; align-items:center; flex-wrap:wrap; }
|
||||
.gpuroles__ico { flex:none; width:38px; height:38px; border-radius:10px; background:var(--st-surface-2); display:grid; place-items:center; color:var(--st-icon); }
|
||||
.gpuroles__row[data-role="main"] .gpuroles__ico { background:var(--st-surface); }
|
||||
.gpuroles__name { flex:1 1 200px; min-width:0; }
|
||||
.gpuroles__name strong { display:block; overflow-wrap:anywhere; }
|
||||
.gpuroles__name small { display:block; color:var(--st-ink-muted); font-size:var(--st-fs-sm); }
|
||||
.gpuroles__tag { display:inline-block; margin-left:6px; padding:1px 8px; border-radius:var(--st-r-pill); font-size:11px; font-weight:700; vertical-align:1px; background:var(--st-accent-tint); color:var(--st-accent-text); border:1px solid var(--st-accent); }
|
||||
.gpuroles__row[data-role="main"] .gpuroles__tag { background:var(--st-surface); }
|
||||
.gpuroles__tag--neutral { background:var(--st-surface-2) !important; border-color:var(--st-line); color:var(--st-ink-muted); }
|
||||
.gpuroles__roles { display:inline-flex; padding:3px; border-radius:12px; background:var(--st-surface-2); border:1px solid var(--st-line); gap:2px; flex:none; }
|
||||
.gpuroles__row[data-role="main"] .gpuroles__roles { background:var(--st-surface); }
|
||||
.gpuroles__roles button { height:34px; min-width:76px; padding:0 14px; border-radius:9px; border:0; background:transparent; cursor:pointer; font:500 var(--st-fs-md)/1 var(--st-font); color:var(--st-ink-soft); }
|
||||
.gpuroles__roles button:hover:not([aria-checked="true"]):not(:disabled) { background:var(--st-line-soft); }
|
||||
.gpuroles__roles button[aria-checked="true"] { background:var(--st-accent); color:var(--st-accent-ink); font-weight:700; }
|
||||
.gpuroles__roles button[aria-checked="true"][data-r="helper"] { background:var(--st-info); color:#fff; }
|
||||
.gpuroles__roles button[aria-checked="true"][data-r="off"] { background:var(--st-ink-muted); color:var(--st-surface); }
|
||||
.gpuroles__roles button:disabled { opacity:.4; cursor:not-allowed; }
|
||||
.gpuroles__roles button:focus-visible, .gpuroles__link:focus-visible, .gpuroles__details summary:focus-visible { outline:2px solid var(--st-focus); outline-offset:2px; }
|
||||
.gpuroles__note { margin-top:10px; display:flex; gap:10px; align-items:flex-start; padding:10px 12px; border-radius:10px; font-size:var(--st-fs-sm); line-height:1.5; }
|
||||
.gpuroles__note svg { flex:none; margin-top:2px; }
|
||||
.gpuroles__note p { margin:0; flex:1; }
|
||||
.gpuroles__note--warn { background:var(--st-warn-tint); color:var(--st-warn-text); }
|
||||
.gpuroles__note--info { background:var(--st-info-tint); color:var(--st-info-text); }
|
||||
.gpuroles__note .st-btn { flex:none; align-self:center; }
|
||||
.gpuroles__toast:empty { display:none; }
|
||||
.gpuroles__toast { margin-top:2px; }
|
||||
.gpuroles__details { margin-top:4px; border-top:1px solid var(--st-line-soft); padding-top:12px; }
|
||||
.gpuroles__details summary { cursor:pointer; color:var(--st-accent-text); font-weight:600; font-size:var(--st-fs-sm); }
|
||||
.gpuroles__body { margin-top:10px; font-size:var(--st-fs-sm); color:var(--st-ink-soft); display:grid; gap:8px; }
|
||||
.gpuroles__body dl { margin:0; display:grid; grid-template-columns:auto 1fr; gap:6px 14px; }
|
||||
.gpuroles__body dt { font-weight:700; }
|
||||
.gpuroles__body dd { margin:0; }
|
||||
.gpuroles__fit { display:flex; gap:8px; align-items:center; }
|
||||
.gpuroles__dot { width:9px; height:9px; border-radius:50%; background:var(--st-accent); flex:none; }
|
||||
.gpuroles__fit[data-state="bad"] .gpuroles__dot { background:var(--st-danger); }
|
||||
@media (max-width:560px) { .gpuroles__roles { width:100%; } .gpuroles__roles button { flex:1; min-width:0; } }
|
||||
@@ -0,0 +1,123 @@
|
||||
/** "Answer speed" is one number everywhere: every token the model produced over its decode time, never over a time that holds the prompt being read. */
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import test, { mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { build } from 'esbuild';
|
||||
import { sumEngineRequests, turnEngineSpeed } from '../server/agent/engine-speed.mjs';
|
||||
import { TurnProjector } from '../server/agent/events.mjs';
|
||||
import { answerSpeed } from '../server/model-providers.mjs';
|
||||
import * as monitor from '../server/monitor.mjs';
|
||||
import * as serving from '../server/serving.mjs';
|
||||
|
||||
async function moduleAt(path) { const result = await build({ entryPoints: [fileURLToPath(new URL(path, import.meta.url))], bundle: true, write: false, platform: 'node', format: 'esm' }); return import(`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`); }
|
||||
const { chatMetrics } = await moduleAt('../src/lib/chat-metrics.ts');
|
||||
|
||||
// Two real requests from the owner PC (Gyro-S with MTP drafting, RTX 5070), as the engine's history lists them.
|
||||
const REAL = [
|
||||
{ time: 1000.0, prompt_tokens: 62, reused: 0, prompt_ms: 941, decode_ms: 711, engine_generated: 34, output_tokens: 34, drafts_offered: 34, drafts_accepted: 18 },
|
||||
{ time: 1010.0, prompt_tokens: 308, reused: 0, prompt_ms: 1212, decode_ms: 1108, engine_generated: 73, output_tokens: 73, drafts_offered: 68, drafts_accepted: 45 },
|
||||
];
|
||||
|
||||
test('the engine speed of a turn: produced tokens over decode time, prompt reading kept apart', () => {
|
||||
const one = sumEngineRequests(REAL, { since: 1009_000 });
|
||||
assert.equal(one.tokens, 73);
|
||||
assert.equal(one.tps, 65.9);
|
||||
assert.equal(one.promptTps, 254.1);
|
||||
const both = sumEngineRequests(REAL, { since: 999_000 });
|
||||
assert.equal(both.tokens, 107);
|
||||
assert.equal(both.tps, Number((107 / 1.819).toFixed(1))); // 58.8: the sum over the sum, not the mean of the two
|
||||
// dividing by the whole time (reading + decoding) is the number users saw as "lower than before"
|
||||
assert.ok(73 / ((1212 + 1108) / 1000) < 32);
|
||||
});
|
||||
|
||||
test('drafted tokens are not output: the speed uses the engine count, whatever was offered', () => {
|
||||
const r = sumEngineRequests([{ ...REAL[1], drafts_offered: 9999 }], { since: 1009_000 });
|
||||
assert.equal(r.tps, 65.9);
|
||||
});
|
||||
|
||||
test("another chat's requests on the same engine are not this turn's", () => {
|
||||
const stranger = { time: 1011.0, prompt_tokens: 10, reused: 0, prompt_ms: 100, decode_ms: 1000, engine_generated: 10 };
|
||||
const r = sumEngineRequests([REAL[1], stranger], { since: 1009_000, attempts: 1 });
|
||||
assert.equal(r.tokens, 10); // the newest one only: the turn made one model request
|
||||
assert.equal(sumEngineRequests([], { since: 0 }), null);
|
||||
assert.equal(sumEngineRequests([{ time: 5, engine_generated: 5, decode_ms: 0 }], { since: 0 }), null);
|
||||
});
|
||||
|
||||
test('reading the engine: its own /metrics with the key, null when it fails', async () => {
|
||||
let seen;
|
||||
const fetcher = async (url, init) => { seen = { url, auth: init.headers.authorization }; return { ok: true, json: async () => ({ requests: REAL }) }; };
|
||||
const r = await turnEngineSpeed({ baseUrl: 'http://127.0.0.1:8080/v1', key: 'k', since: 1009_000, fetcher });
|
||||
assert.equal(r.tps, 65.9);
|
||||
assert.equal(seen.url, 'http://127.0.0.1:8080/metrics?requests=all');
|
||||
assert.equal(seen.auth, 'Bearer k');
|
||||
assert.equal(await turnEngineSpeed({ baseUrl: 'http://127.0.0.1:8080/v1', since: 0, fetcher: async () => { throw new Error('down'); } }), null);
|
||||
assert.equal(await turnEngineSpeed({ baseUrl: 'http://127.0.0.1:8080/v1', since: 0, fetcher: async () => ({ ok: false }) }), null);
|
||||
});
|
||||
|
||||
const chunk = (attemptId, c) => ({ type: 'assistant-stream', frame: { type: 'chunk', attemptId, chunk: c } });
|
||||
|
||||
test('the finished agent turn shows the engine speed and the prompt reading speed', () => {
|
||||
const p = new TurnProjector({ startedAt: Date.now() });
|
||||
p.consume(chunk('a1', { type: 'text-delta', text: 'Done.' }));
|
||||
p.consume(chunk('a1', { type: 'usage', usage: { inputTokens: 308, outputTokens: 73 } }));
|
||||
p.ended = { kind: 'completed' };
|
||||
const done = p.finish({ model: 'm', contextMax: 32768, engine: { tps: 65.9, promptTps: 254.1 } }).find(f => f.type === 'done');
|
||||
assert.equal(done.tps, 65.9);
|
||||
assert.equal(done.promptTps, 254.1);
|
||||
assert.equal(done.tokensOut, 73);
|
||||
});
|
||||
|
||||
test('a short reply still gets a speed (it used to come out empty under 0.3 s of streaming)', () => {
|
||||
mock.timers.enable({ apis: ['Date'], now: 3_000_000 });
|
||||
try {
|
||||
const p = new TurnProjector({ startedAt: Date.now() });
|
||||
mock.timers.tick(2000); // reading the prompt
|
||||
p.consume(chunk('a1', { type: 'text-delta', text: 'Hi!' }));
|
||||
p.consume(chunk('a1', { type: 'usage', usage: { inputTokens: 40, outputTokens: 3 } }));
|
||||
mock.timers.tick(100);
|
||||
p.ended = { kind: 'completed' };
|
||||
const done = p.finish({}).find(f => f.type === 'done');
|
||||
assert.ok(done.tps > 0 && done.tps <= 60, `${done.tps}`);
|
||||
} finally { mock.timers.reset(); }
|
||||
});
|
||||
|
||||
test('a model API reply is timed from its first token, not from the request', () => {
|
||||
assert.equal(answerSpeed(100, 12_000, 2_000), 10); // 100 tokens in the 10 s after the first token
|
||||
assert.ok(100 / 12 < answerSpeed(100, 12_000, 2_000)); // the whole-wait number is the lower one
|
||||
assert.equal(answerSpeed(50, 1000, null), 50); // no first-token time: the whole time is all there is
|
||||
assert.equal(answerSpeed(50, 1000, 900), 50); // under a quarter second left: not used
|
||||
assert.equal(answerSpeed(null, 1000, 0), null);
|
||||
assert.equal(answerSpeed(0, 1000, 0), null);
|
||||
});
|
||||
|
||||
test('the monitor and the request list average answer speeds, not tokens over the whole request', () => {
|
||||
monitor.record({ tokensIn: 308, tokensOut: 73, durationMs: 2320, tps: 65.9 });
|
||||
monitor.record({ tokensIn: 62, tokensOut: 34, durationMs: 1652, tps: 47.8 });
|
||||
assert.equal(monitor.snapshot().totals.avgTps, 56.9);
|
||||
monitor.record({ tokensIn: 1, tokensOut: 5, durationMs: 100 }); // no decode figure: left out, not divided by the wall time
|
||||
assert.equal(monitor.snapshot().totals.avgTps, 56.9);
|
||||
const a = serving.open({ client: 't', endpoint: '/x', model: 'm' });
|
||||
serving.mark(a.id, { tps: 65.9 });
|
||||
serving.finish(a.id, { tokensOut: 73, durationMs: 2320 });
|
||||
assert.equal(serving.rateSample(), 65.9);
|
||||
});
|
||||
|
||||
const reply = (o) => ({ role: 'assistant', at: new Date().toISOString(), content: 'x', ...o });
|
||||
const chat = (messages) => ({ id: 's', messages, tokensIn: 0, tokensOut: 0, statsAvailable: true });
|
||||
|
||||
test('the footer shows the last reply own speed, and the prompt reading speed next to it', () => {
|
||||
const s = chat([reply({ tps: 61, inputTokens: 10, tokens: 5 }), reply({ tps: 66, promptTps: 254, inputTokens: 20, tokens: 7 })]);
|
||||
const m = chatMetrics(s, null, 32768);
|
||||
assert.equal(m.speed, 66);
|
||||
assert.equal(m.promptSpeed, 254);
|
||||
// an older reply's number is never shown under "last reply"
|
||||
const m2 = chatMetrics(chat([reply({ tps: 61, inputTokens: 10, tokens: 5 }), reply({ inputTokens: 20, tokens: 7 })]), null, 32768);
|
||||
assert.equal(m2.speed, null);
|
||||
});
|
||||
|
||||
test('while it thinks the footer reads the engine like the monitor does', () => {
|
||||
const s = chat([reply({ streaming: true, tps: 12 })]);
|
||||
const monitorState = { phase: 'thinking', outputTps: 64.2, context: { sessionId: 's', usedTokens: 100, maxTokens: 32768 } };
|
||||
assert.equal(chatMetrics(s, monitorState, 32768).speed, 64.2);
|
||||
assert.equal(chatMetrics(s, { ...monitorState, outputTps: 0 }, 32768).speed, 12); // no reading yet: the stream's own estimate
|
||||
});
|
||||
@@ -55,7 +55,7 @@ test('with an older engine installed the app refuses to download or load Gyro, w
|
||||
test('the screens use the existing "needs a newer engine" notice for a size, and the buttons are held back', async () => {
|
||||
const models = await readFile(new URL('../src/components/NativeModels.tsx', import.meta.url), 'utf8');
|
||||
assert.match(models, /selectedFamily && \(!selectedSize \|\| selectedSize\.needsNewerEngine\) \? <><h2 className="h3">\{tr\("This model needs a newer engine\."\)\}/);
|
||||
assert.match(models, /!!selectedSize\.needsNewerEngine \|\| expert && deviceChoice === 'split'/);
|
||||
assert.match(models, /!!selectedSize\.needsNewerEngine\}/);
|
||||
const guided = await readFile(new URL('../src/components/GuidedSetup.tsx', import.meta.url), 'utf8');
|
||||
assert.equal((guided.match(/needsNewerEngine/g) ?? []).length, 3);
|
||||
});
|
||||
@@ -7,6 +7,7 @@ const root=resolve(import.meta.dirname,'../../dist'),upstream=process.env.STRATA
|
||||
const native=process.env.STRATA_UI_PREVIEW==='1' ? JSON.parse(await readFile(new URL('./ui-preview-native.json',import.meta.url),'utf8')) : await (await fetch(upstream+'/api/native')).json();
|
||||
native.hardware={cpu:{cores:16},ram_gb:128,gpus:[{index:0,name:'NVIDIA RTX 4090',vram_gb:24,usable:true}],disk_free_gb:250};
|
||||
if(process.env.STRATA_ONBOARDING_SCENARIO==='multi-gpu')native.hardware.gpus.push({index:1,name:'NVIDIA RTX 3090',vram_gb:24,usable:true});
|
||||
if(process.env.STRATA_ONBOARDING_GPUS)native.hardware.gpus=JSON.parse(process.env.STRATA_ONBOARDING_GPUS);
|
||||
native.catalog.this_pc=native.hardware;
|
||||
native.catalog.recommendation={family:'qwen',model:'Q2_0',context:32768};
|
||||
native.catalog.summary='Supported hardware fixture';
|
||||
@@ -60,6 +61,7 @@ if(['make-it-yours','ready'].includes(process.env.STRATA_ONBOARDING_START)){
|
||||
}
|
||||
}
|
||||
}
|
||||
if(process.env.STRATA_ONBOARDING_GPUS){native.instances=[];native.defaultId=null;}/* GPU roles previews: the model is prepared, not loaded, so the roles shown are the recommendation */
|
||||
if(process.env.STRATA_PREVIEW_STATE_FILE){const saved=JSON.parse(await readFile(process.env.STRATA_PREVIEW_STATE_FILE,'utf8'));Object.assign(native,saved.native);setup=saved.setup;const running=native.instances.find(i=>i.id===native.defaultId),entry=native.installed.models.find(m=>m.model===running?.model),family=native.catalog.families.find(f=>f.sizes.some(s=>s.id===entry?.model)),size=family?.sizes.find(s=>s.id===entry?.model);if(family&&size){selected={family:family.family,model:size.model,context:entry.context??32768,vision:entry.images==='off'?'no':'yes',kv:entry.kv??'int8',low_ram:entry.lowRamMode??'auto'};loadVision=entry.visionEnabled===false?'no':selected.vision;}}
|
||||
const server=http.createServer(async(req,res)=>{try{
|
||||
const path=new URL(req.url,'http://fixture').pathname;let payload='';if(req.method!=='GET')for await(const chunk of req)payload+=chunk;const body=payload?JSON.parse(payload):{};
|
||||
|
||||
@@ -77,6 +77,23 @@ test('a typed path must be an absolute existing directory', async () => {
|
||||
} finally { await rm(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('the model page takes a folder or one .gguf file, with or without quotes around the path', async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'strata-gguf-typed-'));
|
||||
try {
|
||||
await writeFile(join(dir, 'model-00001-of-00002.gguf'), 'x');
|
||||
await writeFile(join(dir, 'notes.txt'), 'x');
|
||||
const only = { isAbsolute, fileType: /\.gguf$/i };
|
||||
assert.equal(await typedFolder(dir, only), dir);
|
||||
assert.equal(await typedFolder(join(dir, 'model-00001-of-00002.gguf'), only), join(dir, 'model-00001-of-00002.gguf'));
|
||||
assert.equal(await typedFolder(`"${join(dir, 'model-00001-of-00002.gguf')}"`, only), join(dir, 'model-00001-of-00002.gguf'));
|
||||
assert.equal(await typedFolder(` "${dir}" `, only), dir);
|
||||
await assert.rejects(typedFolder(join(dir, 'notes.txt'), only), /folder, or a \.gguf file/);
|
||||
await assert.rejects(typedFolder(join(dir, 'missing.gguf'), only), /does not exist/);
|
||||
// the other pickers still refuse a file
|
||||
await assert.rejects(typedFolder(join(dir, 'model-00001-of-00002.gguf'), { isAbsolute }), /not a file/);
|
||||
} finally { await rm(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
async function startHost() {
|
||||
const home = await mkdtemp(join(tmpdir(), 'strata-folder-home-'));
|
||||
const net = await import('node:net');
|
||||
|
||||
@@ -41,9 +41,9 @@ test('domain tunnel validates hostnames and keeps the token out of arguments and
|
||||
await f.connector.stop();
|
||||
});
|
||||
test('missing prerequisites and connector failure leave no inference listener running', async () => {
|
||||
const noKey = fixture({ hasKey: () => false });
|
||||
await assert.rejects(noKey.connector.start({ mode: 'quick' }), /API key/);
|
||||
assert.equal(noKey.launched(), undefined);
|
||||
const noKey = fixture({ hasKey: () => false }); // without a key the tunnel starts (owner decision, 0.2.2) and the state carries a warning
|
||||
await noKey.connector.start({ mode: 'quick' });
|
||||
assert.notEqual(noKey.launched(), undefined);
|
||||
const missing = fixture({ locate: async () => null });
|
||||
await assert.rejects(missing.connector.start({ mode: 'quick' }), /connector could not be prepared automatically/i);
|
||||
assert.equal(missing.connector.state().status, 'error');
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/** GPU roles (Main / Helper / Off): src/lib/gpu-roles.ts and server/gpu-roles.mjs run the vectors strata_control/gpu_roles.py runs (tests/data/gpu_roles_vectors.json). */
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { transform } from 'esbuild';
|
||||
|
||||
const { code } = await transform(await readFile(new URL('../src/lib/gpu-roles.ts', import.meta.url), 'utf8'), { loader: 'ts', format: 'esm' });
|
||||
const R = await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`);
|
||||
const S = await import('../server/gpu-roles.mjs');
|
||||
const V = JSON.parse(await readFile(new URL('../../../tests/data/gpu_roles_vectors.json', import.meta.url), 'utf8'));
|
||||
const roles = d => d === null || d === undefined ? d : Object.fromEntries(Object.entries(d).map(([k, v]) => [Number(k), v]));
|
||||
|
||||
test('recommend: the best card is Main, others Helper, 8 GB or less / built-in / unusable start Off', () => {
|
||||
for (const c of V.recommend) {
|
||||
const r = R.recommend(V.hw[c.hw]);
|
||||
assert.deepEqual(r.roles, roles(c.roles), c.hw);
|
||||
assert.equal(r.main, c.main, c.hw);
|
||||
assert.equal(r.why, c.why, c.hw);
|
||||
}
|
||||
});
|
||||
|
||||
test('setRole: one click, exactly one Main among the cards that are on', () => {
|
||||
for (const c of V.set_role) {
|
||||
const out = R.setRole(roles(c.roles), V.hw[c.hw], c.card, c.role);
|
||||
assert.deepEqual(out.roles, roles(c.after), JSON.stringify(c));
|
||||
assert.equal(out.note, c.note, JSON.stringify(c));
|
||||
}
|
||||
});
|
||||
|
||||
test('order: Helpers first, Main LAST, Off left out; one card is a number in the mirror', () => {
|
||||
for (const c of V.order) {
|
||||
assert.deepEqual(R.order(roles(c.roles), V.hw[c.hw]), c.gpu, JSON.stringify(c));
|
||||
assert.deepEqual(S.order(roles(c.roles), V.hw[c.hw]), c.gpu, 'server copy: ' + JSON.stringify(c));
|
||||
}
|
||||
const m = R.mirror({ 0: 'off', 1: 'main' }, V.hw.two);
|
||||
assert.equal(m.gpu, 1);
|
||||
assert.equal(m.gpu_order, 'as_given');
|
||||
assert.deepEqual(R.mirror({ 0: 'helper', 1: 'main' }, V.hw.two).gpu, [0, 1]);
|
||||
const none = R.mirror({ 0: 'off', 1: 'off' }, V.hw.two);
|
||||
assert.deepEqual([none.gpu, none.gpu_none], [[], true]);
|
||||
assert.deepEqual(S.mirror({ 0: 'helper', 1: 'main' }, V.hw.two), R.mirror({ 0: 'helper', 1: 'main' }, V.hw.two));
|
||||
assert.deepEqual(S.mirror({ 0: 'off', 1: 'off' }, V.hw.two), none);
|
||||
});
|
||||
|
||||
test('warnings: only built-in graphics as Main, a card the engine cannot run, too little memory, CPU only', () => {
|
||||
for (const c of V.warnings) {
|
||||
const w = R.warnings(roles(c.roles), V.hw[c.hw], { spillGb: c.spill_gb ?? 0 });
|
||||
assert.deepEqual(w.map(x => x.code), c.codes, JSON.stringify(c));
|
||||
if (c.fix) assert.deepEqual(w[0].fix, c.fix);
|
||||
for (const x of w) assert.ok(x.text.length < 90, x.text);
|
||||
}
|
||||
// a slower card as Main is understood by people: no warning
|
||||
assert.deepEqual(R.warnings({ 0: 'main', 1: 'helper' }, V.hw.two), []);
|
||||
assert.equal(R.warnings({ 0: 'off', 1: 'off' }, V.hw.two, { engineRunsCpuOnly: true })[0].text, 'Performance is degraded on CPU only.');
|
||||
assert.equal(R.warnings({ 0: 'off', 1: 'off' }, V.hw.two)[0].text, R.NO_CARD_LINE);
|
||||
});
|
||||
|
||||
test('legacy gpu settings show the roles the engine really uses', () => {
|
||||
for (const c of V.legacy) assert.deepEqual(R.fromLegacy(c.gpu, V.hw[c.hw], c.gpu_order), roles(c.roles), JSON.stringify(c));
|
||||
});
|
||||
|
||||
test('saved roles: parse; the server copy agrees on which are usable', () => {
|
||||
assert.deepEqual(R.parseSaved({ 0: 'helper', 1: 'main' }), { 0: 'helper', 1: 'main' });
|
||||
for (const bad of [{ 0: 'boss' }, { 0: 'main', 1: 'main' }, 'x', {}, null]) { assert.equal(R.parseSaved(bad), null); assert.equal(S.parseSaved(bad), null); }
|
||||
});
|
||||
|
||||
test('words: the sentence and the one-line summary', () => {
|
||||
assert.equal(R.sentence(V.hw.one), 'Strata will use your RTX 4070 Ti.');
|
||||
assert.match(R.sentence(V.hw.three), /RTX 4070 Ti is Main: it is faster\./);
|
||||
assert.match(R.sentence(V.hw.three), /RTX 2060 starts Off: 8 GB or less\./);
|
||||
assert.equal(R.describe({ 0: 'helper', 1: 'main' }, V.hw.two), 'Main: GPU 2 RTX 4070 Ti. Helper: GPU 1 RTX 3060.');
|
||||
assert.equal(R.noteText('old_main_helper:1', V.hw.two), 'RTX 4070 Ti is now a Helper.');
|
||||
assert.equal(R.noteText('only_main', V.hw.two), 'The only card that is on has to be Main.');
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import net from 'node:net';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdtempSync, realpathSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { findFreePort, listenProblem } from '../server/ports.mjs';
|
||||
|
||||
const cwd = fileURLToPath(new URL('..', import.meta.url));
|
||||
const hold = port => new Promise(resolve => { const s = net.createServer(); s.listen(port, '127.0.0.1', () => resolve(s)); });
|
||||
|
||||
test('a taken preferred port gives the next free one, never the taken one', async () => {
|
||||
const first = await hold(0); const taken = first.address().port;
|
||||
try {
|
||||
const got = await findFreePort(taken, '127.0.0.1');
|
||||
assert.notEqual(got, taken);
|
||||
assert.ok(got > 0);
|
||||
assert.notEqual(await findFreePort(got, '127.0.0.1', { skip: [got] }), got);
|
||||
} finally { first.close(); }
|
||||
});
|
||||
|
||||
test('the plain sentences name the cause and the one next step', () => {
|
||||
const use = listenProblem({ code: 'EADDRINUSE' }, 8791);
|
||||
assert.match(use, /Port 8791 is already in use/); assert.match(use, /STRATA_PORT/);
|
||||
const denied = listenProblem({ code: 'EACCES' }, 8792);
|
||||
assert.match(denied, /Hyper-V, WSL and Docker/); assert.match(denied, /excludedportrange/); assert.match(denied, /STRATA_PORT/);
|
||||
assert.doesNotMatch(denied, /EACCES|permission denied/i);
|
||||
});
|
||||
|
||||
test('the host on a taken port prints the plain sentence and exits, with no stack trace', async t => {
|
||||
const home = realpathSync(mkdtempSync(join(tmpdir(), 'strata-port-'))); t.after(() => rmSync(home, { recursive: true, force: true }));
|
||||
const blocker = await hold(0); t.after(() => blocker.close());
|
||||
const port = blocker.address().port;
|
||||
const child = spawn(process.execPath, ['server/index.mjs'], { cwd, env: { ...process.env, STRATA_HOME: home, STRATA_PORT: String(port), STRATA_MODELS_DIR: join(home, 'm') } });
|
||||
let err = ''; child.stderr.on('data', d => { err += d; });
|
||||
const code = await new Promise(resolve => { const timer = setTimeout(() => { child.kill(); resolve('timeout'); }, 30000); child.once('exit', c => { clearTimeout(timer); resolve(c); }); });
|
||||
assert.equal(code, 1, err);
|
||||
assert.match(err, new RegExp(`Port ${port} is already in use`));
|
||||
assert.doesNotMatch(err, /at Server\.setupListenHandle/);
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { createLocalShare, NO_KEY_WARNING } from '../server/local-share.mjs';
|
||||
import { createCloudflare, NO_KEY_TUNNEL_WARNING } from '../server/cloudflare.mjs';
|
||||
|
||||
test('sharing on the network works without an API key and says so; with a key there is no warning', async () => {
|
||||
let key = false;
|
||||
const share = createLocalShare({ handle: async (req, res) => res.end('ok'), hasKey: () => key, host: '127.0.0.1', port: 0 });
|
||||
try {
|
||||
const started = await share.start();
|
||||
assert.equal(started.active, true);
|
||||
assert.equal(started.warning, NO_KEY_WARNING);
|
||||
key = true;
|
||||
assert.equal(share.state().warning, null);
|
||||
} finally { await share.stop(); }
|
||||
});
|
||||
|
||||
test('a Cloudflare tunnel connects without an API key and carries the stronger warning', async () => {
|
||||
let key = false;
|
||||
const origin = { start: async () => {}, stop: async () => {} };
|
||||
const launch = () => {
|
||||
const child = new EventEmitter();
|
||||
child.stdout = new EventEmitter(); child.stderr = new EventEmitter();
|
||||
child.kill = () => { setImmediate(() => child.emit('exit')); };
|
||||
setImmediate(() => child.stderr.emit('data', 'https://demo-abc.trycloudflare.com\nRegistered tunnel connection\n'));
|
||||
return child;
|
||||
};
|
||||
const tunnel = createCloudflare({ origin, originUrl: 'http://127.0.0.1:1', hasKey: () => key, launch, locate: async () => '/bin/cloudflared', timeoutMs: 5000 });
|
||||
try {
|
||||
const first = await tunnel.start({ mode: 'quick' });
|
||||
assert.ok(['starting', 'connected'].includes(first.status));
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
const now = tunnel.state();
|
||||
assert.equal(now.status, 'connected');
|
||||
assert.equal(now.warning, NO_KEY_TUNNEL_WARNING);
|
||||
assert.match(NO_KEY_TUNNEL_WARNING, /internet/);
|
||||
key = true;
|
||||
assert.equal(tunnel.state().warning, null);
|
||||
} finally { await tunnel.stop(); }
|
||||
});
|
||||
@@ -32,7 +32,7 @@ test('directory preferences survive restart, scope demo installs, and reject bad
|
||||
import assert from 'node:assert/strict';import {join} from 'node:path';import {existsSync} from 'node:fs';
|
||||
const store=await import('./server/store.mjs');const models=await import('./server/models.mjs');
|
||||
const original=store.MODELS_DIR,id='qwen38-q4_k_m';store.update({installed:{[id]:{simulated:true}},setup:{complete:true,step:4},serving:{apiKey:'fixture-only-key'}});
|
||||
for(const path of ['relative/path',join(store.HOME,'missing'),store.DATA_DIR,store.HOME,'/'])assert.throws(()=>store.validateModelsDirectory(path));
|
||||
for(const path of ['relative/path',join(store.HOME,'missing'),store.DATA_DIR,store.HOME])assert.throws(()=>store.validateModelsDirectory(path));
|
||||
assert.equal(store.MODELS_DIR,original);assert.equal(existsSync(join(store.HOME,'missing')),false);
|
||||
store.changeModelsDirectory(${JSON.stringify(next)});models.init({send(){}});
|
||||
assert.equal(models.isOnDisk(id),false);assert.equal(store.get().setup.complete,true);assert.equal(store.get().serving.apiKey,'fixture-only-key');
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync, spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, parse } from 'node:path';
|
||||
import { alignModels, dataFolderFromPick, isDriveRoot, libraryFolderFromPick, makeFolder, rememberDataFolder, syncDataFolder, readDataFolder, terminalSettingsFile } from '../server/model-folder.mjs';
|
||||
import { nativeFixture } from './helpers/native-fixture.mjs';
|
||||
|
||||
const scratch = t => { const dir = realpathSync(mkdtempSync(join(tmpdir(), 'strata-folder-'))); t.after(() => rmSync(dir, { recursive: true, force: true })); return dir; };
|
||||
|
||||
test('a whole drive is not filled with models: it becomes Strata (data) and Strata/Models (library)', () => {
|
||||
const root = parse(tmpdir()).root;
|
||||
assert.equal(isDriveRoot(root), true);
|
||||
assert.equal(isDriveRoot(tmpdir()), false);
|
||||
assert.deepEqual(dataFolderFromPick(root), { path: join(root, 'Strata'), note: `${root} is a whole drive, so the models go in ${join(root, 'Strata')}.` });
|
||||
assert.equal(libraryFolderFromPick(root).path, join(root, 'Strata', 'Models'));
|
||||
assert.deepEqual(dataFolderFromPick(tmpdir()), { path: tmpdir(), note: null });
|
||||
assert.deepEqual(libraryFolderFromPick(tmpdir()), { path: tmpdir(), note: null });
|
||||
});
|
||||
|
||||
test('a folder that cannot be made is said in words', t => {
|
||||
const dir = scratch(t);
|
||||
writeFileSync(join(dir, 'a-file'), 'x');
|
||||
assert.throws(() => makeFolder(join(dir, 'a-file', 'Strata')), /Strata could not create .*Choose another folder\./);
|
||||
assert.equal(makeFolder(join(dir, 'new', 'Strata')), join(dir, 'new', 'Strata'));
|
||||
assert.equal(existsSync(join(dir, 'new', 'Strata')), true);
|
||||
});
|
||||
|
||||
test('models and Models are one folder; a disk that cannot hold the link gets a plain models folder', t => {
|
||||
const dir = scratch(t), library = join(dir, 'Models');
|
||||
mkdirSync(library);
|
||||
const refuses = () => { throw Object.assign(new Error('EPERM: operation not permitted, symlink'), { code: 'EPERM' }); };
|
||||
const result = alignModels(dir, library, { link: refuses });
|
||||
assert.equal(result.linked, false);
|
||||
assert.equal(existsSync(join(dir, 'models')), true);
|
||||
// where the names are one folder already (Windows, macOS) nothing is made; where a link works it is used
|
||||
const other = scratch(t);
|
||||
const calls = [];
|
||||
assert.equal(alignModels(other, join(other, 'Models'), { link: (...args) => { calls.push(args); } }).linked, true);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0][1], join(other, 'models'));
|
||||
});
|
||||
|
||||
test('the data folder is written into the Control settings file, merged and only when it changes', t => {
|
||||
const dir = scratch(t), file = join(dir, 'config', 'app-settings.json');
|
||||
assert.equal(rememberDataFolder(file, 'D:\\Strata'), true);
|
||||
assert.equal(JSON.parse(readFileSync(file, 'utf8')).data_dir, 'D:\\Strata');
|
||||
writeFileSync(file, JSON.stringify({ installs: ['C:\\x'], data_dir: 'C:\\old' }));
|
||||
assert.equal(rememberDataFolder(file, 'D:\\Strata'), true);
|
||||
assert.deepEqual(JSON.parse(readFileSync(file, 'utf8')).installs, ['C:\\x']);
|
||||
assert.equal(rememberDataFolder(file, 'D:\\Strata'), false);
|
||||
writeFileSync(file, '\uFEFF{"data_dir":"E:\\\\Strata","installs":[]}');
|
||||
assert.equal(rememberDataFolder(file, 'E:\\Strata'), false);
|
||||
writeFileSync(file, 'not json');
|
||||
assert.equal(rememberDataFolder(file, 'F:\\Strata'), true);
|
||||
assert.equal(rememberDataFolder('', 'F:\\Strata'), false);
|
||||
});
|
||||
|
||||
test('choosing a folder that is gone or cannot be written says so in words', async t => {
|
||||
const f = nativeFixture();
|
||||
t.after(() => f.cleanup());
|
||||
await f.control.refresh();
|
||||
assert.throws(() => f.control.setDownloadDirectory(join(f.repo, 'not-there')), /That folder does not exist\. Check the path, or that the drive is connected\./);
|
||||
assert.throws(() => f.control.setDownloadDirectory(join(f.repo, 'setup.py')), /Choose a folder, not a file\./);
|
||||
});
|
||||
|
||||
test('Windows: a drive picked as D:\\ becomes D:\\Strata, and the person is not asked for a different folder', { skip: process.platform !== 'win32' }, async t => {
|
||||
const dir = scratch(t);
|
||||
const letter = 'QRSTUVWXYZ'.split('').find(l => !existsSync(`${l}:\\`));
|
||||
assert.ok(letter, 'a free drive letter');
|
||||
execFileSync('subst', [`${letter}:`, dir]);
|
||||
t.after(() => { try { execFileSync('subst', [`${letter}:`, '/D']); } catch { /* gone */ } });
|
||||
const f = nativeFixture();
|
||||
t.after(() => f.cleanup());
|
||||
await f.control.refresh();
|
||||
f.control.setDownloadDirectory(`${letter}:\\`);
|
||||
const shown = f.control.state().downloadDirectory;
|
||||
assert.equal(shown.toLowerCase(), `${letter}:\\Strata`.toLowerCase());
|
||||
assert.equal(existsSync(join(dir, 'Strata')), true);
|
||||
assert.equal(readdirNames(dir).includes('Strata'), true);
|
||||
});
|
||||
|
||||
function readdirNames(dir) { return execFileSync('cmd', ['/c', 'dir', '/b', dir]).toString().split(/\r?\n/).filter(Boolean); }
|
||||
|
||||
test('Windows: the model library on a whole drive goes to X:\\Strata\\Models', { skip: process.platform !== 'win32' }, t => {
|
||||
const dir = scratch(t), home = scratch(t);
|
||||
const letter = 'QRSTUVWXYZ'.split('').find(l => !existsSync(`${l}:\\`));
|
||||
execFileSync('subst', [`${letter}:`, dir]);
|
||||
t.after(() => { try { execFileSync('subst', [`${letter}:`, '/D']); } catch { /* gone */ } });
|
||||
const cwd = fileURLToPath(new URL('..', import.meta.url));
|
||||
const drive = `${letter}:\\`;
|
||||
const run = spawnSync(process.execPath, ['--input-type=module', '-e', `const store=await import('./server/store.mjs');console.log(store.validateModelsDirectory(${JSON.stringify(drive)}));`],
|
||||
{ cwd, env: { ...process.env, STRATA_HOME: home, STRATA_MODELS_DIR: join(home, 'models') }, encoding: 'utf8' });
|
||||
assert.equal(run.status, 0, run.stderr);
|
||||
assert.equal(run.stdout.trim().toLowerCase(), `${drive}Strata\\Models`.toLowerCase());
|
||||
assert.equal(existsSync(join(dir, 'Strata', 'Models')), true);
|
||||
});
|
||||
|
||||
test('the app and the Terminal share one models folder: the one changed last wins, the other file follows, nothing moves', t => {
|
||||
const dir = scratch(t), appFile = join(dir, 'app-settings.json'), terminalFile = join(dir, 'roaming', 'settings.json');
|
||||
const c = join(dir, 'on-c'), d = join(dir, 'on-d');
|
||||
mkdirSync(c); mkdirSync(d);
|
||||
const seed = (file, folder, at) => { mkdirSync(join(file, '..'), { recursive: true }); writeFileSync(file, JSON.stringify({ data_dir: folder, data_dir_at: at, installs: ['keep'] })); };
|
||||
// the Terminal chose D: after the app chose C:
|
||||
seed(appFile, c, 1000); seed(terminalFile, d, 2000);
|
||||
assert.deepEqual(syncDataFolder({ appFile, terminalFile }), { folder: d, from: 'terminal' });
|
||||
assert.deepEqual(readDataFolder(appFile), { folder: d, at: 2000 });
|
||||
assert.deepEqual(JSON.parse(readFileSync(appFile, 'utf8')).installs, ['keep']);
|
||||
assert.deepEqual(syncDataFolder({ appFile, terminalFile }), { folder: d, from: null });
|
||||
// the app chose C: later
|
||||
seed(appFile, c, 3000);
|
||||
assert.deepEqual(syncDataFolder({ appFile, terminalFile }), { folder: c, from: 'app' });
|
||||
assert.deepEqual(readDataFolder(terminalFile), { folder: c, at: 3000 });
|
||||
// the Terminal has none: nothing is written there; the app has none: it takes the Terminal's
|
||||
const lone = join(dir, 'lone.json');
|
||||
assert.deepEqual(syncDataFolder({ appFile: join(dir, 'none.json'), terminalFile: lone }), { folder: null, from: null });
|
||||
assert.equal(existsSync(lone), false);
|
||||
seed(lone, d, 10);
|
||||
assert.deepEqual(syncDataFolder({ appFile: join(dir, 'none.json'), terminalFile: lone }), { folder: d, from: 'terminal' });
|
||||
// the app's preference (native-host.json) counts when its file names none
|
||||
assert.deepEqual(syncDataFolder({ appFile: join(dir, 'none2.json'), terminalFile: lone, app: { folder: c, at: 99 } }), { folder: c, from: 'app' });
|
||||
// a Terminal folder on a disk that is not there is never adopted
|
||||
seed(terminalFile, join(dir, 'unplugged'), 9999); seed(appFile, c, 5);
|
||||
assert.deepEqual(syncDataFolder({ appFile, terminalFile }), { folder: c, from: 'app' });
|
||||
assert.equal(readdirSyncSafe(c), 0);
|
||||
});
|
||||
const readdirSyncSafe = folder => execFileSync(process.execPath, ['-e', `console.log(require('fs').readdirSync(${JSON.stringify(folder)}).length)`]).toString().trim() * 1;
|
||||
|
||||
test('the Terminal settings file is where setup.py and Control keep it', () => {
|
||||
assert.equal(terminalSettingsFile({ APPDATA: 'C:\R' }, 'win32'), join('C:\R', 'Strata', 'settings.json'));
|
||||
assert.equal(terminalSettingsFile({ XDG_CONFIG_HOME: '/x' }, 'linux'), join('/x', 'strata', 'settings.json'));
|
||||
assert.equal(terminalSettingsFile({ STRATA_TERMINAL_SETTINGS_FILE: '/t.json' }, 'linux'), '/t.json');
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const cwd = fileURLToPath(new URL('..', import.meta.url));
|
||||
function fixture(t) { const home = realpathSync(mkdtempSync(join(tmpdir(), 'strata-fallback-'))); t.after(() => rmSync(home, { recursive: true, force: true })); return home; }
|
||||
function load(env) {
|
||||
const code = "const s = await import('./server/store.mjs'); console.log(JSON.stringify({ dir: s.MODELS_DIR, notice: s.MODELS_DIR_NOTICE }));";
|
||||
const run = spawnSync(process.execPath, ['--input-type=module', '-e', code], { cwd, encoding: 'utf8', env: { ...process.env, ...env } });
|
||||
assert.equal(run.status, 0, run.stderr);
|
||||
return JSON.parse(run.stdout.trim().split('\n').pop());
|
||||
}
|
||||
|
||||
test('a Documents folder that cannot be made does not stop the host: models go to STRATA_HOME/models with a notice', t => {
|
||||
const home = fixture(t);
|
||||
const blocker = join(home, 'not-a-folder'); writeFileSync(blocker, 'x'); // mkdir below a file fails like a blocked Documents folder
|
||||
const { dir, notice } = load({ STRATA_HOME: join(home, 'h'), STRATA_MODELS_DIR: '', STRATA_DOCUMENTS_DIR: join(blocker, 'Documents') });
|
||||
assert.equal(dir, realpathSync(join(home, 'h', 'models')));
|
||||
assert.match(notice, /could not use .* for models/);
|
||||
assert.match(notice, /Settings/);
|
||||
});
|
||||
|
||||
test('a usable Documents folder is used and says nothing', t => {
|
||||
const home = fixture(t);
|
||||
const docs = join(home, 'Documents'); mkdirSync(docs);
|
||||
const { dir, notice } = load({ STRATA_HOME: join(home, 'h'), STRATA_MODELS_DIR: '', STRATA_DOCUMENTS_DIR: docs });
|
||||
assert.equal(dir, realpathSync(join(docs, 'Strata', 'Models')));
|
||||
assert.equal(notice, '');
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { createNativeControl } from '../server/native-control.mjs';
|
||||
import { createControl } from '../server/control-runtime.mjs';
|
||||
|
||||
/** The host in the packaged app with a stand-in for Strata Control: the move itself is Control's (tests/terminal/test_models_move.py). */
|
||||
function fixture(t, { plan = {}, move = null, unfinished = null } = {}) {
|
||||
const root = realpathSync(mkdtempSync(join(tmpdir(), 'strata-mfolder-')));
|
||||
const install = join(root, 'install'), data = join(root, 'host'), oldFolder = join(root, 'old'), newFolder = join(root, 'new');
|
||||
for (const d of [install, data, oldFolder, newFolder]) mkdirSync(d, { recursive: true });
|
||||
const earlier = { install: process.env.STRATA_INSTALL_DIR, terminal: process.env.STRATA_TERMINAL_SETTINGS_FILE, settings: process.env.STRATA_SETTINGS_FILE, comp: process.env.STRATA_COMPONENTS_DIR };
|
||||
process.env.STRATA_INSTALL_DIR = install;
|
||||
process.env.STRATA_TERMINAL_SETTINGS_FILE = join(root, 'terminal-settings.json');
|
||||
process.env.STRATA_COMPONENTS_DIR = join(root, 'components');
|
||||
delete process.env.STRATA_SETTINGS_FILE;
|
||||
const calls = [];
|
||||
const control = {
|
||||
control: { dir: join(root, 'control'), python: join(root, 'control', 'python.exe'), cli: join(root, 'control', 'strata.py') }, version: '0.2.2',
|
||||
async installedModels() { return { install_dir: install, models: [] }; },
|
||||
async engine() { return { usable: true, engine: { version: '0.2.2', root: root } }; },
|
||||
async importPlan() { return {}; },
|
||||
async planModelsMove(from, to) {
|
||||
calls.push(['plan', from, to]);
|
||||
return { ok: true, issue: null, old: from, new: to, same_drive: false, free_bytes: 900e9, models_bytes: 74e9, needed_bytes: 77.7e9, models: [{ group: 'gyro-s', name: 'gyro-s', bytes: 58e9 }, { group: 'coder', name: 'coder', bytes: 16e9 }],
|
||||
choices: { move: { possible: true, reason: '' }, keep: { possible: true, reason: '' }, empty: { possible: true, reason: '' } }, ...plan };
|
||||
},
|
||||
async moveModels(from, to, options) {
|
||||
calls.push(['move', from, to, options.installDir]);
|
||||
if (move) return move(from, to, options);
|
||||
options.onEvent({ event: 'progress', done: 5e9, total: 74e9, file: 'models/gyro-s/a.gguf', group: 'gyro-s', index: 1, count: 3, seconds_left: 300, same_drive: false });
|
||||
return { ok: true, event: 'result', done: true, stopped: false, moved: ['gyro-s', 'coder'], remaining: [], bytes_moved: 74e9, total_bytes: 74e9, files_moved: 3, same_drive: false };
|
||||
},
|
||||
async interruptedModelsMove(from) { calls.push(['status', from]); return unfinished; },
|
||||
async resumeModelsMove(from, options) {
|
||||
calls.push(['resume', from, options.installDir]);
|
||||
options.onEvent({ event: 'progress', done: 20e9, total: 74e9, file: 'models/coder/c.gguf', group: 'coder', index: 2, count: 3, seconds_left: 120, same_drive: false });
|
||||
return { ok: true, event: 'result', done: true, stopped: false, moved: ['coder'], remaining: [], bytes_moved: 74e9, total_bytes: 74e9, files_moved: 2, same_drive: false };
|
||||
},
|
||||
async leaveModelsMove(from, options) { calls.push(['leave', from, options.installDir]); return { ok: true, removed: 1, restored: 1, bytes_removed: 5 }; },
|
||||
async stop() { return { ok: true }; },
|
||||
};
|
||||
const spawnProcess = () => { throw new Error('the host starts no process here'); };
|
||||
const native = createNativeControl({ control, directory: data, spawnProcess, fetcher: async () => Response.json({}), requests: () => [] });
|
||||
t.after(() => {
|
||||
for (const [key, name] of [['install', 'STRATA_INSTALL_DIR'], ['terminal', 'STRATA_TERMINAL_SETTINGS_FILE'], ['settings', 'STRATA_SETTINGS_FILE'], ['comp', 'STRATA_COMPONENTS_DIR']]) {
|
||||
if (earlier[key] === undefined) delete process.env[name]; else process.env[name] = earlier[key];
|
||||
}
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
native.setDownloadDirectory(oldFolder);
|
||||
return { native, control, calls, root, install, oldFolder, newFolder, terminalFile: process.env.STRATA_TERMINAL_SETTINGS_FILE };
|
||||
}
|
||||
const finish = async (native, id) => { for (let i = 0; i < 400; i++) { const job = native.state().jobs.find(j => j.id === id); if (job.state !== 'running') return job; await new Promise(r => setTimeout(r, 10)); } throw new Error('job did not finish'); };
|
||||
|
||||
test('the plan says what the picked folder means and keeps the person\'s spelling of it', async t => {
|
||||
const f = fixture(t);
|
||||
const plan = await f.native.planModelsFolder(f.newFolder);
|
||||
assert.equal(plan.current, f.oldFolder);
|
||||
assert.equal(plan.folder, f.newFolder);
|
||||
assert.equal(plan.wholeDrive, false);
|
||||
assert.equal(plan.shown, f.newFolder);
|
||||
assert.equal(plan.models.length, 2);
|
||||
assert.deepEqual(f.calls[0], ['plan', f.oldFolder, f.newFolder]);
|
||||
});
|
||||
|
||||
test('a whole drive is planned as <drive>\\Strata, shown as <drive>\\Strata\\Models', async t => {
|
||||
const f = fixture(t);
|
||||
const root = f.root.slice(0, f.root.indexOf(process.platform === 'win32' ? '\\' : '/') + 1) || '/';
|
||||
const plan = await f.native.planModelsFolder(root);
|
||||
assert.equal(plan.wholeDrive, true);
|
||||
assert.equal(plan.folder, join(root, 'Strata'));
|
||||
assert.equal(plan.shown, join(root, 'Strata', 'Models'));
|
||||
rmSync(join(root, 'Strata'), { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('the card is blocked while a model runs or a job runs: a running model must be stopped on purpose', async t => {
|
||||
const f = fixture(t);
|
||||
await assert.rejects(f.native.startModelsFolder({ folder: f.newFolder, choice: 'bogus' }), /Choose what to do/);
|
||||
const refused = fixture(t, { plan: { choices: { move: { possible: false, reason: 'Your models need 77 GB, this drive has 38 GB free.' }, keep: { possible: true, reason: '' }, empty: { possible: true, reason: '' } } } });
|
||||
await assert.rejects(refused.native.startModelsFolder({ folder: refused.newFolder, choice: 'move' }), /need 77 GB/);
|
||||
assert.equal(refused.calls.some(c => c[0] === 'move'), false);
|
||||
});
|
||||
|
||||
test('Keep: the new folder is used for new downloads, nothing is moved, both settings files follow', async t => {
|
||||
const f = fixture(t);
|
||||
writeFileSync(f.terminalFile, JSON.stringify({ data_dir: f.oldFolder, data_dir_at: 1 })); // the Terminal had chosen the old folder
|
||||
const result = await f.native.startModelsFolder({ folder: f.newFolder, choice: 'keep' });
|
||||
assert.equal(result.done, true);
|
||||
assert.equal(f.native.state().downloadDirectory, f.newFolder);
|
||||
assert.equal(f.calls.some(c => c[0] === 'move'), false);
|
||||
assert.equal(JSON.parse(readFileSync(f.terminalFile, 'utf8')).data_dir, f.newFolder);
|
||||
});
|
||||
|
||||
test('Start empty: the old models are no longer listed (their run configs are set aside, not deleted), the model files stay', async t => {
|
||||
const f = fixture(t);
|
||||
const oldFile = join(f.oldFolder, 'models', 'gyro-s', 'a.gguf');
|
||||
mkdirSync(join(f.oldFolder, 'models', 'gyro-s'), { recursive: true });
|
||||
writeFileSync(oldFile, 'x');
|
||||
writeFileSync(join(f.install, 'strata-gyro-s.json'), JSON.stringify({ args: ['--native', oldFile] }));
|
||||
writeFileSync(join(f.install, 'strata-other.json'), JSON.stringify({ args: ['--native', join(f.root, 'elsewhere', 'b.gguf')] }));
|
||||
const result = await f.native.startModelsFolder({ folder: f.newFolder, choice: 'empty' });
|
||||
assert.deepEqual(result.setAside, ['strata-gyro-s.json']);
|
||||
assert.equal(existsSync(join(f.install, 'strata-gyro-s.json')), false);
|
||||
assert.equal(existsSync(join(f.install, 'old-models', 'strata-gyro-s.json')), true);
|
||||
assert.equal(existsSync(join(f.install, 'strata-other.json')), true, 'a model elsewhere is not touched');
|
||||
assert.equal(existsSync(oldFile), true);
|
||||
assert.equal(f.native.state().downloadDirectory, f.newFolder);
|
||||
});
|
||||
|
||||
test('Move: a job with progress; the folder changes only when everything moved', async t => {
|
||||
const f = fixture(t);
|
||||
const job = await f.native.startModelsFolder({ folder: f.newFolder, choice: 'move' });
|
||||
assert.equal(job.kind, 'models-move');
|
||||
const done = await finish(f.native, job.id);
|
||||
assert.equal(done.state, 'done');
|
||||
assert.equal(done.result.done, true);
|
||||
assert.equal(done.result.sameDrive, false);
|
||||
assert.equal(f.native.state().downloadDirectory, f.newFolder);
|
||||
assert.deepEqual(f.calls.find(c => c[0] === 'move').slice(1), [f.oldFolder, f.newFolder, f.install]);
|
||||
});
|
||||
|
||||
test('Move stopped: the folder stays, what moved is reported', async t => {
|
||||
const f = fixture(t, { move: async () => ({ ok: true, event: 'result', done: false, stopped: true, moved: ['coder'], remaining: ['gyro-s'], bytes_moved: 16e9, total_bytes: 74e9, files_moved: 1, same_drive: false }) });
|
||||
const job = await f.native.startModelsFolder({ folder: f.newFolder, choice: 'move' });
|
||||
const done = await finish(f.native, job.id);
|
||||
assert.equal(done.result.stopped, true);
|
||||
assert.deepEqual(done.result.moved, ['coder']);
|
||||
assert.equal(f.native.state().downloadDirectory, f.oldFolder, 'a partial move never points the app at an incomplete folder');
|
||||
});
|
||||
|
||||
test('Move failed (disk full): the folder stays and the job says what happened', async t => {
|
||||
const f = fixture(t, { move: async () => { throw Object.assign(new Error('The drive ran out of space.'), { code: 'disk_full', details: 'ENOSPC: no space left on device', output: { moved: ['coder'], remaining: ['gyro-s'], bytes_moved: 52e9, total_bytes: 74e9 } }); } });
|
||||
const job = await f.native.startModelsFolder({ folder: f.newFolder, choice: 'move' });
|
||||
const done = await finish(f.native, job.id);
|
||||
assert.equal(done.state, 'error');
|
||||
assert.equal(done.result.code, 'disk_full');
|
||||
assert.equal(done.result.bytes, 52e9);
|
||||
assert.match(done.details, /ENOSPC/);
|
||||
assert.equal(f.native.state().downloadDirectory, f.oldFolder);
|
||||
});
|
||||
|
||||
test('Move: Stop writes the cancel file and the run ends by itself', async t => {
|
||||
let release;
|
||||
const f = fixture(t, { move: (from, to, options) => new Promise(resolve => { release = () => resolve({ ok: true, event: 'result', done: false, stopped: existsSync(options.cancelFile), moved: [], remaining: ['gyro-s', 'coder'], bytes_moved: 0, total_bytes: 74e9, files_moved: 0, same_drive: false }); }) });
|
||||
const job = await f.native.startModelsFolder({ folder: f.newFolder, choice: 'move' });
|
||||
await f.native.cancel(job.id);
|
||||
assert.equal(f.native.state().jobs.find(j => j.id === job.id).state, 'running', 'it ends when Control has cleaned up, not before');
|
||||
release();
|
||||
const done = await finish(f.native, job.id);
|
||||
assert.equal(done.result.stopped, true);
|
||||
assert.equal(f.native.state().downloadDirectory, f.oldFolder);
|
||||
});
|
||||
|
||||
test('a move needs the app: the Control command line is built from the plan and the cancel file', async () => {
|
||||
const seen = [];
|
||||
const control = createControl({ control: { dir: '/c', python: '/c/py', cli: '/c/strata.py' }, run: async (args, options) => { seen.push([args, options]); return args.includes('--plan') ? { code: 0, json: { ok: true, plan: { ok: true } }, text: '', stderr: '' } : { code: 0, json: { ok: true, event: 'result', done: true }, text: '', stderr: '' }; } });
|
||||
assert.deepEqual(await control.planModelsMove('/old', '/new'), { ok: true });
|
||||
assert.deepEqual(seen[0][0], ['models', 'move', '/new', '--from', '/old', '--plan', '--output', 'json']);
|
||||
const result = await control.moveModels('/old', '/new', { installDir: '/i', cancelFile: '/c.cancel' });
|
||||
assert.equal(result.done, true);
|
||||
assert.deepEqual(seen[1][0], ['models', 'move', '/new', '--from', '/old', '--install-dir', '/i', '--cancel-file', '/c.cancel', '--output', 'jsonl']);
|
||||
const failing = createControl({ control: { dir: '/c', python: '/c/py', cli: '/c/strata.py' }, run: async () => ({ code: 5, json: { ok: false, error: { code: 'disk_full', message: 'The drive ran out of space.', details: 'ENOSPC' }, moved: [] }, text: '', stderr: '' }) });
|
||||
await assert.rejects(failing.moveModels('/o', '/n'), e => e.code === 'disk_full' && e.details === 'ENOSPC' && /ran out of space/.test(e.message));
|
||||
});
|
||||
|
||||
test('an unfinished move is found at the next start, and Finish resumes it as a job that changes the folder only when done', async t => {
|
||||
const f = fixture(t);
|
||||
f.control.interruptedModelsMove = async from => { f.calls.push(['status', from]); return { state: 'failed', from: f.oldFolder, to: f.newFolder, bytes: 74e9 }; };
|
||||
assert.equal((await f.native.interruptedMove()).to, f.newFolder);
|
||||
assert.deepEqual(f.calls[0], ['status', f.oldFolder]);
|
||||
const job = await f.native.finishInterruptedMove({ stopRunning: true });
|
||||
const done = await finish(f.native, job.id);
|
||||
assert.equal(done.state, 'done');
|
||||
assert.equal(f.calls.find(c => c[0] === 'resume')[1], f.oldFolder);
|
||||
assert.equal(f.native.state().downloadDirectory, f.newFolder);
|
||||
});
|
||||
|
||||
test('Leave it keeps the old folder and asks Control to remove the partial copies; nothing interrupted means nothing to do', async t => {
|
||||
const f = fixture(t);
|
||||
assert.equal(await f.native.interruptedMove(), null);
|
||||
await assert.rejects(f.native.leaveInterruptedMove(), /No move was interrupted/);
|
||||
f.control.interruptedModelsMove = async () => ({ state: 'running', from: f.oldFolder, to: f.newFolder, bytes: 1 });
|
||||
const result = await f.native.leaveInterruptedMove();
|
||||
assert.equal(result.folder, f.oldFolder);
|
||||
assert.deepEqual(f.calls.find(c => c[0] === 'leave').slice(0, 2), ['leave', f.oldFolder]);
|
||||
assert.equal(f.native.state().downloadDirectory, f.oldFolder);
|
||||
});
|
||||
|
||||
test('the Control command lines for status, resume and leave', async () => {
|
||||
const seen = [];
|
||||
const control = createControl({ control: { dir: '/c', python: '/c/py', cli: '/c/strata.py' }, run: async args => { seen.push(args); return args.includes('--status') ? { code: 0, json: { ok: true, interrupted: { from: '/o', to: '/n' } }, text: '', stderr: '' } : args.includes('--leave') ? { code: 0, json: { ok: true, removed: 0 }, text: '', stderr: '' } : { code: 0, json: { ok: true, event: 'result', done: true }, text: '', stderr: '' }; } });
|
||||
assert.equal((await control.interruptedModelsMove('/o')).to, '/n');
|
||||
await control.resumeModelsMove('/o', { installDir: '/i', cancelFile: '/c.cancel' });
|
||||
await control.leaveModelsMove('/o', { installDir: '/i' });
|
||||
assert.deepEqual(seen[0], ['models', 'move', '--status', '--from', '/o', '--output', 'json']);
|
||||
assert.deepEqual(seen[1], ['models', 'move', '--resume', '--from', '/o', '--install-dir', '/i', '--cancel-file', '/c.cancel', '--output', 'jsonl']);
|
||||
assert.deepEqual(seen[2], ['models', 'move', '--leave', '--from', '/o', '--install-dir', '/i', '--output', 'json']);
|
||||
});
|
||||
|
||||
test('the models folder strings exist in all 8 languages with the same placeholders', () => {
|
||||
const read = lang => JSON.parse(readFileSync(new URL(`../src/locales/${lang}.json`, import.meta.url), 'utf8'));
|
||||
const en = read('en');
|
||||
const ours = Object.keys(en).filter(k => Number(k.slice(1)) >= 8339 && Number(k.slice(1)) <= 8422);
|
||||
assert.equal(ours.length, 84);
|
||||
for (const lang of ['de', 'es', 'ja', 'ko', 'pt', 'ru', 'zh']) {
|
||||
const text = read(lang);
|
||||
for (const key of ours) {
|
||||
assert.ok(text[key], `${lang} ${key}`);
|
||||
assert.deepEqual((text[key].match(/\{\d\}/g) ?? []).sort(), (en[key].match(/\{\d\}/g) ?? []).sort(), `${lang} ${key} placeholders`);
|
||||
}
|
||||
}
|
||||
// every string the dialog shows through tr() is in the catalogue
|
||||
const source = readFileSync(new URL('../src/components/ModelsFolderDialog.tsx', import.meta.url), 'utf8');
|
||||
const values = new Set(Object.values(en));
|
||||
for (const text of ['Models folder', 'Change folder', 'Move my models', 'Use both folders', 'Start empty', 'Stop moving', 'Your models are moved', 'Moving stopped']) {
|
||||
assert.ok(source.includes(text) && values.has(text), text);
|
||||
}
|
||||
});
|
||||
@@ -4,10 +4,9 @@ import { readFile } from 'node:fs/promises';
|
||||
|
||||
test('the card list preselects the card the loaded model runs on, and the sidebar card says the version and the card', async () => {
|
||||
const models = await readFile(new URL('../src/components/NativeModels.tsx', import.meta.url), 'utf8');
|
||||
assert.ok(models.indexOf('const loadedSelected = ') < models.indexOf('const loadedCards = '));
|
||||
assert.ok(models.indexOf('const loadedCards = ') < models.indexOf('const activeCards = '));
|
||||
assert.match(models, /cardOptions\.options\.find\(o => o\.key === cardsKey\) \?\? loadedCards \?\?/);
|
||||
assert.match(models, /o\.cards\.every\(c => loadedSelected\.gpus\.includes\(c\)\)/);
|
||||
assert.ok(models.indexOf('const loadedSelected = ') < models.indexOf('const loadedRoles = '));
|
||||
assert.match(models, /fromLegacy\(loadedSelected\.gpus, roleCards, 'as_given'\)/);
|
||||
assert.match(models, /roles \? normalizeRoles\(roles, roleCards\) : loadedRoles \?\? recommended\.roles/);
|
||||
const side = await readFile(new URL('../src/app/Sidebar.tsx', import.meta.url), 'utf8');
|
||||
assert.match(side, /nativeSize\?\.model \?\? nativeCurrent\.model\.toUpperCase\(\)/);
|
||||
assert.match(side, /`GPU \$\{g \+ 1\}`/);
|
||||
|
||||
@@ -96,3 +96,22 @@ test('#3211 #3221: the Monitor opens on the model\'s assigned cards and the drop
|
||||
const css = await readFile(new URL('../src/styles/annotation-fixes.css', import.meta.url), 'utf8');
|
||||
assert.match(css, /\.select__opt \{ height: auto; min-height: 36px;/, 'a wrapped name no longer runs over the next row');
|
||||
});
|
||||
|
||||
test('a card picked in the screen replaces an old AMD config\'s "hip_ordinal" (it would send the engine to the old card)', async () => {
|
||||
const f = nativeFixture();
|
||||
try {
|
||||
await f.control.refresh(true);
|
||||
const { readFileSync, writeFileSync } = await import('node:fs');
|
||||
const { join } = await import('node:path');
|
||||
const path = join(f.repo, 'strata-iq2_xs.json');
|
||||
const config = JSON.parse(readFileSync(path, 'utf8')); config.hip_ordinal = 0; writeFileSync(path, JSON.stringify(config));
|
||||
await f.control.refresh(true);
|
||||
const done = await finishJob(f.control, f.control.operation('load', { model: 'iq2_xs', gpus: [1] }).id);
|
||||
assert.equal(done.state, 'done', done.error);
|
||||
const instance = f.control.resolveInstance(done.result.instanceId);
|
||||
assert.deepEqual(instance.gpus, [1]);
|
||||
const written = JSON.parse(readFileSync(instance.configPath, 'utf8'));
|
||||
assert.equal(written.gpu, 1);
|
||||
assert.equal('hip_ordinal' in written, false);
|
||||
} finally { await f.cleanup(); }
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
/** GPU roles in the host: a load writes the roles and the legacy mirror (Main last), a file that names roles keeps them, every card Off is allowed. */
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { nativeFixture, finishJob } from './helpers/native-fixture.mjs';
|
||||
|
||||
const load = async (f, args) => { const done = await finishJob(f.control, f.control.operation('load', { model: 'iq2_xs', ...args }).id); return done; };
|
||||
const written = (f, done) => JSON.parse(readFileSync(f.control.resolveInstance(done.result.instanceId).configPath, 'utf8'));
|
||||
|
||||
test('roles chosen in the screen: Helpers first, Main LAST, Off left out, and "gpu_roles" beside the mirror', async () => {
|
||||
const f = nativeFixture();
|
||||
try {
|
||||
await f.control.refresh(true);
|
||||
const done = await load(f, { gpuRoles: { 0: 'main', 1: 'helper' } });
|
||||
assert.equal(done.state, 'done', done.error);
|
||||
const cfg = written(f, done);
|
||||
assert.deepEqual(cfg.gpu, [1, 0], 'card 0 is Main, so it is the last stage');
|
||||
assert.equal(cfg.gpu_order, 'as_given');
|
||||
assert.deepEqual(cfg.gpu_roles, { 0: 'main', 1: 'helper' });
|
||||
assert.equal(cfg.layer_split, 'auto');
|
||||
assert.deepEqual(f.control.resolveInstance(done.result.instanceId).gpus, [1, 0]);
|
||||
await f.control.unload(done.result.instanceId);
|
||||
const one = await load(f, { gpuRoles: { 0: 'off', 1: 'main' } });
|
||||
assert.equal(one.state, 'done', one.error);
|
||||
const c1 = written(f, one);
|
||||
assert.equal(c1.gpu, 1, 'one card is a number, never a list');
|
||||
assert.deepEqual(c1.gpu_roles, { 0: 'off', 1: 'main' });
|
||||
} finally { await f.cleanup(); }
|
||||
});
|
||||
|
||||
test('every card Off is allowed: no card in the list, "gpu_none" says it was meant', async () => {
|
||||
const f = nativeFixture();
|
||||
try {
|
||||
await f.control.refresh(true);
|
||||
const done = await load(f, { gpuRoles: { 0: 'off', 1: 'off' } });
|
||||
assert.equal(done.state, 'done', done.error);
|
||||
const cfg = written(f, done);
|
||||
assert.deepEqual(cfg.gpu, []);
|
||||
assert.equal(cfg.gpu_none, true);
|
||||
} finally { await f.cleanup(); }
|
||||
});
|
||||
|
||||
test('a settings file that names roles keeps them when the screen only suggests cards; a plain card list replaces them', async () => {
|
||||
const f = nativeFixture();
|
||||
try {
|
||||
await f.control.refresh(true);
|
||||
const path = join(f.repo, 'strata-iq2_xs.json');
|
||||
const config = JSON.parse(readFileSync(path, 'utf8'));
|
||||
Object.assign(config, { gpu: [0, 1], gpu_order: 'as_given', gpu_roles: { 0: 'helper', 1: 'main' } });
|
||||
writeFileSync(path, JSON.stringify(config));
|
||||
await f.control.refresh(true);
|
||||
const kept = await load(f, { gpus: [1, 0], gpusFromRecommendation: true });
|
||||
assert.equal(kept.state, 'done', kept.error);
|
||||
assert.deepEqual(written(f, kept).gpu, [0, 1]);
|
||||
await f.control.unload(kept.result.instanceId);
|
||||
const picked = await load(f, { gpus: [1] });
|
||||
assert.equal(picked.state, 'done', picked.error);
|
||||
const cfg = written(f, picked);
|
||||
assert.equal(cfg.gpu, 1);
|
||||
assert.equal('gpu_roles' in cfg, false, 'a card list picked by hand replaces the roles');
|
||||
} finally { await f.cleanup(); }
|
||||
});
|
||||
|
||||
test('roles that make no sense are refused with a plain line', async () => {
|
||||
const f = nativeFixture();
|
||||
try {
|
||||
await f.control.refresh(true);
|
||||
const two = await load(f, { gpuRoles: { 0: 'main', 1: 'main' } });
|
||||
assert.equal(two.state, 'error');
|
||||
assert.match(two.error, /Main, Helper or Off/);
|
||||
const nope = await load(f, { gpuRoles: { 5: 'main' } });
|
||||
assert.equal(nope.state, 'error');
|
||||
assert.match(nope.error, /detected graphics cards/);
|
||||
} finally { await f.cleanup(); }
|
||||
});
|
||||
@@ -164,3 +164,12 @@ test('download-and-load prepares vision automatically while loading text only wh
|
||||
assert.deepEqual(JSON.parse(readFileSync(join(f.repo,'strata-iq2_xs.json'))).vision,{gpu:0});
|
||||
}finally{await f.cleanup();}
|
||||
});
|
||||
|
||||
test('a card that shares the PC memory does not hold back a fifth of the shared pool as graphics memory (#3510)',()=>{
|
||||
const original={gpu:0,args:['--max-context','131072','--pool-workers','10'],vision:{gpu:0}};
|
||||
const shared={cpu:{cores:16},ram_gb:128,gpus:[{index:0,vram_gb:82,uma:true,name:'AMD Radeon 8060S'}]};
|
||||
const apu=resourceSettings(original,shared,'work');
|
||||
assert.equal(apu.vram_reserve_mib,2048,'the display floor with pictures, not 20% of 82 GB (16 GB)');
|
||||
const discrete=resourceSettings(original,{cpu:{cores:16},ram_gb:128,gpus:[{index:0,vram_gb:82,name:'a card'}]},'work');
|
||||
assert.equal(discrete.vram_reserve_mib,Math.floor(82*1024*.2),'a card with its own memory keeps its fifth');
|
||||
});
|
||||
@@ -18,8 +18,10 @@ test('a second model on a card another model uses still loads (recommend, never
|
||||
|
||||
test('the screens say which card a loaded model uses, prefer a free card for a second model, and show an unload refusal beside its button', async () => {
|
||||
const models = await readFile(new URL('../src/components/NativeModels.tsx', import.meta.url), 'utf8');
|
||||
assert.match(models, /tr\('Already used by \{0\}', \[usedBy\(o\.cards\)\.join\(', '\)\]\)/);
|
||||
assert.match(models, /multiple \? cardOptions\.options\.find\(o => o\.recommended && !usedBy\(o\.cards\)\.length\)/);
|
||||
assert.match(models, /usedBy=\{i => usedBy\(\[i\]\)\}/);
|
||||
assert.match(models, /const busy = \(i: number\) => expert && multiple && usedBy\(\[i\]\)\.length > 0/); // a card another loaded model uses starts Off
|
||||
const roles = await readFile(new URL('../src/components/GpuRoles.tsx', import.meta.url), 'utf8');
|
||||
assert.match(roles, /tr\('In use by \{0\}', \[other\.join\(', '\)\]\)/);
|
||||
assert.match(models, /loadedAction\(`instances\/\$\{i\.id\}\/unload`\)/);
|
||||
assert.match(models, /loadedError \? <ErrorNotice message=\{loadedError\}/);
|
||||
const control = await readFile(new URL('../server/native-control.mjs', import.meta.url), 'utf8');
|
||||
@@ -28,7 +30,7 @@ test('the screens say which card a loaded model uses, prefer a free card for a s
|
||||
|
||||
test('usedBy is declared before the card choice that calls it (a const used earlier broke the Models page when Run multiple models was on)', async () => {
|
||||
const models = await readFile(new URL('../src/components/NativeModels.tsx', import.meta.url), 'utf8');
|
||||
assert.ok(models.indexOf('const usedBy = ') > 0 && models.indexOf('const usedBy = ') < models.indexOf('const activeCards = '));
|
||||
assert.ok(models.indexOf('const usedBy = ') > 0 && models.indexOf('const usedBy = ') < models.indexOf('const recommended = '));
|
||||
});
|
||||
|
||||
test('a model that failed to start is not offered in the chat model picker', async () => {
|
||||
@@ -54,8 +56,9 @@ test('a loaded model is named from the catalog (family and version), the file id
|
||||
|
||||
test('card names use the GPU numbering of the other screens, and the fit figures subtract the models that stay loaded', async () => {
|
||||
const models = await readFile(new URL('../src/components/NativeModels.tsx', import.meta.url), 'utf8');
|
||||
assert.ok(models.includes('(GPU ${Number(n) + 1})') && models.includes('text.replace(/\\(card (\\d+)\\)/g'));
|
||||
assert.match(models, /<strong>\{gpuNumbers\(o\.label\)\}<\/strong>/);
|
||||
const roles = await readFile(new URL('../src/lib/gpu-roles.ts', import.meta.url), 'utf8');
|
||||
assert.ok(roles.includes('`GPU ${c.index + 1}`')); // GPU 1 is card 0, as on every other screen
|
||||
assert.match(await readFile(new URL('../src/components/GpuRoles.tsx', import.meta.url), 'utf8'), /\{gpuLabel\(c\)\} · \{c\.name\}/);
|
||||
assert.match(models, /const keptModels = expert && multiple/);
|
||||
assert.match(models, /inUse=\{keptModels\.length \?/);
|
||||
const card = await readFile(new URL('../src/components/PcFitCard.tsx', import.meta.url), 'utf8');
|
||||
|
||||
@@ -534,3 +534,19 @@ test('the app never adopts an older command-line install: Control is always told
|
||||
const env = controlEnvironment({ dir: '/c' }, { env: { APPDATA: '/old/appdata' }, components: join(tmpdir(), 'Strata', 'components') });
|
||||
assert.equal(env.STRATA_SETTINGS_FILE, join(tmpdir(), 'Strata', 'app-settings.json'));
|
||||
});
|
||||
|
||||
test("onboarding 'use your own GGUF' (#3507, #3424): the models folder the person picked holds the GGUF files, so preparing them never takes it for the data folder", async () => {
|
||||
const f = packagedFixture();
|
||||
try {
|
||||
const own = join(f.root, 'Strata-data', 'models', 'unsloth-UD-Q4_K_XL');
|
||||
mkdirSync(own, { recursive: true });
|
||||
await f.native.refresh(true);
|
||||
f.native.setDownloadDirectory(own); // what the first-run folder chooser wrote
|
||||
await f.native.plan({ family: 'qwen', model: 'IQ3_S', gguf_dir: own });
|
||||
const sent = f.rpc.at(-1).arguments;
|
||||
assert.equal(sent.gguf_dir, own);
|
||||
assert.notEqual(sent.data_dir, own, 'the folder of GGUF files is not the data folder: the packs would be downloaded into it');
|
||||
// a folder of GGUF files named like a data folder's models/<tag> hands its Strata folder to the installer
|
||||
assert.ok(sent.data_dir === undefined || sent.data_dir === join(f.root, 'Strata-data'), `data_dir was ${sent.data_dir}`);
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
@@ -166,7 +166,7 @@ test('no recognised engine: the app still downloads, warns in the card, and load
|
||||
|
||||
test('the draft layer download of setup step 6 is a stage of its own and counts in the one figure (#3257)', () => {
|
||||
lib.setTranslationLanguage('en');
|
||||
const draft = { parts: [{ kind: 'model', name: 'Model files', state: 'done', done_gb: 60, total_gb: 60 }, { kind: 'draft', name: 'Speed-up layer', state: 'downloading', done_gb: 2, total_gb: 5 }] };
|
||||
const draft = { parts: [{ kind: 'model', name: 'Model files', state: 'done', done_gb: 60, total_gb: 60 }, { kind: 'draft', name: 'Draft layer', state: 'downloading', done_gb: 2, total_gb: 5 }] };
|
||||
assert.deepEqual(lib.installStage(draft), { kind: 'draft' });
|
||||
assert.equal(lib.stageText(lib.installStage(draft)), 'Preparing your model…');
|
||||
const p = lib.installProgress(draft);
|
||||
|
||||
@@ -57,5 +57,5 @@ test('Cargo.toml, tauri.conf.json and the desktop package agree on the app versi
|
||||
const cargo = /^version\s*=\s*"([^"]+)"/m.exec(await readFile(new URL('src-tauri/Cargo.toml', root), 'utf8'))[1];
|
||||
const pkg = JSON.parse(await readFile(new URL('package.json', root), 'utf8')).version;
|
||||
assert.equal(cargo, pkg);
|
||||
assert.equal(cargo, '0.2.1');
|
||||
assert.equal(cargo, '0.2.2');
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
/** The API key is optional (owner decision, 0.2.2): removing it while sharing or the Cloudflare tunnel runs is allowed, and the key stays removed. */
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const home = mkdtempSync(join(tmpdir(), 'strata-serving-key-'));
|
||||
process.env.STRATA_HOME = home;
|
||||
const serving = await import('../server/serving.mjs');
|
||||
|
||||
test('removing the API key while local sharing and the tunnel run is allowed', () => {
|
||||
serving.init({ send() {} }, { port: 8080, host: '127.0.0.1' });
|
||||
serving.patch({ generateKey: true });
|
||||
assert.equal(serving.state().access.apiKeySet, true);
|
||||
serving.setSharing({ active: true, url: 'http://192.168.1.5:8792/v1' });
|
||||
serving.setTunnel({ status: 'connected', mode: 'quick', url: 'https://example.trycloudflare.com', error: null, installed: true, originUrl: null });
|
||||
assert.equal(serving.state().access.otherDevices, true);
|
||||
const after = serving.patch({ apiKey: null });
|
||||
assert.equal(after.access.apiKeySet, false);
|
||||
assert.equal(serving.authorized({ headers: {} }), true);
|
||||
});
|
||||
test.after(() => { try { rmSync(home, { recursive: true, force: true }); } catch { /* temp folder */ } });
|
||||
@@ -24,10 +24,10 @@ test('tasks persist, prevent overlapping runs, pause and resume without catch-up
|
||||
const restarted=createTaskService({read:()=>saved,write:t=>{saved=structuredClone(t);},now:()=>now,run:async()=>({text:'new',demo:true})});
|
||||
restarted.put({enabled:true},task.id);assert.ok(Date.parse(saved[0].nextRunAt)>now);restarted.close();service.close();
|
||||
});
|
||||
test('sharing requires a key and never exposes host controls or files', async () => {
|
||||
test('sharing never exposes host controls or files, and a key is checked when one is set', async () => {
|
||||
let key=false;
|
||||
const share=createLocalShare({host:'127.0.0.1',port:0,hasKey:()=>key,handle:(req,res)=>{res.writeHead(req.headers.authorization==='Bearer test-only-key'?200:401);res.end('API');}});
|
||||
await assert.rejects(share.start(),/API key/);key=true;
|
||||
key=true;
|
||||
try {
|
||||
const {url}=await share.start();const origin=new URL(url).origin;
|
||||
assert.equal((await fetch(origin+'/api/tasks')).status,404);
|
||||
|
||||
@@ -27,6 +27,14 @@ test('a check made for other versions is ignored, so an old "Update available" d
|
||||
assert.equal(readUpdateCache({ appVersion: '0.2.1', engineVersion: '0.1.45' }), null);
|
||||
});
|
||||
|
||||
test('a new Control version, or a version that is not known yet, hides the old result', () => {
|
||||
saveUpdateCache(result(), { appVersion: '0.2.0', engineVersion: '0.1.45', controlVersion: '0.1.45' });
|
||||
assert.equal(readUpdateCache({ appVersion: '0.2.0', engineVersion: '0.1.45', controlVersion: '0.1.45' }).engine.status, 'available');
|
||||
assert.equal(readUpdateCache({ appVersion: '0.2.0', engineVersion: '0.1.45', controlVersion: '0.1.46' }), null, 'Control changed');
|
||||
assert.equal(readUpdateCache({ appVersion: null, engineVersion: '0.1.45', controlVersion: '0.1.45' }), null, 'app version still loading');
|
||||
assert.equal(readUpdateCache({ appVersion: '0.2.0', engineVersion: '0.1.45' }), null, 'Control version not known');
|
||||
});
|
||||
|
||||
test('unreadable or blocked storage is harmless', () => {
|
||||
store.set('strata.update-check.v1', '{not json');
|
||||
assert.equal(readUpdateCache({}), null);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# This is the quick speed check. The standard benchmark (docs/BENCHMARK_PROTOCOL.md), whose results other people can compare with, is `strata bench run`.
|
||||
cd "$(dirname "$0")" || exit 1
|
||||
if [ -x .venv/bin/python ]; then
|
||||
echo "Quick speed check. For results other people can compare with (the standard benchmark, 20 to 30 minutes), run: .venv/bin/python strata.py bench run"
|
||||
echo "Quick speed check. For results other people can compare with (the standard benchmark, a few minutes), run: .venv/bin/python strata.py bench run"
|
||||
echo
|
||||
exec .venv/bin/python tools/strata_bench.py "$@"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# First real `strata bench run` on the released 0.2.1 (terminal 0.2.1, engine 0.1.46 cuda12 Linux packages)
|
||||
|
||||
Model Qwen3.8-Flash-Next IQ3_XXS (hand-written run config that mirrors setup: int8 KV, MTP spec 4; z440 layer split auto, P2003 resident experts),
|
||||
`strata bench run --profile` (the runner starts and stops the engine), protocol `strata-qwen38-thinking-v1-draft.1`, track `custom`. Dated 2026-10-20.
|
||||
|
||||
| | P2003 (RTX 3060 12 GB, Core Ultra 7 265) | z440 (2x RTX A4000, Xeon E5-2643 v3) |
|
||||
|---|---|---|
|
||||
| `short` decode tok/s (median of 3) | 64.4 | 90.3 |
|
||||
| `prompt-4k` prompt / decode tok/s, TTFT | 962 / 55.4, 4.28 s | 1158 / 74.0, 3.56 s |
|
||||
| `prompt-16k` prompt / decode tok/s, TTFT | 1103 / 56.1, 14.9 s | 1852 / 75.8, 8.9 s |
|
||||
| sanity checks | 12 of 12 | 12 of 12 |
|
||||
| required suite after loading | 3 min 12 s | 2 min 24 s |
|
||||
| optional concurrency (4 at once, server capacity 1), total tok/s | 54.5, 56.4, 56.5 | 72.3, 69.8, 75.4 |
|
||||
|
||||
Numbers are from `run3-with-fixes` on each box (run1 and run2 agree within a few percent). Each box has three runs:
|
||||
|
||||
- `run1-release-archive-as-shipped`: the release terminal archive untouched. Status `nonconforming`: the archive has no `COMMIT` file.
|
||||
- `run2-with-commit-and-optional-concurrency`: the same plus a `COMMIT` file (the release commit, added by hand) and `--optional-concurrency`. Status `complete`.
|
||||
- `run3-with-fixes`: the bench fixes of this branch overlaid on the release terminal. Status `complete`; storage, runtime, engine log and fresh-load line are filled.
|
||||
(The track note says "box: BOX" because the run script was not given the box name.)
|
||||
|
||||
## What the protocol and the runner got wrong (fixed on this branch unless marked)
|
||||
|
||||
1. The release archive carried no COMMIT, so every release run was nonconforming. `package_terminal.py` takes `--commit` / `STRATA_COMMIT` and refuses to build a distributable archive without one. The release must be rebuilt (or a COMMIT file added) for a conforming result.
|
||||
2. The drive of the model files was not found on an LVM volume (`lsblk -s` nests two levels): "Storage: not measured" on z440.
|
||||
3. No CUDA runtime on P2003: driver 615 prints `CUDA UMD Version`. Also `nvidia-smi` reports the newest CUDA the driver supports (13.0 / 13.4), not the library the engine ran on (12.9). Now: runtime = the engine's build runtime from `engine-component.json`, plus `cuda-driver-supports`.
|
||||
4. `engine.log` in the result was empty (`serve --quiet`). Now the run config's log from the start of the run.
|
||||
5. A freshly started engine was reported as "could not tell whether the server was freshly loaded".
|
||||
6. The protocol said 20 to 30 minutes after loading, unmeasured. Measured: 2 to 4 minutes (plus 1.5 to 2 minutes for the optional stage and 1.5 to 5 minutes of model hashing before the clock starts). Text corrected in the protocol, COMMUNITY_BENCHMARKS.md, BENCH.bat and bench.sh.
|
||||
7. 35 of 36 4K/16K performance requests (and 5 of 18 `short`) stop at the 512-token cap while still reasoning, so "Total s" is the time to the cap and "first answer" is mostly missing. The readouts now carry a `Capped` column and say so. The cap itself is part of the frozen protocol (a change needs a new protocol ID); a larger cap or a non-reasoning performance prompt is the proposal if the owner wants answer times.
|
||||
8. The `short` prompt is about 80 tokens, so its prompt rate (130 to 190 tok/s) is the fixed cost of a request, not a speed. The readouts leave it out (results.json keeps it).
|
||||
9. Not fixed: the runner hashes the model files while the engine is already loaded (RAM is tight on a 46 GB box with a 44 GB model; it worked) and the hash time is outside `required_duration_s`; run-to-run decode varies up to 9 % between runs with the same engine (adaptive expert cache), more than the protocol's min-max spread within one run shows.
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
# Strata standard benchmark: NVIDIA GeForce RTX 3060
|
||||
|
||||
Protocol `strata-qwen38-thinking-v1-draft.1` (draft), run `20261020T081401Z-f0529433`, started 2026-10-20T08:14:01Z. **Status: nonconforming.**
|
||||
|
||||
Written by `strata bench run`. The machine-readable result is [results.json](results.json); every request, its raw usage and timings and the client event offsets are in [outputs.jsonl](outputs.jsonl).
|
||||
|
||||
This run is not a complete standard result:
|
||||
|
||||
- the Strata commit is unknown (not a git checkout and no COMMIT file)
|
||||
|
||||
## Hardware and software
|
||||
|
||||
- GPU: NVIDIA GeForce RTX 3060 (12.0 GiB dedicated)
|
||||
- CPU: Intel(R) Core(TM) Ultra 7 265; RAM: 46.2 GiB
|
||||
- Storage of the model files: nvme Lexar SSD NM1090 PRO 2TB (nvme)
|
||||
- OS: Ubuntu 24.04.5 LTS 7.0.0-38-generic; drivers: gpu-driver 615.71.09; runtime: not measured
|
||||
- Strata commit: `not measured`; engine 0.1.46; release build
|
||||
- Background work: none besides sshd
|
||||
|
||||
## Model and configuration
|
||||
|
||||
- Model: qwen3.8-flash-next-iq3_xxs; quantization IQ3_XXS; revision not measured; source revision not measured
|
||||
- Files: `Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00001-of-00002.gguf` `219ea929900dfa9e...`; `Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00002-of-00002.gguf` `316b46f3a2dbd68c...`; `expert-profile.bin` `8f59b4aa8873209d...`
|
||||
- Track: **custom** (release 0.2.1 terminal+engine 0.1.46 cuda12, hand-written run config mirroring setup defaults (IQ3_XXS, int8 KV, MTP spec 4); box: p2003)
|
||||
- Context 32768 tokens; server capacity 1 slot(s)
|
||||
- Sampling: temperature 1.0, top_p 0.95, top_k 20, min_p 0.0, presence_penalty 0.0, repetition_penalty 1.0, reasoning_effort xhigh; thinking on, preserved
|
||||
- Engine options: `config.json`; chat template: `provenance/chat_template.jinja`
|
||||
|
||||
## Method
|
||||
|
||||
The frozen requests of `bench/prompts/standard-v1` (corpus `e75eff8b9feea04f...`) were sent over loopback HTTP, streaming, one at a time, in this order: 1 warm-up (excluded), 3 repetitions of the short, 4K and 16K prompts, and the twelve sanity checks. Prompt and decode speed are the engine's own `timings`; time to first token is the client's monotonic clock from dispatch to the first nonempty reasoning, answer or tool-call delta. No request was retried. Time after the model loaded: 3 min 16 s.
|
||||
|
||||
## Results
|
||||
|
||||
| Case | Prompt tok/s | Decode tok/s | TTFT s | First answer s | Total s |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| short | 184.2 (181.8-194.6) n=3 | 60.6 (59.1-62.7) n=3 | 0.45 (0.42-0.45) n=3 | 2.17 (2.11-2.94) n=3 | 6.7 (5.4-8.6) n=3 |
|
||||
| prompt-4k | 962.1 (960.4-968.8) n=3 | 55.4 (51.2-57.4) n=3 | 4.28 (4.26-4.29) n=3 | 11.19 (10.15-12.24) n=2 | 13.5 (13.2-14.2) n=3 |
|
||||
| prompt-16k | 1103.4 (1103.3-1106.9) n=3 | 54.1 (52.8-55.0) n=3 | 14.88 (14.84-14.88) n=3 | 21.05 (21.05-21.05) n=1 | 24.3 (24.2-24.6) n=3 |
|
||||
|
||||
Cells are median (min-max) n=samples.
|
||||
|
||||
| Phase | Planned | Completed | Truncated | Failed | Skipped |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| warmup | 1 | 0 | 1 | 0 | 0 |
|
||||
| performance | 9 | 2 | 7 | 0 | 0 |
|
||||
| sanity | 12 | 12 | 0 | 0 | 0 |
|
||||
| concurrency | 0 | 0 | 0 | 0 | 0 |
|
||||
|
||||
Sanity checks: **12 of 12**.
|
||||
|
||||
| Case | Result | Reason |
|
||||
| --- | --- | --- |
|
||||
| I1 | pass | the expected JSON value |
|
||||
| I2 | pass | the expected JSON value |
|
||||
| I3 | pass | the expected JSON value |
|
||||
| I4 | pass | the exact expected text |
|
||||
| R1 | pass | the exact expected text |
|
||||
| R2 | pass | the exact expected text |
|
||||
| R3 | pass | the exact expected text |
|
||||
| R4 | pass | the expected JSON value |
|
||||
| T1 | pass | the expected tool call |
|
||||
| T2 | pass | the expected tool call |
|
||||
| T3 | pass | the expected tool call |
|
||||
| T4 | pass | the expected tool call |
|
||||
|
||||
## Failures, exclusions and limitations
|
||||
|
||||
- No request failed.
|
||||
- Excluded from speed summaries: performance-prompt-16k-r1: first_answer_s is not available for this request
|
||||
- Excluded from speed summaries: performance-prompt-16k-r3: first_answer_s is not available for this request
|
||||
- Excluded from speed summaries: performance-prompt-4k-r1: first_answer_s is not available for this request
|
||||
- Prompt-prefix reuse is the shared template prefix only; the cache was not emptied between requests, and the expert cache stays warm and evolves. These are not cold-model measurements.
|
||||
- Output caps include reasoning; a request that reaches its cap is `truncated` and fails its sanity check.
|
||||
- The runner cannot see other programs on the machine, the repeat-stop and loop-recovery switches of the server, or the quantization repository's revision; see `missing` in results.json for every value it could not read.
|
||||
- The runner could not tell whether the server was freshly loaded.
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"engine_options": {
|
||||
"--pack": "iq3_xxs",
|
||||
"--native": "Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00001-of-00002.gguf",
|
||||
"--ple-gguf": "Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00002-of-00002.gguf",
|
||||
"--expert-profile": "expert-profile.bin",
|
||||
"--expert-cache": "auto",
|
||||
"--prefill": "auto",
|
||||
"--spec": "4",
|
||||
"--spec-min-p": "0.5",
|
||||
"--mtp": "rt",
|
||||
"--max-context": "32768",
|
||||
"--kv": "int8",
|
||||
"--resident-experts": true,
|
||||
"--pool-workers": "13"
|
||||
},
|
||||
"other_engine_options": {
|
||||
"--pack": "iq3_xxs",
|
||||
"--native": "Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00001-of-00002.gguf",
|
||||
"--ple-gguf": "Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00002-of-00002.gguf",
|
||||
"--max-context": "32768",
|
||||
"--resident-experts": true
|
||||
},
|
||||
"server_defaults": {
|
||||
"temperature": 1.0,
|
||||
"top_p": 0.95,
|
||||
"top_k": 20,
|
||||
"n_predict": -1
|
||||
},
|
||||
"total_slots": 1,
|
||||
"context": 32768,
|
||||
"source": "effective engine arguments of the profile"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{"request_id": "warmup", "phase": "warmup", "case_id": "warmup", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 128, "seed": 1, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}}, "messages": [{"role": "user", "content": "[case warmup]\nExplain how a hash table finds a value, including collisions and one small example."}], "tools": null, "final_answer": "A hash table stores values by mapping a **key** to an index using a hash function.\n\nTo find a value:\n\n1. Hash the key:", "reasoning": "We need answer user's request: \"[case warmup] Explain how a hash table finds a value, including collisions and one small example.\" Need provide concise clear explanation. Need final. Include hash table lookup process: hash key to bucket, compare stored key, collisions handled chaining/open addressing, example. Ensure mention collisions and small example. Maybe case warmup? Could be typo? Answer. Final in English. Need maybe no overcomplicate. Let's craft.\n", "tool_calls": [], "finish_reason": "length", "usage": {"prompt_tokens": 75, "completion_tokens": 128, "total_tokens": 203, "prompt_tokens_details": {"cached_tokens": 0}}, "timings": {"cache_n": 0, "prompt_n": 75, "prompt_ms": 667.2, "prompt_per_token_ms": 8.896, "prompt_per_second": 112.4, "predicted_n": 128, "predicted_ms": 2699.3, "predicted_per_token_ms": 21.088, "predicted_per_second": 47.4, "draft_n": 115, "draft_n_accepted": 63}, "error": null, "http_status": 200, "dispatch_offset_s": 0.0, "end_offset_s": 3.371313, "first_delta": {"offset_s": 0.711867, "kind": "reasoning"}, "first_answer_offset_s": 2.89692, "client_events": [[0.711867, "reasoning"], [0.762308, "reasoning"], [0.763172, "reasoning"], [0.763182, "reasoning"], [0.763186, "reasoning"], [0.815702, "reasoning"], [0.815719, "reasoning"], [0.874093, "reasoning"], [0.929995, "reasoning"], [0.93001, "reasoning"], [0.930015, "reasoning"], [0.930019, "reasoning"], [0.991476, "reasoning"], [0.991492, "reasoning"], [0.991497, "reasoning"], [0.991511, "reasoning"], [1.0435, "reasoning"], [1.043527, "reasoning"], [1.043532, "reasoning"], [1.043535, "reasoning"], [1.130722, "reasoning"], [1.130753, "reasoning"], [1.130758, "reasoning"], [1.130763, "reasoning"], [1.13077, "reasoning"], [1.130774, "reasoning"], [1.21355, "reasoning"], [1.213583, "reasoning"], [1.262127, "reasoning"], [1.262155, "reasoning"], [1.313383, "reasoning"], [1.31341, "reasoning"], [1.362483, "reasoning"], [1.362516, "reasoning"], [1.362526, "reasoning"], [1.362533, "reasoning"], [1.392602, "reasoning"], [1.42122, "reasoning"], [1.452046, "reasoning"], [1.482824, "reasoning"], [1.522728, "reasoning"], [1.561016, "reasoning"], [1.613379, "reasoning"], [1.613402, "reasoning"], [1.666446, "reasoning"], [1.666462, "reasoning"], [1.72185, "reasoning"], [1.774063, "reasoning"], [1.77409, "reasoning"], [1.813882, "reasoning"], [1.84412, "reasoning"], [1.874359, "reasoning"], [1.914627, "reasoning"], [1.943161, "reasoning"], [1.980915, "reasoning"], [2.010585, "reasoning"], [2.051283, "reasoning"], [2.05131, "reasoning"], [2.078719, "reasoning"], [2.11404, "reasoning"], [2.114077, "reasoning"], [2.14384, "reasoning"], [2.196479, "reasoning"], [2.196506, "reasoning"], [2.244964, "reasoning"], [2.288266, "reasoning"], [2.288294, "reasoning"], [2.288299, "reasoning"], [2.317158, "reasoning"], [2.348043, "reasoning"], [2.388966, "reasoning"], [2.389031, "reasoning"], [2.389057, "reasoning"], [2.428461, "reasoning"], [2.42849, "reasoning"], [2.465871, "reasoning"], [2.4659, "reasoning"], [2.493863, "reasoning"], [2.543276, "reasoning"], [2.577326, "reasoning"], [2.631813, "reasoning"], [2.631839, "reasoning"], [2.631844, "reasoning"], [2.659288, "reasoning"], [2.685582, "reasoning"], [2.714737, "reasoning"], [2.743481, "reasoning"], [2.785839, "reasoning"], [2.824821, "reasoning"], [2.824846, "reasoning"], [2.859159, "reasoning"], [2.859184, "reasoning"], [2.859192, "reasoning"], [2.896889, "reasoning"], [2.89692, "answer"], [2.933777, "answer"], [2.933805, "answer"], [2.933813, "answer"], [2.969063, "answer"], [3.00201, "answer"], [3.043991, "answer"], [3.086385, "answer"], [3.125819, "answer"], [3.16729, "answer"], [3.167316, "answer"], [3.167321, "answer"], [3.167324, "answer"], [3.206636, "answer"], [3.206667, "answer"], [3.206678, "answer"], [3.206682, "answer"], [3.249334, "answer"], [3.249365, "answer"], [3.249375, "answer"], [3.290843, "answer"], [3.290866, "answer"], [3.290873, "answer"], [3.290877, "answer"], [3.330395, "answer"], [3.330422, "answer"], [3.330429, "answer"], [3.370651, "answer"], [3.370677, "answer"], [3.370683, "answer"]]}
|
||||
{"request_id": "performance-short-r1", "phase": "performance", "case_id": "short", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 512, "seed": 1, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}}, "messages": [{"role": "user", "content": "[case performance-short-r1]\nExplain how a hash table finds a value, including collisions and one small example."}], "tools": null, "final_answer": "A hash table finds a value by turning the **key** into a table index using a **hash function**.\n\nExample: suppose we want to find the value associated with `\"user123\"`.\n\n1. **Hash the key**: \n `hash(\"user123\") = 4` \n So the table looks at index `4`.\n\n2. **Check the bucket at index 4**:\n - If it contains `\"user123\"`, compare keys to confirm a match.\n - If the bucket contains multiple entries because different keys hashed to the same index, follow the bucket’s chain or search within that bucket.\n\n3. **Return the value** once the matching key is found.\n\nCollisions happen when two different keys hash to the same index. For example:\n\n```text\nhash(\"A\") = 4\nhash(\"B\") = 4\n```\n\nBoth keys go to index `4`. If the table uses chaining, index `4` might hold a list:\n\n```text\n4: [\"A\", \"B\"]\n```\n\nTo find `\"B\"`, the hash table hashes `\"B\"` to index `4`, scans that bucket, compares the key to `\"A\"` first, sees it does not match, then compares to `\"B\"`, finds a match, and returns its value.", "reasoning": "We need answer user: \"Explain how a hash table finds a value, including collisions and one small example.\" Simple. Now produce final. Need concise but thorough. Explain hashing key to index, buckets, chaining/open addressing, collisions, search steps, example maybe insert key-value pairs, hash to index, collision at same index, chain. Mention equality checks, load factor maybe resizing. Ensure \"one small example\". No special formatting maybe bullets.\n", "tool_calls": [], "finish_reason": "stop", "usage": {"prompt_tokens": 77, "completion_tokens": 374, "total_tokens": 451, "prompt_tokens_details": {"cached_tokens": 0}}, "timings": {"cache_n": 0, "prompt_n": 77, "prompt_ms": 395.6, "prompt_per_token_ms": 5.138, "prompt_per_second": 194.6, "predicted_n": 374, "predicted_ms": 6333.3, "predicted_per_token_ms": 16.934, "predicted_per_second": 59.1, "draft_n": 339, "draft_n_accepted": 196}, "error": null, "http_status": 200, "dispatch_offset_s": 0.0, "end_offset_s": 6.733542, "first_delta": {"offset_s": 0.419533, "kind": "reasoning"}, "first_answer_offset_s": 2.16627, "client_events": [[0.419533, "reasoning"], [0.453426, "reasoning"], [0.453444, "reasoning"], [0.453449, "reasoning"], [0.453453, "reasoning"], [0.494347, "reasoning"], [0.494364, "reasoning"], [0.535672, "reasoning"], [0.535688, "reasoning"], [0.535693, "reasoning"], [0.57882, "reasoning"], [0.578837, "reasoning"], [0.578841, "reasoning"], [0.578845, "reasoning"], [0.616392, "reasoning"], [0.616418, "reasoning"], [0.616424, "reasoning"], [0.616428, "reasoning"], [0.655195, "reasoning"], [0.655224, "reasoning"], [0.65523, "reasoning"], [0.655233, "reasoning"], [0.68768, "reasoning"], [0.687705, "reasoning"], [0.724832, "reasoning"], [0.724858, "reasoning"], [0.753844, "reasoning"], [0.790714, "reasoning"], [0.790744, "reasoning"], [0.79075, "reasoning"], [0.790754, "reasoning"], [0.823479, "reasoning"], [0.823507, "reasoning"], [0.856308, "reasoning"], [0.856334, "reasoning"], [0.890418, "reasoning"], [0.929015, "reasoning"], [0.959993, "reasoning"], [0.996323, "reasoning"], [0.996348, "reasoning"], [0.996985, "reasoning"], [1.027805, "reasoning"], [1.027829, "reasoning"], [1.059874, "reasoning"], [1.099683, "reasoning"], [1.099716, "reasoning"], [1.099726, "reasoning"], [1.099733, "reasoning"], [1.125267, "reasoning"], [1.155134, "reasoning"], [1.185901, "reasoning"], [1.22401, "reasoning"], [1.265304, "reasoning"], [1.291834, "reasoning"], [1.317997, "reasoning"], [1.345283, "reasoning"], [1.373295, "reasoning"], [1.399379, "reasoning"], [1.424399, "reasoning"], [1.452371, "reasoning"], [1.479066, "reasoning"], [1.508657, "reasoning"], [1.508683, "reasoning"], [1.534034, "reasoning"], [1.566944, "reasoning"], [1.566971, "reasoning"], [1.600328, "reasoning"], [1.600358, "reasoning"], [1.625349, "reasoning"], [1.650483, "reasoning"], [1.676662, "reasoning"], [1.703429, "reasoning"], [1.733289, "reasoning"], [1.757494, "reasoning"], [1.788458, "reasoning"], [1.788486, "reasoning"], [1.822352, "reasoning"], [1.853033, "reasoning"], [1.853059, "reasoning"], [1.878529, "reasoning"], [1.905368, "reasoning"], [1.945825, "reasoning"], [1.945854, "reasoning"], [1.945871, "reasoning"], [1.97114, "reasoning"], [2.002974, "reasoning"], [2.038974, "reasoning"], [2.038999, "reasoning"], [2.08195, "reasLine truncated
|
||||
{"request_id": "performance-prompt-4k-r1", "phase": "performance", "case_id": "prompt-4k", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 512, "seed": 1, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}}, "messages": [{"role": "user", "content": "[case performance-prompt-4k-r1]\nrecord000001: alpha beta gamma delta epsilon zeta eta theta.\nrecord000002: alpha beta gamma delta epsilon zeta eta theta.\nrecord000003: alpha beta gamma delta epsilon zeta eta theta.\nrecord000004: alpha beta gamma delta epsilon zeta eta theta.\nrecord000005: alpha beta gamma delta epsilon zeta eta theta.\nrecord000006: alpha beta gamma delta epsilon zeta eta theta.\nrecord000007: alpha beta gamma delta epsilon zeta eta theta.\nrecord000008: alpha beta gamma delta epsilon zeta eta theta.\nrecord000009: alpha beta gamma delta epsilon zeta eta theta.\nrecord000010: alpha beta gamma delta epsilon zeta eta theta.\nrecord000011: alpha beta gamma delta epsilon zeta eta theta.\nrecord000012: alpha beta gamma delta epsilon zeta eta theta.\nrecord000013: alpha beta gamma delta epsilon zeta eta theta.\nrecord000014: alpha beta gamma delta epsilon zeta eta theta.\nrecord000015: alpha beta gamma delta epsilon zeta eta theta.\nrecord000016: alpha beta gamma delta epsilon zeta eta theta.\nrecord000017: alpha beta gamma delta epsilon zeta eta theta.\nrecord000018: alpha beta gamma delta epsilon zeta eta theta.\nrecord000019: alpha beta gamma delta epsilon zeta eta theta.\nrecord000020: alpha beta gamma delta epsilon zeta eta theta.\nrecord000021: alpha beta gamma delta epsilon zeta eta theta.\nrecord000022: alpha beta gamma delta epsilon zeta eta theta.\nrecord000023: alpha beta gamma delta epsilon zeta eta theta.\nrecord000024: alpha beta gamma delta epsilon zeta eta theta.\nrecord000025: alpha beta gamma delta epsilon zeta eta theta.\nrecord000026: alpha beta gamma delta epsilon zeta eta theta.\nrecord000027: alpha beta gamma delta epsilon zeta eta theta.\nrecord000028: alpha beta gamma delta epsilon zeta eta theta.\nrecord000029: alpha beta gamma delta epsilon zeta eta theta.\nrecord000030: alpha beta gamma delta epsilon zeta eta theta.\nrecord000031: alpha beta gamma delta epsilon zeta eta theta.\nrecord000032: alpha beta gamma delta epsilon zeta eta theta.\nrecord000033: alpha beta gamma delta epsilon zeta eta theta.\nrecord000034: alpha beta gamma delta epsilon zeta eta theta.\nrecord000035: alpha beta gamma delta epsilon zeta eta theta.\nrecord000036: alpha beta gamma delta epsilon zeta eta theta.\nrecord000037: alpha beta gamma delta epsilon zeta eta theta.\nrecord000038: alpha beta gamma delta epsilon zeta eta theta.\nrecord000039: alpha beta gamma delta epsilon zeta eta theta.\nrecord000040: alpha beta gamma delta epsilon zeta eta theta.\nrecord000041: alpha beta gamma delta epsilon zeta eta theta.\nrecord000042: alpha beta gamma delta epsilon zeta eta theta.\nrecord000043: alpha beta gamma delta epsilon zeta eta theta.\nrecord000044: alpha beta gamma delta epsilon zeta eta theta.\nrecord000045: alpha beta gamma delta epsilon zeta eta theta.\nrecord000046: alpha beta gamma delta epsilon zeta eta theta.\nrecord000047: alpha beta gamma delta epsilon zeta eta theta.\nrecord000048: alpha beta gamma delta epsilon zeta eta theta.\nrecord000049: alpha beta gamma delta epsilon zeta eta theta.\nrecord000050: alpha beta gamma delta epsilon zeta eta theta.\nrecord000051: alpha beta gamma delta epsilon zeta eta theta.\nrecord000052: alpha beta gamma delta epsilon zeta eta theta.\nrecord000053: alpha beta gamma delta epsilon zeta eta theta.\nrecord000054: alpha beta gamma delta epsilon zeta eta theta.\nrecord000055: alpha beta gamma delta epsilon zeta eta theta.\nrecord000056: alpha beta gamma delta epsilon zeta eta theta.\nrecord000057: alpha beta gamma delta epsilon zeta eta theta.\nrecord000058: alpha beta gamma delta epsilon zeta eta theta.\nrecord000059: alpha beta gamma delta epsilon zeta eta theta.\nrecord000060: alpha beta gamma delta epsilon zeta eta theta.\nrecord000061: alpha beta gamma delta epsilon zeta eta theta.\nrecord000062: alpha beta gamma delta epsilon zeta eta theta.\nrecord000063: alpha beta gamma delta epsilon zeta eta theta.\nrecord000064: alpha beta gamma delta epsilon zeta eta theta.\nrecord000065: alpha beta gamma delta epsilon zeta eta theta.\nrecord000066: alpha beta gamma delta epsilon zeta eta theta.\nrecord000067: alpha beta gamma delta epsilon zeta eta theta.\nrecord000068: alpha beta gamma delta epsilon zeta eta theta.\nrecord000069: alpha beta gamma delta epsilon zeta eta theta.\nrecord000070: alpha beta gamma delta epsilon zeta eta theta.\nrecord000071: alpha beta gamma delta epsilon zeta eta theta.\nrecord000072: alpha beta gamma delta epsilon zeta eta theta.\nrecord000073: alLine truncated
|
||||
{"request_id": "performance-prompt-16k-r1", "phase": "performance", "case_id": "prompt-16k", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 512, "seed": 1, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}}, "messages": [{"role": "user", "content": "[case performance-prompt-16k-r1]\nrecord000001: alpha beta gamma delta epsilon zeta eta theta.\nrecord000002: alpha beta gamma delta epsilon zeta eta theta.\nrecord000003: alpha beta gamma delta epsilon zeta eta theta.\nrecord000004: alpha beta gamma delta epsilon zeta eta theta.\nrecord000005: alpha beta gamma delta epsilon zeta eta theta.\nrecord000006: alpha beta gamma delta epsilon zeta eta theta.\nrecord000007: alpha beta gamma delta epsilon zeta eta theta.\nrecord000008: alpha beta gamma delta epsilon zeta eta theta.\nrecord000009: alpha beta gamma delta epsilon zeta eta theta.\nrecord000010: alpha beta gamma delta epsilon zeta eta theta.\nrecord000011: alpha beta gamma delta epsilon zeta eta theta.\nrecord000012: alpha beta gamma delta epsilon zeta eta theta.\nrecord000013: alpha beta gamma delta epsilon zeta eta theta.\nrecord000014: alpha beta gamma delta epsilon zeta eta theta.\nrecord000015: alpha beta gamma delta epsilon zeta eta theta.\nrecord000016: alpha beta gamma delta epsilon zeta eta theta.\nrecord000017: alpha beta gamma delta epsilon zeta eta theta.\nrecord000018: alpha beta gamma delta epsilon zeta eta theta.\nrecord000019: alpha beta gamma delta epsilon zeta eta theta.\nrecord000020: alpha beta gamma delta epsilon zeta eta theta.\nrecord000021: alpha beta gamma delta epsilon zeta eta theta.\nrecord000022: alpha beta gamma delta epsilon zeta eta theta.\nrecord000023: alpha beta gamma delta epsilon zeta eta theta.\nrecord000024: alpha beta gamma delta epsilon zeta eta theta.\nrecord000025: alpha beta gamma delta epsilon zeta eta theta.\nrecord000026: alpha beta gamma delta epsilon zeta eta theta.\nrecord000027: alpha beta gamma delta epsilon zeta eta theta.\nrecord000028: alpha beta gamma delta epsilon zeta eta theta.\nrecord000029: alpha beta gamma delta epsilon zeta eta theta.\nrecord000030: alpha beta gamma delta epsilon zeta eta theta.\nrecord000031: alpha beta gamma delta epsilon zeta eta theta.\nrecord000032: alpha beta gamma delta epsilon zeta eta theta.\nrecord000033: alpha beta gamma delta epsilon zeta eta theta.\nrecord000034: alpha beta gamma delta epsilon zeta eta theta.\nrecord000035: alpha beta gamma delta epsilon zeta eta theta.\nrecord000036: alpha beta gamma delta epsilon zeta eta theta.\nrecord000037: alpha beta gamma delta epsilon zeta eta theta.\nrecord000038: alpha beta gamma delta epsilon zeta eta theta.\nrecord000039: alpha beta gamma delta epsilon zeta eta theta.\nrecord000040: alpha beta gamma delta epsilon zeta eta theta.\nrecord000041: alpha beta gamma delta epsilon zeta eta theta.\nrecord000042: alpha beta gamma delta epsilon zeta eta theta.\nrecord000043: alpha beta gamma delta epsilon zeta eta theta.\nrecord000044: alpha beta gamma delta epsilon zeta eta theta.\nrecord000045: alpha beta gamma delta epsilon zeta eta theta.\nrecord000046: alpha beta gamma delta epsilon zeta eta theta.\nrecord000047: alpha beta gamma delta epsilon zeta eta theta.\nrecord000048: alpha beta gamma delta epsilon zeta eta theta.\nrecord000049: alpha beta gamma delta epsilon zeta eta theta.\nrecord000050: alpha beta gamma delta epsilon zeta eta theta.\nrecord000051: alpha beta gamma delta epsilon zeta eta theta.\nrecord000052: alpha beta gamma delta epsilon zeta eta theta.\nrecord000053: alpha beta gamma delta epsilon zeta eta theta.\nrecord000054: alpha beta gamma delta epsilon zeta eta theta.\nrecord000055: alpha beta gamma delta epsilon zeta eta theta.\nrecord000056: alpha beta gamma delta epsilon zeta eta theta.\nrecord000057: alpha beta gamma delta epsilon zeta eta theta.\nrecord000058: alpha beta gamma delta epsilon zeta eta theta.\nrecord000059: alpha beta gamma delta epsilon zeta eta theta.\nrecord000060: alpha beta gamma delta epsilon zeta eta theta.\nrecord000061: alpha beta gamma delta epsilon zeta eta theta.\nrecord000062: alpha beta gamma delta epsilon zeta eta theta.\nrecord000063: alpha beta gamma delta epsilon zeta eta theta.\nrecord000064: alpha beta gamma delta epsilon zeta eta theta.\nrecord000065: alpha beta gamma delta epsilon zeta eta theta.\nrecord000066: alpha beta gamma delta epsilon zeta eta theta.\nrecord000067: alpha beta gamma delta epsilon zeta eta theta.\nrecord000068: alpha beta gamma delta epsilon zeta eta theta.\nrecord000069: alpha beta gamma delta epsilon zeta eta theta.\nrecord000070: alpha beta gamma delta epsilon zeta eta theta.\nrecord000071: alpha beta gamma delta epsilon zeta eta theta.\nrecord000072: alpha beta gamma delta epsilon zeta eta theta.\nrecord000073:Line truncated
|
||||
{"request_id": "performance-short-r2", "phase": "performance", "case_id": "short", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 512, "seed": 2, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}}, "messages": [{"role": "user", "content": "[case performance-short-r2]\nExplain how a hash table finds a value, including collisions and one small example."}], "tools": null, "final_answer": "A hash table finds a value by using a **hash function** to convert a key into an index (or bucket) where the value might be stored.\n\n## How lookup works\n\n1. **Hash the key**\n - The hash table takes the key, such as `\"apple\"`, and runs it through a hash function.\n - The hash function produces a number used to select a bucket.\n\n2. **Go to that bucket**\n - The table stores many buckets, each of which can hold one or more key-value pairs.\n\n3. **Compare keys in the bucket**\n - The hash table checks the stored keys in that bucket.\n - If it finds a matching key, it returns the associated value.\n\n## Collisions\n\nA collision happens when two different keys hash to the same bucket.\n\nFor example, both `\"apple\"` and `\"banana\"` might produce bucket index 3.\n\nHash tables handle collisions using methods such as:\n\n- **Separate chaining**: each bucket stores a small list of key-value pairs.\n- **Open addressing**: the table probes nearby buckets until an empty one is found.\n\nIn most practical hash tables, collisions are expected, so lookup may need to check multiple stored keys inside one bucket.\n\n## Small example\n\nSuppose we have a tiny hash table with 5 buckets and this simple hash function:\n\n```text\nhash(key) = sum of character codes of key mod 5\n```\n\nKeys:\n\n```text\n\"cat\" -> (99 + 97 + 116) mod 5 = 144 mod 5 = 4\n\"dog\" -> (100 + 111 + 103) mod 5 = 314 mod 5 =", "reasoning": "We need answer user: \"Explain how a hash table finds a value, including collisions and one small example.\" Now craft final. Need likely concise but explanatory. Include hashing, buckets, collisions chain/open addressing, example maybe keys strings. Ensure accurate: hash function maps key to bucket, find: compute hash, go bucket, compare keys stored; if collision multiple entries in same bucket, search bucket list or probe sequence. Example small: hash table size 5; hash(\"apple\")=2, hash(\"cat\")=2; insert; get(\"cat\") check bucket 2 keys. Maybe mention load factor. Let's write final with headings. Ensure not too long.\n", "tool_calls": [], "finish_reason": "length", "usage": {"prompt_tokens": 77, "completion_tokens": 512, "total_tokens": 589, "prompt_tokens_details": {"cached_tokens": 0}}, "timings": {"cache_n": 0, "prompt_n": 77, "prompt_ms": 418.1, "prompt_per_token_ms": 5.43, "prompt_per_second": 184.2, "predicted_n": 512, "predicted_ms": 8163.0, "predicted_per_token_ms": 15.943, "predicted_per_second": 62.7, "draft_n": 450, "draft_n_accepted": 290}, "error": null, "http_status": 200, "dispatch_offset_s": 0.0, "end_offset_s": 8.585731, "first_delta": {"offset_s": 0.445552, "kind": "reasoning"}, "first_answer_offset_s": 2.944832, "client_events": [[0.445552, "reasoning"], [0.487494, "reasoning"], [0.487511, "reasoning"], [0.487515, "reasoning"], [0.48752, "reasoning"], [0.53426, "reasoning"], [0.534276, "reasoning"], [0.58038, "reasoning"], [0.580396, "reasoning"], [0.580401, "reasoning"], [0.629146, "reasoning"], [0.629162, "reasoning"], [0.629166, "reasoning"], [0.62917, "reasoning"], [0.672148, "reasoning"], [0.672176, "reasoning"], [0.672181, "reasoning"], [0.672185, "reasoning"], [0.715161, "reasoning"], [0.715187, "reasoning"], [0.715193, "reasoning"], [0.715196, "reasoning"], [0.750838, "reasoning"], [0.750868, "reasoning"], [0.778983, "reasoning"], [0.817614, "reasoning"], [0.817667, "reasoning"], [0.817691, "reasoning"], [0.817702, "reasoning"], [0.851631, "reasoning"], [0.851676, "reasoning"], [0.851696, "reasoning"], [0.882854, "reasoning"], [0.882881, "reasoning"], [0.912384, "reasoning"], [0.948565, "reasoning"], [0.9486, "reasoning"], [0.978211, "reasoning"], [0.978242, "reasoning"], [1.009638, "reasoning"], [1.046604, "reasoning"], [1.083813, "reasoning"], [1.083841, "reasoning"], [1.083847, "reasoning"], [1.120449, "reasoning"], [1.14612, "reasoning"], [1.190095, "reasoning"], [1.213015, "reasoning"], [1.23861, "reasoning"], [1.262685, "reasoning"], [1.295583, "reasoning"], [1.326797, "reasoning"], [1.326827, "reasoning"], [1.354025, "reasoning"], [1.39318, "reasoning"], [1.393203, "reasoning"], [1.393212, "reasoning"], [1.425195, "reasoning"], [1.425213, "reasoning"], [1.453207, "reasoning"], [1.491719, "reasoning"], [1.491774, "reasoning"], [1.491797, "reasoning"], [1.491809, "reasoning"], [1.531505, "reasoning"], [1.567174, "reasoning"], [1.567205, "reaLine truncated
|
||||
{"request_id": "performance-prompt-4k-r2", "phase": "performance", "case_id": "prompt-4k", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 512, "seed": 2, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}}, "messages": [{"role": "user", "content": "[case performance-prompt-4k-r2]\nrecord000001: alpha beta gamma delta epsilon zeta eta theta.\nrecord000002: alpha beta gamma delta epsilon zeta eta theta.\nrecord000003: alpha beta gamma delta epsilon zeta eta theta.\nrecord000004: alpha beta gamma delta epsilon zeta eta theta.\nrecord000005: alpha beta gamma delta epsilon zeta eta theta.\nrecord000006: alpha beta gamma delta epsilon zeta eta theta.\nrecord000007: alpha beta gamma delta epsilon zeta eta theta.\nrecord000008: alpha beta gamma delta epsilon zeta eta theta.\nrecord000009: alpha beta gamma delta epsilon zeta eta theta.\nrecord000010: alpha beta gamma delta epsilon zeta eta theta.\nrecord000011: alpha beta gamma delta epsilon zeta eta theta.\nrecord000012: alpha beta gamma delta epsilon zeta eta theta.\nrecord000013: alpha beta gamma delta epsilon zeta eta theta.\nrecord000014: alpha beta gamma delta epsilon zeta eta theta.\nrecord000015: alpha beta gamma delta epsilon zeta eta theta.\nrecord000016: alpha beta gamma delta epsilon zeta eta theta.\nrecord000017: alpha beta gamma delta epsilon zeta eta theta.\nrecord000018: alpha beta gamma delta epsilon zeta eta theta.\nrecord000019: alpha beta gamma delta epsilon zeta eta theta.\nrecord000020: alpha beta gamma delta epsilon zeta eta theta.\nrecord000021: alpha beta gamma delta epsilon zeta eta theta.\nrecord000022: alpha beta gamma delta epsilon zeta eta theta.\nrecord000023: alpha beta gamma delta epsilon zeta eta theta.\nrecord000024: alpha beta gamma delta epsilon zeta eta theta.\nrecord000025: alpha beta gamma delta epsilon zeta eta theta.\nrecord000026: alpha beta gamma delta epsilon zeta eta theta.\nrecord000027: alpha beta gamma delta epsilon zeta eta theta.\nrecord000028: alpha beta gamma delta epsilon zeta eta theta.\nrecord000029: alpha beta gamma delta epsilon zeta eta theta.\nrecord000030: alpha beta gamma delta epsilon zeta eta theta.\nrecord000031: alpha beta gamma delta epsilon zeta eta theta.\nrecord000032: alpha beta gamma delta epsilon zeta eta theta.\nrecord000033: alpha beta gamma delta epsilon zeta eta theta.\nrecord000034: alpha beta gamma delta epsilon zeta eta theta.\nrecord000035: alpha beta gamma delta epsilon zeta eta theta.\nrecord000036: alpha beta gamma delta epsilon zeta eta theta.\nrecord000037: alpha beta gamma delta epsilon zeta eta theta.\nrecord000038: alpha beta gamma delta epsilon zeta eta theta.\nrecord000039: alpha beta gamma delta epsilon zeta eta theta.\nrecord000040: alpha beta gamma delta epsilon zeta eta theta.\nrecord000041: alpha beta gamma delta epsilon zeta eta theta.\nrecord000042: alpha beta gamma delta epsilon zeta eta theta.\nrecord000043: alpha beta gamma delta epsilon zeta eta theta.\nrecord000044: alpha beta gamma delta epsilon zeta eta theta.\nrecord000045: alpha beta gamma delta epsilon zeta eta theta.\nrecord000046: alpha beta gamma delta epsilon zeta eta theta.\nrecord000047: alpha beta gamma delta epsilon zeta eta theta.\nrecord000048: alpha beta gamma delta epsilon zeta eta theta.\nrecord000049: alpha beta gamma delta epsilon zeta eta theta.\nrecord000050: alpha beta gamma delta epsilon zeta eta theta.\nrecord000051: alpha beta gamma delta epsilon zeta eta theta.\nrecord000052: alpha beta gamma delta epsilon zeta eta theta.\nrecord000053: alpha beta gamma delta epsilon zeta eta theta.\nrecord000054: alpha beta gamma delta epsilon zeta eta theta.\nrecord000055: alpha beta gamma delta epsilon zeta eta theta.\nrecord000056: alpha beta gamma delta epsilon zeta eta theta.\nrecord000057: alpha beta gamma delta epsilon zeta eta theta.\nrecord000058: alpha beta gamma delta epsilon zeta eta theta.\nrecord000059: alpha beta gamma delta epsilon zeta eta theta.\nrecord000060: alpha beta gamma delta epsilon zeta eta theta.\nrecord000061: alpha beta gamma delta epsilon zeta eta theta.\nrecord000062: alpha beta gamma delta epsilon zeta eta theta.\nrecord000063: alpha beta gamma delta epsilon zeta eta theta.\nrecord000064: alpha beta gamma delta epsilon zeta eta theta.\nrecord000065: alpha beta gamma delta epsilon zeta eta theta.\nrecord000066: alpha beta gamma delta epsilon zeta eta theta.\nrecord000067: alpha beta gamma delta epsilon zeta eta theta.\nrecord000068: alpha beta gamma delta epsilon zeta eta theta.\nrecord000069: alpha beta gamma delta epsilon zeta eta theta.\nrecord000070: alpha beta gamma delta epsilon zeta eta theta.\nrecord000071: alpha beta gamma delta epsilon zeta eta theta.\nrecord000072: alpha beta gamma delta epsilon zeta eta theta.\nrecord000073: alLine truncated
|
||||
{"request_id": "performance-prompt-16k-r2", "phase": "performance", "case_id": "prompt-16k", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 512, "seed": 2, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}}, "messages": [{"role": "user", "content": "[case performance-prompt-16k-r2]\nrecord000001: alpha beta gamma delta epsilon zeta eta theta.\nrecord000002: alpha beta gamma delta epsilon zeta eta theta.\nrecord000003: alpha beta gamma delta epsilon zeta eta theta.\nrecord000004: alpha beta gamma delta epsilon zeta eta theta.\nrecord000005: alpha beta gamma delta epsilon zeta eta theta.\nrecord000006: alpha beta gamma delta epsilon zeta eta theta.\nrecord000007: alpha beta gamma delta epsilon zeta eta theta.\nrecord000008: alpha beta gamma delta epsilon zeta eta theta.\nrecord000009: alpha beta gamma delta epsilon zeta eta theta.\nrecord000010: alpha beta gamma delta epsilon zeta eta theta.\nrecord000011: alpha beta gamma delta epsilon zeta eta theta.\nrecord000012: alpha beta gamma delta epsilon zeta eta theta.\nrecord000013: alpha beta gamma delta epsilon zeta eta theta.\nrecord000014: alpha beta gamma delta epsilon zeta eta theta.\nrecord000015: alpha beta gamma delta epsilon zeta eta theta.\nrecord000016: alpha beta gamma delta epsilon zeta eta theta.\nrecord000017: alpha beta gamma delta epsilon zeta eta theta.\nrecord000018: alpha beta gamma delta epsilon zeta eta theta.\nrecord000019: alpha beta gamma delta epsilon zeta eta theta.\nrecord000020: alpha beta gamma delta epsilon zeta eta theta.\nrecord000021: alpha beta gamma delta epsilon zeta eta theta.\nrecord000022: alpha beta gamma delta epsilon zeta eta theta.\nrecord000023: alpha beta gamma delta epsilon zeta eta theta.\nrecord000024: alpha beta gamma delta epsilon zeta eta theta.\nrecord000025: alpha beta gamma delta epsilon zeta eta theta.\nrecord000026: alpha beta gamma delta epsilon zeta eta theta.\nrecord000027: alpha beta gamma delta epsilon zeta eta theta.\nrecord000028: alpha beta gamma delta epsilon zeta eta theta.\nrecord000029: alpha beta gamma delta epsilon zeta eta theta.\nrecord000030: alpha beta gamma delta epsilon zeta eta theta.\nrecord000031: alpha beta gamma delta epsilon zeta eta theta.\nrecord000032: alpha beta gamma delta epsilon zeta eta theta.\nrecord000033: alpha beta gamma delta epsilon zeta eta theta.\nrecord000034: alpha beta gamma delta epsilon zeta eta theta.\nrecord000035: alpha beta gamma delta epsilon zeta eta theta.\nrecord000036: alpha beta gamma delta epsilon zeta eta theta.\nrecord000037: alpha beta gamma delta epsilon zeta eta theta.\nrecord000038: alpha beta gamma delta epsilon zeta eta theta.\nrecord000039: alpha beta gamma delta epsilon zeta eta theta.\nrecord000040: alpha beta gamma delta epsilon zeta eta theta.\nrecord000041: alpha beta gamma delta epsilon zeta eta theta.\nrecord000042: alpha beta gamma delta epsilon zeta eta theta.\nrecord000043: alpha beta gamma delta epsilon zeta eta theta.\nrecord000044: alpha beta gamma delta epsilon zeta eta theta.\nrecord000045: alpha beta gamma delta epsilon zeta eta theta.\nrecord000046: alpha beta gamma delta epsilon zeta eta theta.\nrecord000047: alpha beta gamma delta epsilon zeta eta theta.\nrecord000048: alpha beta gamma delta epsilon zeta eta theta.\nrecord000049: alpha beta gamma delta epsilon zeta eta theta.\nrecord000050: alpha beta gamma delta epsilon zeta eta theta.\nrecord000051: alpha beta gamma delta epsilon zeta eta theta.\nrecord000052: alpha beta gamma delta epsilon zeta eta theta.\nrecord000053: alpha beta gamma delta epsilon zeta eta theta.\nrecord000054: alpha beta gamma delta epsilon zeta eta theta.\nrecord000055: alpha beta gamma delta epsilon zeta eta theta.\nrecord000056: alpha beta gamma delta epsilon zeta eta theta.\nrecord000057: alpha beta gamma delta epsilon zeta eta theta.\nrecord000058: alpha beta gamma delta epsilon zeta eta theta.\nrecord000059: alpha beta gamma delta epsilon zeta eta theta.\nrecord000060: alpha beta gamma delta epsilon zeta eta theta.\nrecord000061: alpha beta gamma delta epsilon zeta eta theta.\nrecord000062: alpha beta gamma delta epsilon zeta eta theta.\nrecord000063: alpha beta gamma delta epsilon zeta eta theta.\nrecord000064: alpha beta gamma delta epsilon zeta eta theta.\nrecord000065: alpha beta gamma delta epsilon zeta eta theta.\nrecord000066: alpha beta gamma delta epsilon zeta eta theta.\nrecord000067: alpha beta gamma delta epsilon zeta eta theta.\nrecord000068: alpha beta gamma delta epsilon zeta eta theta.\nrecord000069: alpha beta gamma delta epsilon zeta eta theta.\nrecord000070: alpha beta gamma delta epsilon zeta eta theta.\nrecord000071: alpha beta gamma delta epsilon zeta eta theta.\nrecord000072: alpha beta gamma delta epsilon zeta eta theta.\nrecord000073:Line truncated
|
||||
{"request_id": "performance-short-r3", "phase": "performance", "case_id": "short", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 512, "seed": 3, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}}, "messages": [{"role": "user", "content": "[case performance-short-r3]\nExplain how a hash table finds a value, including collisions and one small example."}], "tools": null, "final_answer": "A hash table maps keys to values using a hash function.\n\n1. The key is hashed to get an array index.\n2. It checks that slot for the value.\n3. If the key is there, return its value.\n\nCollisions happen when two keys hash to the same index. They are resolved, for example, with chaining: each slot stores a linked list of entries.\n\nExample:\n\n- Table size: 3\n- `hash(\"apple\") % 3 = 0`\n- `hash(\"cat\") % 3 = 0`\n\nStorage:\n\n```text\nindex 0: (\"apple\", 1) -> (\"cat\", 2)\n```\n\nLookup `\"cat\"`:\n\n1. Hash `\"cat\"` → index 0.\n2. Check entries at index 0.\n3. Find `\"cat\"` → return `2`.\n\nAverage lookup time is about O(1), but worst case is O(n) if many keys collide in one slot.", "reasoning": "We need answer user: \"Explain how a hash table finds a value, including collisions and one small example. [case performance-short-r3]\" Need provide concise but enough. done. Need likely explain hash function, index, lookup, collisions resolved via chaining/open addressing, small example. Maybe mention performance average O(1), worst O(n). Ensure short due case performance-short? Maybe target short. Need no analysis in final? Just answer.\n", "tool_calls": [], "finish_reason": "stop", "usage": {"prompt_tokens": 77, "completion_tokens": 304, "total_tokens": 381, "prompt_tokens_details": {"cached_tokens": 0}}, "timings": {"cache_n": 0, "prompt_n": 77, "prompt_ms": 423.6, "prompt_per_token_ms": 5.501, "prompt_per_second": 181.8, "predicted_n": 304, "predicted_ms": 5013.5, "predicted_per_token_ms": 16.492, "predicted_per_second": 60.6, "draft_n": 290, "draft_n_accepted": 174}, "error": null, "http_status": 200, "dispatch_offset_s": 0.0, "end_offset_s": 5.440577, "first_delta": {"offset_s": 0.448388, "kind": "reasoning"}, "first_answer_offset_s": 2.106216, "client_events": [[0.448388, "reasoning"], [0.4903, "reasoning"], [0.490317, "reasoning"], [0.490322, "reasoning"], [0.490326, "reasoning"], [0.533798, "reasoning"], [0.533814, "reasoning"], [0.578635, "reasoning"], [0.578652, "reasoning"], [0.578657, "reasoning"], [0.621742, "reasoning"], [0.621759, "reasoning"], [0.621764, "reasoning"], [0.621767, "reasoning"], [0.671416, "reasoning"], [0.671444, "reasoning"], [0.671449, "reasoning"], [0.671454, "reasoning"], [0.714872, "reasoning"], [0.714904, "reasoning"], [0.714913, "reasoning"], [0.714917, "reasoning"], [0.750077, "reasoning"], [0.796384, "reasoning"], [0.796407, "reasoning"], [0.796415, "reasoning"], [0.796419, "reasoning"], [0.844526, "reasoning"], [0.844549, "reasoning"], [0.844558, "reasoning"], [0.870593, "reasoning"], [0.911178, "reasoning"], [0.951595, "reasoning"], [0.95163, "reasoning"], [0.999258, "reasoning"], [1.041654, "reasoning"], [1.041674, "reasoning"], [1.083801, "reasoning"], [1.083845, "reasoning"], [1.083866, "reasoning"], [1.118975, "reasoning"], [1.118995, "reasoning"], [1.148486, "reasoning"], [1.186783, "reasoning"], [1.186803, "reasoning"], [1.217346, "reasoning"], [1.217375, "reasoning"], [1.24925, "reasoning"], [1.249273, "reasoning"], [1.285461, "reasoning"], [1.312281, "reasoning"], [1.354444, "reasoning"], [1.354466, "reasoning"], [1.387727, "reasoning"], [1.387761, "reasoning"], [1.41663, "reasoning"], [1.454438, "reasoning"], [1.454477, "reasoning"], [1.454497, "reasoning"], [1.482254, "reasoning"], [1.521552, "reasoning"], [1.52158, "reasoning"], [1.548269, "reasoning"], [1.59012, "reasoning"], [1.590148, "reasoning"], [1.590154, "reasoning"], [1.634592, "reasoning"], [1.63462, "reasoning"], [1.634625, "reasoning"], [1.634629, "reasoning"], [1.667912, "reasoning"], [1.698464, "reasoning"], [1.72899, "reasoning"], [1.756198, "reasoning"], [1.788137, "reasoning"], [1.788165, "reasoning"], [1.815709, "reasoning"], [1.841285, "reasoning"], [1.866235, "reasoning"], [1.891479, "reasoning"], [1.930031, "reasoning"], [1.93006, "reasoning"], [1.930069, "reasoning"], [1.957424, "reasoning"], [1.999075, "reasoning"], [1.999107, "reasoning"], [1.999116, "reasoning"], [2.025028, "reasoning"], [2.067163, "reasoning"], [2.067187, "reasoning"], [2.067195, "reasoning"], [2.106216, "answer"], [2.106253, "answer"], [2.106275, "answer"], [2.145074, "answer"], [2.184788, "answer"], [2.228864, "answer"], [2.228892, "answer"], [2.228897, "answer"], [2.228901, "answer"], [2.266749, "answer"], [2.266776, "answer"], [2.266781, "answer"], [2.306333, "answer"], [2.306361, "aLine truncated
|
||||
{"request_id": "performance-prompt-4k-r3", "phase": "performance", "case_id": "prompt-4k", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 512, "seed": 3, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}}, "messages": [{"role": "user", "content": "[case performance-prompt-4k-r3]\nrecord000001: alpha beta gamma delta epsilon zeta eta theta.\nrecord000002: alpha beta gamma delta epsilon zeta eta theta.\nrecord000003: alpha beta gamma delta epsilon zeta eta theta.\nrecord000004: alpha beta gamma delta epsilon zeta eta theta.\nrecord000005: alpha beta gamma delta epsilon zeta eta theta.\nrecord000006: alpha beta gamma delta epsilon zeta eta theta.\nrecord000007: alpha beta gamma delta epsilon zeta eta theta.\nrecord000008: alpha beta gamma delta epsilon zeta eta theta.\nrecord000009: alpha beta gamma delta epsilon zeta eta theta.\nrecord000010: alpha beta gamma delta epsilon zeta eta theta.\nrecord000011: alpha beta gamma delta epsilon zeta eta theta.\nrecord000012: alpha beta gamma delta epsilon zeta eta theta.\nrecord000013: alpha beta gamma delta epsilon zeta eta theta.\nrecord000014: alpha beta gamma delta epsilon zeta eta theta.\nrecord000015: alpha beta gamma delta epsilon zeta eta theta.\nrecord000016: alpha beta gamma delta epsilon zeta eta theta.\nrecord000017: alpha beta gamma delta epsilon zeta eta theta.\nrecord000018: alpha beta gamma delta epsilon zeta eta theta.\nrecord000019: alpha beta gamma delta epsilon zeta eta theta.\nrecord000020: alpha beta gamma delta epsilon zeta eta theta.\nrecord000021: alpha beta gamma delta epsilon zeta eta theta.\nrecord000022: alpha beta gamma delta epsilon zeta eta theta.\nrecord000023: alpha beta gamma delta epsilon zeta eta theta.\nrecord000024: alpha beta gamma delta epsilon zeta eta theta.\nrecord000025: alpha beta gamma delta epsilon zeta eta theta.\nrecord000026: alpha beta gamma delta epsilon zeta eta theta.\nrecord000027: alpha beta gamma delta epsilon zeta eta theta.\nrecord000028: alpha beta gamma delta epsilon zeta eta theta.\nrecord000029: alpha beta gamma delta epsilon zeta eta theta.\nrecord000030: alpha beta gamma delta epsilon zeta eta theta.\nrecord000031: alpha beta gamma delta epsilon zeta eta theta.\nrecord000032: alpha beta gamma delta epsilon zeta eta theta.\nrecord000033: alpha beta gamma delta epsilon zeta eta theta.\nrecord000034: alpha beta gamma delta epsilon zeta eta theta.\nrecord000035: alpha beta gamma delta epsilon zeta eta theta.\nrecord000036: alpha beta gamma delta epsilon zeta eta theta.\nrecord000037: alpha beta gamma delta epsilon zeta eta theta.\nrecord000038: alpha beta gamma delta epsilon zeta eta theta.\nrecord000039: alpha beta gamma delta epsilon zeta eta theta.\nrecord000040: alpha beta gamma delta epsilon zeta eta theta.\nrecord000041: alpha beta gamma delta epsilon zeta eta theta.\nrecord000042: alpha beta gamma delta epsilon zeta eta theta.\nrecord000043: alpha beta gamma delta epsilon zeta eta theta.\nrecord000044: alpha beta gamma delta epsilon zeta eta theta.\nrecord000045: alpha beta gamma delta epsilon zeta eta theta.\nrecord000046: alpha beta gamma delta epsilon zeta eta theta.\nrecord000047: alpha beta gamma delta epsilon zeta eta theta.\nrecord000048: alpha beta gamma delta epsilon zeta eta theta.\nrecord000049: alpha beta gamma delta epsilon zeta eta theta.\nrecord000050: alpha beta gamma delta epsilon zeta eta theta.\nrecord000051: alpha beta gamma delta epsilon zeta eta theta.\nrecord000052: alpha beta gamma delta epsilon zeta eta theta.\nrecord000053: alpha beta gamma delta epsilon zeta eta theta.\nrecord000054: alpha beta gamma delta epsilon zeta eta theta.\nrecord000055: alpha beta gamma delta epsilon zeta eta theta.\nrecord000056: alpha beta gamma delta epsilon zeta eta theta.\nrecord000057: alpha beta gamma delta epsilon zeta eta theta.\nrecord000058: alpha beta gamma delta epsilon zeta eta theta.\nrecord000059: alpha beta gamma delta epsilon zeta eta theta.\nrecord000060: alpha beta gamma delta epsilon zeta eta theta.\nrecord000061: alpha beta gamma delta epsilon zeta eta theta.\nrecord000062: alpha beta gamma delta epsilon zeta eta theta.\nrecord000063: alpha beta gamma delta epsilon zeta eta theta.\nrecord000064: alpha beta gamma delta epsilon zeta eta theta.\nrecord000065: alpha beta gamma delta epsilon zeta eta theta.\nrecord000066: alpha beta gamma delta epsilon zeta eta theta.\nrecord000067: alpha beta gamma delta epsilon zeta eta theta.\nrecord000068: alpha beta gamma delta epsilon zeta eta theta.\nrecord000069: alpha beta gamma delta epsilon zeta eta theta.\nrecord000070: alpha beta gamma delta epsilon zeta eta theta.\nrecord000071: alpha beta gamma delta epsilon zeta eta theta.\nrecord000072: alpha beta gamma delta epsilon zeta eta theta.\nrecord000073: alLine truncated
|
||||
{"request_id": "performance-prompt-16k-r3", "phase": "performance", "case_id": "prompt-16k", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 512, "seed": 3, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}}, "messages": [{"role": "user", "content": "[case performance-prompt-16k-r3]\nrecord000001: alpha beta gamma delta epsilon zeta eta theta.\nrecord000002: alpha beta gamma delta epsilon zeta eta theta.\nrecord000003: alpha beta gamma delta epsilon zeta eta theta.\nrecord000004: alpha beta gamma delta epsilon zeta eta theta.\nrecord000005: alpha beta gamma delta epsilon zeta eta theta.\nrecord000006: alpha beta gamma delta epsilon zeta eta theta.\nrecord000007: alpha beta gamma delta epsilon zeta eta theta.\nrecord000008: alpha beta gamma delta epsilon zeta eta theta.\nrecord000009: alpha beta gamma delta epsilon zeta eta theta.\nrecord000010: alpha beta gamma delta epsilon zeta eta theta.\nrecord000011: alpha beta gamma delta epsilon zeta eta theta.\nrecord000012: alpha beta gamma delta epsilon zeta eta theta.\nrecord000013: alpha beta gamma delta epsilon zeta eta theta.\nrecord000014: alpha beta gamma delta epsilon zeta eta theta.\nrecord000015: alpha beta gamma delta epsilon zeta eta theta.\nrecord000016: alpha beta gamma delta epsilon zeta eta theta.\nrecord000017: alpha beta gamma delta epsilon zeta eta theta.\nrecord000018: alpha beta gamma delta epsilon zeta eta theta.\nrecord000019: alpha beta gamma delta epsilon zeta eta theta.\nrecord000020: alpha beta gamma delta epsilon zeta eta theta.\nrecord000021: alpha beta gamma delta epsilon zeta eta theta.\nrecord000022: alpha beta gamma delta epsilon zeta eta theta.\nrecord000023: alpha beta gamma delta epsilon zeta eta theta.\nrecord000024: alpha beta gamma delta epsilon zeta eta theta.\nrecord000025: alpha beta gamma delta epsilon zeta eta theta.\nrecord000026: alpha beta gamma delta epsilon zeta eta theta.\nrecord000027: alpha beta gamma delta epsilon zeta eta theta.\nrecord000028: alpha beta gamma delta epsilon zeta eta theta.\nrecord000029: alpha beta gamma delta epsilon zeta eta theta.\nrecord000030: alpha beta gamma delta epsilon zeta eta theta.\nrecord000031: alpha beta gamma delta epsilon zeta eta theta.\nrecord000032: alpha beta gamma delta epsilon zeta eta theta.\nrecord000033: alpha beta gamma delta epsilon zeta eta theta.\nrecord000034: alpha beta gamma delta epsilon zeta eta theta.\nrecord000035: alpha beta gamma delta epsilon zeta eta theta.\nrecord000036: alpha beta gamma delta epsilon zeta eta theta.\nrecord000037: alpha beta gamma delta epsilon zeta eta theta.\nrecord000038: alpha beta gamma delta epsilon zeta eta theta.\nrecord000039: alpha beta gamma delta epsilon zeta eta theta.\nrecord000040: alpha beta gamma delta epsilon zeta eta theta.\nrecord000041: alpha beta gamma delta epsilon zeta eta theta.\nrecord000042: alpha beta gamma delta epsilon zeta eta theta.\nrecord000043: alpha beta gamma delta epsilon zeta eta theta.\nrecord000044: alpha beta gamma delta epsilon zeta eta theta.\nrecord000045: alpha beta gamma delta epsilon zeta eta theta.\nrecord000046: alpha beta gamma delta epsilon zeta eta theta.\nrecord000047: alpha beta gamma delta epsilon zeta eta theta.\nrecord000048: alpha beta gamma delta epsilon zeta eta theta.\nrecord000049: alpha beta gamma delta epsilon zeta eta theta.\nrecord000050: alpha beta gamma delta epsilon zeta eta theta.\nrecord000051: alpha beta gamma delta epsilon zeta eta theta.\nrecord000052: alpha beta gamma delta epsilon zeta eta theta.\nrecord000053: alpha beta gamma delta epsilon zeta eta theta.\nrecord000054: alpha beta gamma delta epsilon zeta eta theta.\nrecord000055: alpha beta gamma delta epsilon zeta eta theta.\nrecord000056: alpha beta gamma delta epsilon zeta eta theta.\nrecord000057: alpha beta gamma delta epsilon zeta eta theta.\nrecord000058: alpha beta gamma delta epsilon zeta eta theta.\nrecord000059: alpha beta gamma delta epsilon zeta eta theta.\nrecord000060: alpha beta gamma delta epsilon zeta eta theta.\nrecord000061: alpha beta gamma delta epsilon zeta eta theta.\nrecord000062: alpha beta gamma delta epsilon zeta eta theta.\nrecord000063: alpha beta gamma delta epsilon zeta eta theta.\nrecord000064: alpha beta gamma delta epsilon zeta eta theta.\nrecord000065: alpha beta gamma delta epsilon zeta eta theta.\nrecord000066: alpha beta gamma delta epsilon zeta eta theta.\nrecord000067: alpha beta gamma delta epsilon zeta eta theta.\nrecord000068: alpha beta gamma delta epsilon zeta eta theta.\nrecord000069: alpha beta gamma delta epsilon zeta eta theta.\nrecord000070: alpha beta gamma delta epsilon zeta eta theta.\nrecord000071: alpha beta gamma delta epsilon zeta eta theta.\nrecord000072: alpha beta gamma delta epsilon zeta eta theta.\nrecord000073:Line truncated
|
||||
{"request_id": "sanity-I1", "phase": "sanity", "case_id": "I1", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 2048, "seed": 1, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}}, "messages": [{"role": "user", "content": "[case sanity-I1]\nReturn only a JSON object with ok set to true and count set to the integer 3. No other keys."}], "tools": null, "final_answer": "{\"ok\":true,\"count\":3}", "reasoning": "We need answer to user: \"[case sanity-I1]\nReturn only a JSON object with ok set to true and count set to the integer 3. No other keys.\"\n\nNeed produce only JSON object. Final only {\"ok\":true,\"count\":3}. Ensure no other keys.\n", "tool_calls": [], "finish_reason": "stop", "usage": {"prompt_tokens": 82, "completion_tokens": 72, "total_tokens": 154, "prompt_tokens_details": {"cached_tokens": 0}}, "timings": {"cache_n": 0, "prompt_n": 82, "prompt_ms": 416.0, "prompt_per_token_ms": 5.073, "prompt_per_second": 197.1, "predicted_n": 72, "predicted_ms": 1028.8, "predicted_per_token_ms": 14.289, "predicted_per_second": 70.0, "draft_n": 64, "draft_n_accepted": 49}, "error": null, "http_status": 200, "dispatch_offset_s": 0.0, "end_offset_s": 1.449242, "first_delta": {"offset_s": 0.442885, "kind": "reasoning"}, "first_answer_offset_s": 1.347978, "client_events": [[0.442885, "reasoning"], [0.480246, "reasoning"], [0.480273, "reasoning"], [0.480278, "reasoning"], [0.524344, "reasoning"], [0.524382, "reasoning"], [0.565282, "reasoning"], [0.607358, "reasoning"], [0.607375, "reasoning"], [0.60738, "reasoning"], [0.649784, "reasoning"], [0.6498, "reasoning"], [0.649805, "reasoning"], [0.64981, "reasoning"], [0.697304, "reasoning"], [0.69733, "reasoning"], [0.697335, "reasoning"], [0.697339, "reasoning"], [0.739488, "reasoning"], [0.739516, "reasoning"], [0.739521, "reasoning"], [0.739524, "reasoning"], [0.783961, "reasoning"], [0.78399, "reasoning"], [0.783996, "reasoning"], [0.784, "reasoning"], [0.829148, "reasoning"], [0.829175, "reasoning"], [0.82918, "reasoning"], [0.829184, "reasoning"], [0.87647, "reasoning"], [0.876499, "reasoning"], [0.876504, "reasoning"], [0.876508, "reasoning"], [0.919645, "reasoning"], [0.919672, "reasoning"], [0.946469, "reasoning"], [0.980225, "reasoning"], [0.980246, "reasoning"], [1.02366, "reasoning"], [1.062101, "reasoning"], [1.062131, "reasoning"], [1.06214, "reasoning"], [1.062144, "reasoning"], [1.087535, "reasoning"], [1.126766, "reasoning"], [1.17472, "reasoning"], [1.174748, "reasoning"], [1.174753, "reasoning"], [1.174757, "reasoning"], [1.21897, "reasoning"], [1.218997, "reasoning"], [1.219008, "reasoning"], [1.219878, "reasoning"], [1.258399, "reasoning"], [1.258425, "reasoning"], [1.258429, "reasoning"], [1.258433, "reasoning"], [1.297565, "reasoning"], [1.297592, "reasoning"], [1.347978, "answer"], [1.348005, "answer"], [1.34801, "answer"], [1.348014, "answer"], [1.395592, "answer"], [1.39562, "answer"], [1.395625, "answer"], [1.395629, "answer"], [1.445545, "answer"]]}
|
||||
{"request_id": "sanity-I2", "phase": "sanity", "case_id": "I2", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 2048, "seed": 1, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}}, "messages": [{"role": "user", "content": "[case sanity-I2]\nSort these integers in ascending order and remove duplicates: 9, 2, -4, 2, 1. Return only a JSON array."}], "tools": null, "final_answer": "[-4,1,2,9]", "reasoning": "We need answer user: \"[case sanity-I2]\nSort these integers in ascending order and remove duplicates: 9, 2, -4, 2, 1. Return only a JSON array.\"\n\nNeed produce final only JSON array. Sort ascending remove duplicates: -4,1,2,9. JSON array: [-4,1,2,9]. Ensure only JSON.\n", "tool_calls": [], "finish_reason": "stop", "usage": {"prompt_tokens": 90, "completion_tokens": 94, "total_tokens": 184, "prompt_tokens_details": {"cached_tokens": 0}}, "timings": {"cache_n": 0, "prompt_n": 90, "prompt_ms": 423.0, "prompt_per_token_ms": 4.7, "prompt_per_second": 212.8, "predicted_n": 94, "predicted_ms": 1325.4, "predicted_per_token_ms": 14.1, "predicted_per_second": 70.9, "draft_n": 87, "draft_n_accepted": 63}, "error": null, "http_status": 200, "dispatch_offset_s": 0.0, "end_offset_s": 1.753054, "first_delta": {"offset_s": 0.4471, "kind": "reasoning"}, "first_answer_offset_s": 1.616198, "client_events": [[0.4471, "reasoning"], [0.481043, "reasoning"], [0.481059, "reasoning"], [0.481064, "reasoning"], [0.481069, "reasoning"], [0.523651, "reasoning"], [0.566825, "reasoning"], [0.566841, "reasoning"], [0.566846, "reasoning"], [0.606715, "reasoning"], [0.606732, "reasoning"], [0.606737, "reasoning"], [0.606742, "reasoning"], [0.651438, "reasoning"], [0.651454, "reasoning"], [0.651469, "reasoning"], [0.651473, "reasoning"], [0.692426, "reasoning"], [0.692452, "reasoning"], [0.692457, "reasoning"], [0.692461, "reasoning"], [0.733354, "reasoning"], [0.733382, "reasoning"], [0.733387, "reasoning"], [0.733391, "reasoning"], [0.777132, "reasoning"], [0.777158, "reasoning"], [0.777164, "reasoning"], [0.777168, "reasoning"], [0.828805, "reasoning"], [0.82883, "reasoning"], [0.828835, "reasoning"], [0.828839, "reasoning"], [0.871725, "reasoning"], [0.871751, "reasoning"], [0.871756, "reasoning"], [0.910068, "reasoning"], [0.910158, "reasoning"], [0.910164, "reasoning"], [0.910167, "reasoning"], [0.952506, "reasoning"], [0.952532, "reasoning"], [0.952537, "reasoning"], [0.952541, "reasoning"], [0.987529, "reasoning"], [0.987555, "reasoning"], [1.026865, "reasoning"], [1.026889, "reasoning"], [1.066512, "reasoning"], [1.06654, "reasoning"], [1.066545, "reasoning"], [1.066549, "reasoning"], [1.093017, "reasoning"], [1.137651, "reasoning"], [1.180098, "reasoning"], [1.221312, "reasoning"], [1.221341, "reasoning"], [1.263192, "reasoning"], [1.26322, "reasoning"], [1.263225, "reasoning"], [1.308634, "reasoning"], [1.308668, "reasoning"], [1.308672, "reasoning"], [1.308676, "reasoning"], [1.346501, "reasoning"], [1.346529, "reasoning"], [1.384348, "reasoning"], [1.384376, "reasoning"], [1.425221, "reasoning"], [1.425248, "reasoning"], [1.425253, "reasoning"], [1.425256, "reasoning"], [1.47001, "reasoning"], [1.470037, "reasoning"], [1.470042, "reasoning"], [1.470046, "reasoning"], [1.511479, "reasoning"], [1.537919, "reasoning"], [1.573731, "reasoning"], [1.573755, "reasoning"], [1.57376, "reasoning"], [1.616172, "reasoning"], [1.616198, "answer"], [1.661088, "answer"], [1.661114, "answer"], [1.661119, "answer"], [1.703693, "answer"], [1.703721, "answer"], [1.703726, "answer"], [1.70373, "answer"], [1.752717, "answer"]]}
|
||||
{"request_id": "sanity-I3", "phase": "sanity", "case_id": "I3", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 2048, "seed": 1, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}}, "messages": [{"role": "user", "content": "[case sanity-I3]\nProject: Orion. Build: 17. Return only a JSON object with keys project (string) and build (integer)."}], "tools": null, "final_answer": "{\"project\":\"Orion\",\"build\":17}", "reasoning": "We need to respond to user: \"[case sanity-I3]\nProject: Orion. Build: 17. Return only a JSON object with keys project (string) and build (integer).\" Need output only JSON object. Done. Need final only {\"project\":\"Orion\",\"build\":17}. Ensure no markdown.\n", "tool_calls": [], "finish_reason": "stop", "usage": {"prompt_tokens": 85, "completion_tokens": 81, "total_tokens": 166, "prompt_tokens_details": {"cached_tokens": 0}}, "timings": {"cache_n": 0, "prompt_n": 85, "prompt_ms": 428.0, "prompt_per_token_ms": 5.035, "prompt_per_second": 198.6, "predicted_n": 81, "predicted_ms": 1092.3, "predicted_per_token_ms": 13.485, "predicted_per_second": 74.2, "draft_n": 79, "draft_n_accepted": 56}, "error": null, "http_status": 200, "dispatch_offset_s": 0.0, "end_offset_s": 1.525548, "first_delta": {"offset_s": 0.452906, "kind": "reasoning"}, "first_answer_offset_s": 1.346157, "client_events": [[0.452906, "reasoning"], [0.486137, "reasoning"], [0.486153, "reasoning"], [0.523083, "reasoning"], [0.523098, "reasoning"], [0.523103, "reasoning"], [0.523107, "reasoning"], [0.562822, "reasoning"], [0.562838, "reasoning"], [0.562842, "reasoning"], [0.562846, "reasoning"], [0.606256, "reasoning"], [0.606272, "reasoning"], [0.606277, "reasoning"], [0.606281, "reasoning"], [0.645328, "reasoning"], [0.645355, "reasoning"], [0.64536, "reasoning"], [0.645363, "reasoning"], [0.684767, "reasoning"], [0.684795, "reasoning"], [0.6848, "reasoning"], [0.684804, "reasoning"], [0.722978, "reasoning"], [0.723004, "reasoning"], [0.723009, "reasoning"], [0.723013, "reasoning"], [0.766745, "reasoning"], [0.766771, "reasoning"], [0.766776, "reasoning"], [0.76678, "reasoning"], [0.804323, "reasoning"], [0.804349, "reasoning"], [0.804354, "reasoning"], [0.804357, "reasoning"], [0.843919, "reasoning"], [0.843947, "reasoning"], [0.843952, "reasoning"], [0.843956, "reasoning"], [0.882744, "reasoning"], [0.925436, "reasoning"], [0.962993, "reasoning"], [0.963018, "reasoning"], [0.999925, "reasoning"], [0.999947, "reasoning"], [0.999955, "reasoning"], [1.02862, "reasoning"], [1.070926, "reasoning"], [1.070949, "reasoning"], [1.070956, "reasoning"], [1.07096, "reasoning"], [1.107627, "reasoning"], [1.147798, "reasoning"], [1.147866, "reasoning"], [1.147894, "reasoning"], [1.190384, "reasoning"], [1.190408, "reasoning"], [1.190415, "reasoning"], [1.190419, "reasoning"], [1.233741, "reasoning"], [1.233763, "reasoning"], [1.23377, "reasoning"], [1.233775, "reasoning"], [1.271981, "reasoning"], [1.272011, "reasoning"], [1.30965, "reasoning"], [1.346134, "reasoning"], [1.346157, "answer"], [1.396589, "answer"], [1.396621, "answer"], [1.39663, "answer"], [1.396635, "answer"], [1.440538, "answer"], [1.440567, "answer"], [1.440571, "answer"], [1.440575, "answer"], [1.48054, "answer"], [1.480569, "answer"]]}
|
||||
{"request_id": "sanity-I4", "phase": "sanity", "case_id": "I4", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 2048, "seed": 1, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}}, "messages": [{"role": "user", "content": "[case sanity-I4]\nReply with exactly the word READY in uppercase and nothing else."}], "tools": null, "final_answer": "READY", "reasoning": "We need to respond to user: \"[case sanity-I4]\nReply with exactly the word READY in uppercase and nothing else.\" Need final exactly READY. No extra. Ensure nothing else.\n", "tool_calls": [], "finish_reason": "stop", "usage": {"prompt_tokens": 71, "completion_tokens": 43, "total_tokens": 114, "prompt_tokens_details": {"cached_tokens": 0}}, "timings": {"cache_n": 0, "prompt_n": 71, "prompt_ms": 389.0, "prompt_per_token_ms": 5.479, "prompt_per_second": 182.5, "predicted_n": 43, "predicted_ms": 604.0, "predicted_per_token_ms": 14.047, "predicted_per_second": 71.2, "draft_n": 36, "draft_n_accepted": 27}, "error": null, "http_status": 200, "dispatch_offset_s": 0.0, "end_offset_s": 0.996883, "first_delta": {"offset_s": 0.411902, "kind": "reasoning"}, "first_answer_offset_s": 0.996466, "client_events": [[0.411902, "reasoning"], [0.437832, "reasoning"], [0.437857, "reasoning"], [0.475216, "reasoning"], [0.475236, "reasoning"], [0.475241, "reasoning"], [0.475247, "reasoning"], [0.505039, "reasoning"], [0.545496, "reasoning"], [0.545513, "reasoning"], [0.545518, "reasoning"], [0.581836, "reasoning"], [0.581852, "reasoning"], [0.581862, "reasoning"], [0.581897, "reasoning"], [0.619372, "reasoning"], [0.619394, "reasoning"], [0.619401, "reasoning"], [0.619405, "reasoning"], [0.660017, "reasoning"], [0.703904, "reasoning"], [0.703924, "reasoning"], [0.703932, "reasoning"], [0.70394, "reasoning"], [0.743644, "reasoning"], [0.743665, "reasoning"], [0.78428, "reasoning"], [0.784303, "reasoning"], [0.78431, "reasoning"], [0.784314, "reasoning"], [0.822986, "reasoning"], [0.823013, "reasoning"], [0.853273, "reasoning"], [0.884613, "reasoning"], [0.884638, "reasoning"], [0.915283, "reasoning"], [0.954387, "reasoning"], [0.954413, "reasoning"], [0.954419, "reasoning"], [0.996466, "answer"]]}
|
||||
{"request_id": "sanity-R1", "phase": "sanity", "case_id": "R1", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 2048, "seed": 1, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}}, "messages": [{"role": "user", "content": "[case sanity-R1]\nrecord000001: alpha beta gamma delta epsilon zeta eta theta.\nrecord000002: alpha beta gamma delta epsilon zeta eta theta.\nrecord000003: alpha beta gamma delta epsilon zeta eta theta.\nrecord000004: alpha beta gamma delta epsilon zeta eta theta.\nrecord000005: alpha beta gamma delta epsilon zeta eta theta.\nrecord000006: alpha beta gamma delta epsilon zeta eta theta.\nrecord000007: alpha beta gamma delta epsilon zeta eta theta.\nrecord000008: alpha beta gamma delta epsilon zeta eta theta.\nrecord000009: alpha beta gamma delta epsilon zeta eta theta.\nrecord000010: alpha beta gamma delta epsilon zeta eta theta.\nrecord000011: alpha beta gamma delta epsilon zeta eta theta.\nrecord000012: alpha beta gamma delta epsilon zeta eta theta.\nrecord000013: alpha beta gamma delta epsilon zeta eta theta.\nrecord000014: alpha beta gamma delta epsilon zeta eta theta.\nrecord000015: alpha beta gamma delta epsilon zeta eta theta.\nrecord000016: alpha beta gamma delta epsilon zeta eta theta.\nrecord000017: alpha beta gamma delta epsilon zeta eta theta.\nrecord000018: alpha beta gamma delta epsilon zeta eta theta.\nrecord000019: alpha beta gamma delta epsilon zeta eta theta.\nrecord000020: alpha beta gamma delta epsilon zeta eta theta.\nrecord000021: alpha beta gamma delta epsilon zeta eta theta.\naccess_code = ORBIT-731\nrecord000022: alpha beta gamma delta epsilon zeta eta theta.\nrecord000023: alpha beta gamma delta epsilon zeta eta theta.\nrecord000024: alpha beta gamma delta epsilon zeta eta theta.\nrecord000025: alpha beta gamma delta epsilon zeta eta theta.\nrecord000026: alpha beta gamma delta epsilon zeta eta theta.\nrecord000027: alpha beta gamma delta epsilon zeta eta theta.\nrecord000028: alpha beta gamma delta epsilon zeta eta theta.\nrecord000029: alpha beta gamma delta epsilon zeta eta theta.\nrecord000030: alpha beta gamma delta epsilon zeta eta theta.\nrecord000031: alpha beta gamma delta epsilon zeta eta theta.\nrecord000032: alpha beta gamma delta epsilon zeta eta theta.\nrecord000033: alpha beta gamma delta epsilon zeta eta theta.\nrecord000034: alpha beta gamma delta epsilon zeta eta theta.\nrecord000035: alpha beta gamma delta epsilon zeta eta theta.\nrecord000036: alpha beta gamma delta epsilon zeta eta theta.\nrecord000037: alpha beta gamma delta epsilon zeta eta theta.\nrecord000038: alpha beta gamma delta epsilon zeta eta theta.\nrecord000039: alpha beta gamma delta epsilon zeta eta theta.\nrecord000040: alpha beta gamma delta epsilon zeta eta theta.\nrecord000041: alpha beta gamma delta epsilon zeta eta theta.\nrecord000042: alpha beta gamma delta epsilon zeta eta theta.\nrecord000043: alpha beta gamma delta epsilon zeta eta theta.\nrecord000044: alpha beta gamma delta epsilon zeta eta theta.\nrecord000045: alpha beta gamma delta epsilon zeta eta theta.\nrecord000046: alpha beta gamma delta epsilon zeta eta theta.\nrecord000047: alpha beta gamma delta epsilon zeta eta theta.\nrecord000048: alpha beta gamma delta epsilon zeta eta theta.\nrecord000049: alpha beta gamma delta epsilon zeta eta theta.\nrecord000050: alpha beta gamma delta epsilon zeta eta theta.\nrecord000051: alpha beta gamma delta epsilon zeta eta theta.\nrecord000052: alpha beta gamma delta epsilon zeta eta theta.\nrecord000053: alpha beta gamma delta epsilon zeta eta theta.\nrecord000054: alpha beta gamma delta epsilon zeta eta theta.\nrecord000055: alpha beta gamma delta epsilon zeta eta theta.\nrecord000056: alpha beta gamma delta epsilon zeta eta theta.\nrecord000057: alpha beta gamma delta epsilon zeta eta theta.\nrecord000058: alpha beta gamma delta epsilon zeta eta theta.\nrecord000059: alpha beta gamma delta epsilon zeta eta theta.\nrecord000060: alpha beta gamma delta epsilon zeta eta theta.\nrecord000061: alpha beta gamma delta epsilon zeta eta theta.\nrecord000062: alpha beta gamma delta epsilon zeta eta theta.\nrecord000063: alpha beta gamma delta epsilon zeta eta theta.\nrecord000064: alpha beta gamma delta epsilon zeta eta theta.\nrecord000065: alpha beta gamma delta epsilon zeta eta theta.\nrecord000066: alpha beta gamma delta epsilon zeta eta theta.\nrecord000067: alpha beta gamma delta epsilon zeta eta theta.\nrecord000068: alpha beta gamma delta epsilon zeta eta theta.\nrecord000069: alpha beta gamma delta epsilon zeta eta theta.\nrecord000070: alpha beta gamma delta epsilon zeta eta theta.\nrecord000071: alpha beta gamma delta epsilon zeta eta theta.\nrecord000072: alpha beta gamma delta epsilon zeta eta theta.\nrecord000073: alpha beta gamma dLine truncated
|
||||
{"request_id": "sanity-R2", "phase": "sanity", "case_id": "R2", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 2048, "seed": 1, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}}, "messages": [{"role": "user", "content": "[case sanity-R2]\nrecord000001: alpha beta gamma delta epsilon zeta eta theta.\nrecord000002: alpha beta gamma delta epsilon zeta eta theta.\nrecord000003: alpha beta gamma delta epsilon zeta eta theta.\nrecord000004: alpha beta gamma delta epsilon zeta eta theta.\nrecord000005: alpha beta gamma delta epsilon zeta eta theta.\nrecord000006: alpha beta gamma delta epsilon zeta eta theta.\nrecord000007: alpha beta gamma delta epsilon zeta eta theta.\nrecord000008: alpha beta gamma delta epsilon zeta eta theta.\nrecord000009: alpha beta gamma delta epsilon zeta eta theta.\nrecord000010: alpha beta gamma delta epsilon zeta eta theta.\nrecord000011: alpha beta gamma delta epsilon zeta eta theta.\nrecord000012: alpha beta gamma delta epsilon zeta eta theta.\nrecord000013: alpha beta gamma delta epsilon zeta eta theta.\nrecord000014: alpha beta gamma delta epsilon zeta eta theta.\nrecord000015: alpha beta gamma delta epsilon zeta eta theta.\nrecord000016: alpha beta gamma delta epsilon zeta eta theta.\nrecord000017: alpha beta gamma delta epsilon zeta eta theta.\nrecord000018: alpha beta gamma delta epsilon zeta eta theta.\nrecord000019: alpha beta gamma delta epsilon zeta eta theta.\nrecord000020: alpha beta gamma delta epsilon zeta eta theta.\nrecord000021: alpha beta gamma delta epsilon zeta eta theta.\nrecord000022: alpha beta gamma delta epsilon zeta eta theta.\nrecord000023: alpha beta gamma delta epsilon zeta eta theta.\nrecord000024: alpha beta gamma delta epsilon zeta eta theta.\nrecord000025: alpha beta gamma delta epsilon zeta eta theta.\nrecord000026: alpha beta gamma delta epsilon zeta eta theta.\nrecord000027: alpha beta gamma delta epsilon zeta eta theta.\nrecord000028: alpha beta gamma delta epsilon zeta eta theta.\nrecord000029: alpha beta gamma delta epsilon zeta eta theta.\nrecord000030: alpha beta gamma delta epsilon zeta eta theta.\nrecord000031: alpha beta gamma delta epsilon zeta eta theta.\nrecord000032: alpha beta gamma delta epsilon zeta eta theta.\nrecord000033: alpha beta gamma delta epsilon zeta eta theta.\nrecord000034: alpha beta gamma delta epsilon zeta eta theta.\nrecord000035: alpha beta gamma delta epsilon zeta eta theta.\nrecord000036: alpha beta gamma delta epsilon zeta eta theta.\nrecord000037: alpha beta gamma delta epsilon zeta eta theta.\nrecord000038: alpha beta gamma delta epsilon zeta eta theta.\nrecord000039: alpha beta gamma delta epsilon zeta eta theta.\nrecord000040: alpha beta gamma delta epsilon zeta eta theta.\nrecord000041: alpha beta gamma delta epsilon zeta eta theta.\nrecord000042: alpha beta gamma delta epsilon zeta eta theta.\nrecord000043: alpha beta gamma delta epsilon zeta eta theta.\nrecord000044: alpha beta gamma delta epsilon zeta eta theta.\nrecord000045: alpha beta gamma delta epsilon zeta eta theta.\nrecord000046: alpha beta gamma delta epsilon zeta eta theta.\nrecord000047: alpha beta gamma delta epsilon zeta eta theta.\nrecord000048: alpha beta gamma delta epsilon zeta eta theta.\nrecord000049: alpha beta gamma delta epsilon zeta eta theta.\nrecord000050: alpha beta gamma delta epsilon zeta eta theta.\nrecord000051: alpha beta gamma delta epsilon zeta eta theta.\nrecord000052: alpha beta gamma delta epsilon zeta eta theta.\nrecord000053: alpha beta gamma delta epsilon zeta eta theta.\nrecord000054: alpha beta gamma delta epsilon zeta eta theta.\nrecord000055: alpha beta gamma delta epsilon zeta eta theta.\nrecord000056: alpha beta gamma delta epsilon zeta eta theta.\nrecord000057: alpha beta gamma delta epsilon zeta eta theta.\nrecord000058: alpha beta gamma delta epsilon zeta eta theta.\nrecord000059: alpha beta gamma delta epsilon zeta eta theta.\nrecord000060: alpha beta gamma delta epsilon zeta eta theta.\nrecord000061: alpha beta gamma delta epsilon zeta eta theta.\nrecord000062: alpha beta gamma delta epsilon zeta eta theta.\nrecord000063: alpha beta gamma delta epsilon zeta eta theta.\nrecord000064: alpha beta gamma delta epsilon zeta eta theta.\nrecord000065: alpha beta gamma delta epsilon zeta eta theta.\nrecord000066: alpha beta gamma delta epsilon zeta eta theta.\nrecord000067: alpha beta gamma delta epsilon zeta eta theta.\nrecord000068: alpha beta gamma delta epsilon zeta eta theta.\nrecord000069: alpha beta gamma delta epsilon zeta eta theta.\nrecord000070: alpha beta gamma delta epsilon zeta eta theta.\nrecord000071: alpha beta gamma delta epsilon zeta eta theta.\nrecord000072: alpha beta gamma delta epsilon zeta eta theta.\nrecord000073: alpha beta gamma delta epsilon zeta eta theLine truncated
|
||||
{"request_id": "sanity-R3", "phase": "sanity", "case_id": "R3", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 2048, "seed": 1, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}}, "messages": [{"role": "user", "content": "[case sanity-R3]\nrecord000001: alpha beta gamma delta epsilon zeta eta theta.\nrecord000002: alpha beta gamma delta epsilon zeta eta theta.\nrecord000003: alpha beta gamma delta epsilon zeta eta theta.\nrecord000004: alpha beta gamma delta epsilon zeta eta theta.\nrecord000005: alpha beta gamma delta epsilon zeta eta theta.\nrecord000006: alpha beta gamma delta epsilon zeta eta theta.\nrecord000007: alpha beta gamma delta epsilon zeta eta theta.\nrecord000008: alpha beta gamma delta epsilon zeta eta theta.\nrecord000009: alpha beta gamma delta epsilon zeta eta theta.\nrecord000010: alpha beta gamma delta epsilon zeta eta theta.\nrecord000011: alpha beta gamma delta epsilon zeta eta theta.\nrecord000012: alpha beta gamma delta epsilon zeta eta theta.\nrecord000013: alpha beta gamma delta epsilon zeta eta theta.\nrecord000014: alpha beta gamma delta epsilon zeta eta theta.\nrecord000015: alpha beta gamma delta epsilon zeta eta theta.\nrecord000016: alpha beta gamma delta epsilon zeta eta theta.\nrecord000017: alpha beta gamma delta epsilon zeta eta theta.\nrecord000018: alpha beta gamma delta epsilon zeta eta theta.\nrecord000019: alpha beta gamma delta epsilon zeta eta theta.\nrecord000020: alpha beta gamma delta epsilon zeta eta theta.\nrecord000021: alpha beta gamma delta epsilon zeta eta theta.\nrecord000022: alpha beta gamma delta epsilon zeta eta theta.\nrecord000023: alpha beta gamma delta epsilon zeta eta theta.\nrecord000024: alpha beta gamma delta epsilon zeta eta theta.\nrecord000025: alpha beta gamma delta epsilon zeta eta theta.\nrecord000026: alpha beta gamma delta epsilon zeta eta theta.\nrecord000027: alpha beta gamma delta epsilon zeta eta theta.\nrecord000028: alpha beta gamma delta epsilon zeta eta theta.\nrecord000029: alpha beta gamma delta epsilon zeta eta theta.\nrecord000030: alpha beta gamma delta epsilon zeta eta theta.\nrecord000031: alpha beta gamma delta epsilon zeta eta theta.\nrecord000032: alpha beta gamma delta epsilon zeta eta theta.\nrecord000033: alpha beta gamma delta epsilon zeta eta theta.\nrecord000034: alpha beta gamma delta epsilon zeta eta theta.\nrecord000035: alpha beta gamma delta epsilon zeta eta theta.\nrecord000036: alpha beta gamma delta epsilon zeta eta theta.\nrecord000037: alpha beta gamma delta epsilon zeta eta theta.\nrecord000038: alpha beta gamma delta epsilon zeta eta theta.\nrecord000039: alpha beta gamma delta epsilon zeta eta theta.\nrecord000040: alpha beta gamma delta epsilon zeta eta theta.\nrecord000041: alpha beta gamma delta epsilon zeta eta theta.\nrecord000042: alpha beta gamma delta epsilon zeta eta theta.\nrecord000043: alpha beta gamma delta epsilon zeta eta theta.\nrecord000044: alpha beta gamma delta epsilon zeta eta theta.\nrecord000045: alpha beta gamma delta epsilon zeta eta theta.\nrecord000046: alpha beta gamma delta epsilon zeta eta theta.\nrecord000047: alpha beta gamma delta epsilon zeta eta theta.\nrecord000048: alpha beta gamma delta epsilon zeta eta theta.\nrecord000049: alpha beta gamma delta epsilon zeta eta theta.\nrecord000050: alpha beta gamma delta epsilon zeta eta theta.\nrecord000051: alpha beta gamma delta epsilon zeta eta theta.\nrecord000052: alpha beta gamma delta epsilon zeta eta theta.\nrecord000053: alpha beta gamma delta epsilon zeta eta theta.\nrecord000054: alpha beta gamma delta epsilon zeta eta theta.\nrecord000055: alpha beta gamma delta epsilon zeta eta theta.\nrecord000056: alpha beta gamma delta epsilon zeta eta theta.\nrecord000057: alpha beta gamma delta epsilon zeta eta theta.\nrecord000058: alpha beta gamma delta epsilon zeta eta theta.\nrecord000059: alpha beta gamma delta epsilon zeta eta theta.\nrecord000060: alpha beta gamma delta epsilon zeta eta theta.\nrecord000061: alpha beta gamma delta epsilon zeta eta theta.\nrecord000062: alpha beta gamma delta epsilon zeta eta theta.\nrecord000063: alpha beta gamma delta epsilon zeta eta theta.\nrecord000064: alpha beta gamma delta epsilon zeta eta theta.\nrecord000065: alpha beta gamma delta epsilon zeta eta theta.\nrecord000066: alpha beta gamma delta epsilon zeta eta theta.\nrecord000067: alpha beta gamma delta epsilon zeta eta theta.\nrecord000068: alpha beta gamma delta epsilon zeta eta theta.\nrecord000069: alpha beta gamma delta epsilon zeta eta theta.\nrecord000070: alpha beta gamma delta epsilon zeta eta theta.\nrecord000071: alpha beta gamma delta epsilon zeta eta theta.\nrecord000072: alpha beta gamma delta epsilon zeta eta theta.\nrecord000073: alpha beta gamma delta epsilon zeta eta theLine truncated
|
||||
{"request_id": "sanity-R4", "phase": "sanity", "case_id": "R4", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 2048, "seed": 1, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}}, "messages": [{"role": "user", "content": "[case sanity-R4]\nrecord000001: alpha beta gamma delta epsilon zeta eta theta.\nrecord000002: alpha beta gamma delta epsilon zeta eta theta.\nrecord000003: alpha beta gamma delta epsilon zeta eta theta.\nrecord000004: alpha beta gamma delta epsilon zeta eta theta.\nrecord000005: alpha beta gamma delta epsilon zeta eta theta.\nrecord000006: alpha beta gamma delta epsilon zeta eta theta.\nrecord000007: alpha beta gamma delta epsilon zeta eta theta.\nrecord000008: alpha beta gamma delta epsilon zeta eta theta.\nrecord000009: alpha beta gamma delta epsilon zeta eta theta.\nrecord000010: alpha beta gamma delta epsilon zeta eta theta.\nrecord000011: alpha beta gamma delta epsilon zeta eta theta.\nrecord000012: alpha beta gamma delta epsilon zeta eta theta.\nrecord000013: alpha beta gamma delta epsilon zeta eta theta.\nrecord000014: alpha beta gamma delta epsilon zeta eta theta.\nrecord000015: alpha beta gamma delta epsilon zeta eta theta.\nrecord000016: alpha beta gamma delta epsilon zeta eta theta.\nrecord000017: alpha beta gamma delta epsilon zeta eta theta.\nrecord000018: alpha beta gamma delta epsilon zeta eta theta.\nrecord000019: alpha beta gamma delta epsilon zeta eta theta.\nrecord000020: alpha beta gamma delta epsilon zeta eta theta.\nrecord000021: alpha beta gamma delta epsilon zeta eta theta.\nrecord000022: alpha beta gamma delta epsilon zeta eta theta.\nrecord000023: alpha beta gamma delta epsilon zeta eta theta.\nrecord000024: alpha beta gamma delta epsilon zeta eta theta.\nrecord000025: alpha beta gamma delta epsilon zeta eta theta.\nrecord000026: alpha beta gamma delta epsilon zeta eta theta.\nrecord000027: alpha beta gamma delta epsilon zeta eta theta.\nrecord000028: alpha beta gamma delta epsilon zeta eta theta.\nrecord000029: alpha beta gamma delta epsilon zeta eta theta.\nrecord000030: alpha beta gamma delta epsilon zeta eta theta.\nrecord000031: alpha beta gamma delta epsilon zeta eta theta.\nrecord000032: alpha beta gamma delta epsilon zeta eta theta.\nrecord000033: alpha beta gamma delta epsilon zeta eta theta.\nrecord000034: alpha beta gamma delta epsilon zeta eta theta.\nrecord000035: alpha beta gamma delta epsilon zeta eta theta.\nrecord000036: alpha beta gamma delta epsilon zeta eta theta.\nrecord000037: alpha beta gamma delta epsilon zeta eta theta.\nrecord000038: alpha beta gamma delta epsilon zeta eta theta.\nrecord000039: alpha beta gamma delta epsilon zeta eta theta.\nrecord000040: alpha beta gamma delta epsilon zeta eta theta.\nrecord000041: alpha beta gamma delta epsilon zeta eta theta.\nrecord000042: alpha beta gamma delta epsilon zeta eta theta.\nrecord000043: alpha beta gamma delta epsilon zeta eta theta.\nrecord000044: alpha beta gamma delta epsilon zeta eta theta.\nrecord000045: alpha beta gamma delta epsilon zeta eta theta.\nrecord000046: alpha beta gamma delta epsilon zeta eta theta.\nrecord000047: alpha beta gamma delta epsilon zeta eta theta.\nrecord000048: alpha beta gamma delta epsilon zeta eta theta.\nrecord000049: alpha beta gamma delta epsilon zeta eta theta.\nrecord000050: alpha beta gamma delta epsilon zeta eta theta.\nrecord000051: alpha beta gamma delta epsilon zeta eta theta.\nrecord000052: alpha beta gamma delta epsilon zeta eta theta.\nrecord000053: alpha beta gamma delta epsilon zeta eta theta.\nrecord000054: alpha beta gamma delta epsilon zeta eta theta.\nrecord000055: alpha beta gamma delta epsilon zeta eta theta.\nrecord000056: alpha beta gamma delta epsilon zeta eta theta.\nrecord000057: alpha beta gamma delta epsilon zeta eta theta.\nrecord000058: alpha beta gamma delta epsilon zeta eta theta.\nrecord000059: alpha beta gamma delta epsilon zeta eta theta.\nrecord000060: alpha beta gamma delta epsilon zeta eta theta.\nrecord000061: alpha beta gamma delta epsilon zeta eta theta.\nrecord000062: alpha beta gamma delta epsilon zeta eta theta.\nrecord000063: alpha beta gamma delta epsilon zeta eta theta.\nrecord000064: alpha beta gamma delta epsilon zeta eta theta.\nrecord000065: alpha beta gamma delta epsilon zeta eta theta.\nrecord000066: alpha beta gamma delta epsilon zeta eta theta.\nrecord000067: alpha beta gamma delta epsilon zeta eta theta.\nrecord000068: alpha beta gamma delta epsilon zeta eta theta.\nrecord000069: alpha beta gamma delta epsilon zeta eta theta.\nrecord000070: alpha beta gamma delta epsilon zeta eta theta.\nrecord000071: alpha beta gamma delta epsilon zeta eta theta.\nrecord000072: alpha beta gamma delta epsilon zeta eta theta.\nrecord000073: alpha beta gamma delta epsilon zeta eta theLine truncated
|
||||
{"request_id": "sanity-T1", "phase": "sanity", "case_id": "T1", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 2048, "seed": 1, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}, "tool_choice": "auto"}, "messages": [{"role": "user", "content": "[case sanity-T1]\nUse lookup_record to look up record A-17."}], "tools": [{"type": "function", "function": {"name": "lookup_record", "description": "Synthetic benchmark lookup; no external action is performed.", "parameters": {"type": "object", "properties": {"id": {"type": "string"}}, "required": ["id"], "additionalProperties": false}}}], "final_answer": "", "reasoning": "We need respond to user: \"Use lookup_record to look up record A-17.\" Need call tool. Simple.\n", "tool_calls": [{"id": "call_edf79b47fea14f34a64c76db", "name": "lookup_record", "arguments": "{\"id\":\"A-17\"}"}], "finish_reason": "tool_calls", "usage": {"prompt_tokens": 325, "completion_tokens": 57, "total_tokens": 382, "prompt_tokens_details": {"cached_tokens": 0}}, "timings": {"cache_n": 0, "prompt_n": 325, "prompt_ms": 823.8, "prompt_per_token_ms": 2.535, "prompt_per_second": 394.5, "predicted_n": 57, "predicted_ms": 712.0, "predicted_per_token_ms": 12.491, "predicted_per_second": 80.1, "draft_n": 48, "draft_n_accepted": 43}, "error": null, "http_status": 200, "dispatch_offset_s": 0.0, "end_offset_s": 1.541336, "first_delta": {"offset_s": 0.85023, "kind": "reasoning"}, "first_answer_offset_s": 1.276112, "client_events": [[0.85023, "reasoning"], [0.889244, "reasoning"], [0.88927, "reasoning"], [0.937, "reasoning"], [0.937017, "reasoning"], [0.937022, "reasoning"], [0.980924, "reasoning"], [0.98094, "reasoning"], [0.980945, "reasoning"], [0.980949, "reasoning"], [1.020816, "reasoning"], [1.020831, "reasoning"], [1.020836, "reasoning"], [1.02084, "reasoning"], [1.059383, "reasoning"], [1.059409, "reasoning"], [1.059414, "reasoning"], [1.059418, "reasoning"], [1.102675, "reasoning"], [1.102703, "reasoning"], [1.102708, "reasoning"], [1.102711, "reasoning"], [1.143557, "reasoning"], [1.143584, "reasoning"], [1.184558, "reasoning"], [1.184587, "reasoning"], [1.276112, "tool_call"], [1.276146, "tool_call"], [1.362988, "tool_call"], [1.36302, "tool_call"], [1.363026, "tool_call"], [1.363031, "tool_call"], [1.405956, "tool_call"], [1.405986, "tool_call"], [1.495075, "tool_call"], [1.495106, "tool_call"]]}
|
||||
{"request_id": "sanity-T2", "phase": "sanity", "case_id": "T2", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 2048, "seed": 1, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}, "tool_choice": "auto"}, "messages": [{"role": "user", "content": "[case sanity-T2]\nUse get_weather for Zagreb in celsius."}], "tools": [{"type": "function", "function": {"name": "get_weather", "description": "Synthetic benchmark lookup; no external action is performed.", "parameters": {"type": "object", "properties": {"city": {"type": "string"}, "unit": {"type": "string"}}, "required": ["city", "unit"], "additionalProperties": false}}}], "final_answer": "", "reasoning": "We need answer user: \"Use get_weather for Zagreb in celsius.\" Need call tool. Then final maybe report result. Need analysis in same language as user's request? User English. We'll think English. Need no need other tools. Call get_weather city Zagreb unit celsius.\n", "tool_calls": [{"id": "call_4106a0f99d9c4274bda4c9ea", "name": "get_weather", "arguments": "{\"city\":\"Zagreb\",\"unit\":\"celsius\"}"}], "finish_reason": "tool_calls", "usage": {"prompt_tokens": 335, "completion_tokens": 104, "total_tokens": 439, "prompt_tokens_details": {"cached_tokens": 0}}, "timings": {"cache_n": 0, "prompt_n": 335, "prompt_ms": 855.4, "prompt_per_token_ms": 2.553, "prompt_per_second": 391.6, "predicted_n": 104, "predicted_ms": 1462.8, "predicted_per_token_ms": 14.065, "predicted_per_second": 71.1, "draft_n": 94, "draft_n_accepted": 70}, "error": null, "http_status": 200, "dispatch_offset_s": 0.0, "end_offset_s": 2.322511, "first_delta": {"offset_s": 0.877752, "kind": "reasoning"}, "first_answer_offset_s": 1.931981, "client_events": [[0.877752, "reasoning"], [0.913336, "reasoning"], [0.913353, "reasoning"], [0.913357, "reasoning"], [0.913361, "reasoning"], [0.955935, "reasoning"], [0.955951, "reasoning"], [0.955955, "reasoning"], [0.955959, "reasoning"], [1.00304, "reasoning"], [1.003055, "reasoning"], [1.045551, "reasoning"], [1.045567, "reasoning"], [1.045572, "reasoning"], [1.045575, "reasoning"], [1.088739, "reasoning"], [1.088766, "reasoning"], [1.088771, "reasoning"], [1.088775, "reasoning"], [1.127049, "reasoning"], [1.127076, "reasoning"], [1.127081, "reasoning"], [1.127084, "reasoning"], [1.153608, "reasoning"], [1.190364, "reasoning"], [1.230407, "reasoning"], [1.230433, "reasoning"], [1.230438, "reasoning"], [1.273648, "reasoning"], [1.27374, "reasoning"], [1.273766, "reasoning"], [1.273785, "reasoning"], [1.315681, "reasoning"], [1.31571, "reasoning"], [1.356622, "reasoning"], [1.35665, "reasoning"], [1.356655, "reasoning"], [1.356659, "reasoning"], [1.390331, "reasoning"], [1.390357, "reasoning"], [1.427814, "reasoning"], [1.427841, "reasoning"], [1.469633, "reasoning"], [1.499341, "reasoning"], [1.499369, "reasoning"], [1.542078, "reasoning"], [1.571254, "reasoning"], [1.597605, "reasoning"], [1.637601, "reasoning"], [1.681307, "reasoning"], [1.681334, "reasoning"], [1.681342, "reasoning"], [1.722673, "reasoning"], [1.722693, "reasoning"], [1.764717, "reasoning"], [1.807504, "reasoning"], [1.807527, "reasoning"], [1.807535, "reasoning"], [1.80754, "reasoning"], [1.851618, "reasoning"], [1.851637, "reasoning"], [1.931981, "tool_call"], [1.932007, "tool_call"], [2.021224, "tool_call"], [2.021248, "tool_call"], [2.021257, "tool_call"], [2.021264, "tool_call"], [2.063666, "tool_call"], [2.152273, "tool_call"], [2.152298, "tool_call"], [2.152307, "tool_call"], [2.19992, "tool_call"], [2.199939, "tool_call"], [2.282532, "tool_call"], [2.282554, "tool_call"]]}
|
||||
{"request_id": "sanity-T3", "phase": "sanity", "case_id": "T3", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 2048, "seed": 1, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}, "tool_choice": "auto"}, "messages": [{"role": "user", "content": "[case sanity-T3]\nUse convert_units to convert 3 km to m."}], "tools": [{"type": "function", "function": {"name": "convert_units", "description": "Synthetic benchmark lookup; no external action is performed.", "parameters": {"type": "object", "properties": {"value": {"type": "number"}, "from_unit": {"type": "string"}, "to_unit": {"type": "string"}}, "required": ["value", "from_unit", "to_unit"], "additionalProperties": false}}}], "final_answer": "", "reasoning": "We need to respond to user: \"[case sanity-T3] Use convert_units to convert 3 km to m.\" Need use tool. Simple. Call convert_units value 3 from km to m. Then final likely result 3000 m. Ensure same language? User English. Final concise.\n", "tool_calls": [{"id": "call_f95792ce9622488dad7f5e7f", "name": "convert_units", "arguments": "{\"value\":3,\"from_unit\":\"km\",\"to_unit\":\"m\"}"}], "finish_reason": "tool_calls", "usage": {"prompt_tokens": 352, "completion_tokens": 117, "total_tokens": 469, "prompt_tokens_details": {"cached_tokens": 0}}, "timings": {"cache_n": 0, "prompt_n": 352, "prompt_ms": 849.7, "prompt_per_token_ms": 2.414, "prompt_per_second": 414.3, "predicted_n": 117, "predicted_ms": 1462.8, "predicted_per_token_ms": 12.503, "predicted_per_second": 80.0, "draft_n": 93, "draft_n_accepted": 80}, "error": null, "http_status": 200, "dispatch_offset_s": 0.0, "end_offset_s": 2.317984, "first_delta": {"offset_s": 0.874021, "kind": "reasoning"}, "first_answer_offset_s": 1.831265, "client_events": [[0.874021, "reasoning"], [0.909056, "reasoning"], [0.909073, "reasoning"], [0.909079, "reasoning"], [0.946601, "reasoning"], [0.946621, "reasoning"], [0.946626, "reasoning"], [0.946631, "reasoning"], [0.987557, "reasoning"], [0.987577, "reasoning"], [0.987582, "reasoning"], [0.987586, "reasoning"], [1.030324, "reasoning"], [1.030351, "reasoning"], [1.071064, "reasoning"], [1.071092, "reasoning"], [1.071098, "reasoning"], [1.071101, "reasoning"], [1.111393, "reasoning"], [1.111424, "reasoning"], [1.111429, "reasoning"], [1.111433, "reasoning"], [1.15366, "reasoning"], [1.153689, "reasoning"], [1.153695, "reasoning"], [1.153699, "reasoning"], [1.191976, "reasoning"], [1.192002, "reasoning"], [1.192008, "reasoning"], [1.219014, "reasoning"], [1.219042, "reasoning"], [1.260302, "reasoning"], [1.260324, "reasoning"], [1.303187, "reasoning"], [1.303218, "reasoning"], [1.303227, "reasoning"], [1.303231, "reasoning"], [1.349144, "reasoning"], [1.38943, "reasoning"], [1.389459, "reasoning"], [1.389468, "reasoning"], [1.389473, "reasoning"], [1.428044, "reasoning"], [1.428072, "reasoning"], [1.454805, "reasoning"], [1.493141, "reasoning"], [1.535702, "reasoning"], [1.535729, "reasoning"], [1.535734, "reasoning"], [1.535737, "reasoning"], [1.571109, "reasoning"], [1.571148, "reasoning"], [1.571159, "reasoning"], [1.603276, "reasoning"], [1.643036, "reasoning"], [1.643064, "reasoning"], [1.643073, "reasoning"], [1.68137, "reasoning"], [1.681397, "reasoning"], [1.681405, "reasoning"], [1.70781, "reasoning"], [1.747544, "reasoning"], [1.74757, "reasoning"], [1.831265, "tool_call"], [1.831298, "tool_call"], [1.915305, "tool_call"], [2.00449, "tool_call"], [2.047337, "tool_call"], [2.047371, "tool_call"], [2.047381, "tool_call"], [2.140775, "tool_call"], [2.18712, "tool_call"], [2.18715, "tool_call"], [2.231676, "tool_call"], [2.317146, "tool_call"], [2.317177, "tool_call"]]}
|
||||
{"request_id": "sanity-T4", "phase": "sanity", "case_id": "T4", "request": {"stream": true, "stream_options": {"include_usage": true}, "max_tokens": 2048, "seed": 1, "temperature": 1.0, "top_p": 0.95, "top_k": 20, "min_p": 0.0, "presence_penalty": 0.0, "repetition_penalty": 1.0, "reasoning_effort": "xhigh", "reasoning_budget_tokens": 0, "chat_template_kwargs": {"enable_thinking": true, "preserve_thinking": true}, "tool_choice": "auto"}, "messages": [{"role": "user", "content": "[case sanity-T4]\nUse get_build_status for project Orion, build 17."}], "tools": [{"type": "function", "function": {"name": "get_build_status", "description": "Synthetic benchmark lookup; no external action is performed.", "parameters": {"type": "object", "properties": {"project": {"type": "string"}, "build": {"type": "integer"}}, "required": ["project", "build"], "additionalProperties": false}}}], "final_answer": "", "reasoning": "We need to answer user's request: \"Use get_build_status for project Orion, build 17.\" Need call tool. Then final likely report.\n", "tool_calls": [{"id": "call_c3a67519d9d342b68db68228", "name": "get_build_status", "arguments": "{\"project\":\"Orion\",\"build\":17}"}], "finish_reason": "tool_calls", "usage": {"prompt_tokens": 339, "completion_tokens": 75, "total_tokens": 414, "prompt_tokens_details": {"cached_tokens": 0}}, "timings": {"cache_n": 0, "prompt_n": 339, "prompt_ms": 839.0, "prompt_per_token_ms": 2.475, "prompt_per_second": 404.1, "predicted_n": 75, "predicted_ms": 877.4, "predicted_per_token_ms": 11.699, "predicted_per_second": 85.5, "draft_n": 55, "draft_n_accepted": 53}, "error": null, "http_status": 200, "dispatch_offset_s": 0.0, "end_offset_s": 1.720559, "first_delta": {"offset_s": 0.863133, "kind": "reasoning"}, "first_answer_offset_s": 1.366401, "client_events": [[0.863133, "reasoning"], [0.898175, "reasoning"], [0.899228, "reasoning"], [0.899237, "reasoning"], [0.938309, "reasoning"], [0.938325, "reasoning"], [0.93833, "reasoning"], [0.938335, "reasoning"], [0.982757, "reasoning"], [0.982773, "reasoning"], [0.982777, "reasoning"], [0.982781, "reasoning"], [1.024311, "reasoning"], [1.024326, "reasoning"], [1.02433, "reasoning"], [1.024345, "reasoning"], [1.065657, "reasoning"], [1.065686, "reasoning"], [1.065691, "reasoning"], [1.065695, "reasoning"], [1.10365, "reasoning"], [1.103683, "reasoning"], [1.103693, "reasoning"], [1.103698, "reasoning"], [1.147164, "reasoning"], [1.147202, "reasoning"], [1.147212, "reasoning"], [1.147218, "reasoning"], [1.168836, "reasoning"], [1.190286, "reasoning"], [1.216994, "reasoning"], [1.249889, "reasoning"], [1.366401, "tool_call"], [1.366436, "tool_call"], [1.411995, "tool_call"], [1.412028, "tool_call"], [1.452321, "tool_call"], [1.452351, "tool_call"], [1.55103, "tool_call"], [1.601584, "tool_call"], [1.682756, "tool_call"], [1.68279, "tool_call"]]}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
{%- set image_count = namespace(value=0) %}
|
||||
{%- set video_count = namespace(value=0) %}
|
||||
{%- macro render_content(content, do_vision_count, is_system_content=false) %}
|
||||
{%- if content is string %}
|
||||
{{- content }}
|
||||
{%- elif content is iterable and content is not mapping %}
|
||||
{%- for item in content %}
|
||||
{%- if 'image' in item or 'image_url' in item or item.type == 'image' %}
|
||||
{%- if is_system_content %}
|
||||
{{- raise_exception('System message cannot contain images.') }}
|
||||
{%- endif %}
|
||||
{%- if do_vision_count %}
|
||||
{%- set image_count.value = image_count.value + 1 %}
|
||||
{%- endif %}
|
||||
{%- if add_vision_id %}
|
||||
{{- 'Picture ' ~ image_count.value ~ ': ' }}
|
||||
{%- endif %}
|
||||
{{- '<|vision_start|><|image_pad|><|vision_end|>' }}
|
||||
{%- elif 'video' in item or item.type == 'video' %}
|
||||
{%- if is_system_content %}
|
||||
{{- raise_exception('System message cannot contain videos.') }}
|
||||
{%- endif %}
|
||||
{%- if do_vision_count %}
|
||||
{%- set video_count.value = video_count.value + 1 %}
|
||||
{%- endif %}
|
||||
{%- if add_vision_id %}
|
||||
{{- 'Video ' ~ video_count.value ~ ': ' }}
|
||||
{%- endif %}
|
||||
{{- '<|vision_start|><|video_pad|><|vision_end|>' }}
|
||||
{%- elif 'text' in item %}
|
||||
{{- item.text }}
|
||||
{%- else %}
|
||||
{{- raise_exception('Unexpected item type in content.') }}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{%- elif content is none or content is undefined %}
|
||||
{{- '' }}
|
||||
{%- else %}
|
||||
{{- raise_exception('Unexpected content type.') }}
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
{%- if not messages %}
|
||||
{{- raise_exception('No messages provided.') }}
|
||||
{%- endif %}
|
||||
{%- set sysns = namespace(count=0, text='') %}
|
||||
{%- for message in messages %}
|
||||
{%- if sysns.count == loop.index0 and (message.role == 'system' or message.role == 'developer') %}
|
||||
{%- set sys_content = render_content(message.content, false, true)|trim %}
|
||||
{%- if sys_content %}
|
||||
{%- set sysns.text = sysns.text + ('\n' if sysns.text else '') + sys_content %}
|
||||
{%- endif %}
|
||||
{%- set sysns.count = sysns.count + 1 %}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{%- set num_sys = sysns.count %}
|
||||
{%- set merged_system = sysns.text %}
|
||||
{%- set reasoning_instructions = '' %}
|
||||
{%- if enable_thinking is undefined or enable_thinking is true %}
|
||||
{%- set resolved_reasoning_effort = reasoning_effort|default('xhigh') %}
|
||||
{%- if resolved_reasoning_effort == 'high' %}
|
||||
{%- set resolved_reasoning_effort = 'xhigh' %}
|
||||
{%- endif %}
|
||||
{%- if resolved_reasoning_effort not in ('xhigh', 'medium', 'low') %}
|
||||
{{- raise_exception('Unexpected reasoning effort ' ~ reasoning_effort ~ '. Supported types are xhigh (default), medium, and low.') }}
|
||||
{%- endif %}
|
||||
{%- if resolved_reasoning_effort == 'xhigh' %}
|
||||
{%- set reasoning_instructions = 'Reasoning effort is set to xhigh. Please think carefully through the task, validate key assumptions, consider plausible alternatives, and prioritize correctness, consistency, and clarity in the final answer.' %}
|
||||
{%- elif resolved_reasoning_effort == 'low' %}
|
||||
{%- set reasoning_instructions = 'Reasoning effort is set to low. Keep your thinking brief and focused, moving directly to the conclusion without unnecessary elaboration.' %}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{%- if tools and tools is iterable and tools is not mapping %}
|
||||
{{- '<|im_start|>system\n' }}
|
||||
{%- if reasoning_instructions %}
|
||||
{{- reasoning_instructions + '\n\n' }}
|
||||
{%- endif %}
|
||||
{{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
|
||||
{%- for tool in tools %}
|
||||
{{- "\n" }}
|
||||
{{- tool | tojson }}
|
||||
{%- endfor %}
|
||||
{{- "\n</tools>" }}
|
||||
{{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
|
||||
{%- if merged_system %}
|
||||
{{- '\n\n' + merged_system }}
|
||||
{%- endif %}
|
||||
{{- '<|im_end|>\n' }}
|
||||
{%- else %}
|
||||
{%- if merged_system %}
|
||||
{{- '<|im_start|>system\n' + (reasoning_instructions + '\n\n' if reasoning_instructions else '') + merged_system + '<|im_end|>\n' }}
|
||||
{%- elif reasoning_instructions %}
|
||||
{{- '<|im_start|>system\n' + reasoning_instructions + '<|im_end|>\n' }}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
|
||||
{%- for message in messages[::-1] %}
|
||||
{%- set index = (messages|length - 1) - loop.index0 %}
|
||||
{%- if ns.multi_step_tool and message.role == "user" %}
|
||||
{%- set content = render_content(message.content, false)|trim %}
|
||||
{%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
|
||||
{%- set ns.multi_step_tool = false %}
|
||||
{%- set ns.last_query_index = index %}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{%- for message in messages %}
|
||||
{%- if loop.index0 >= num_sys %}
|
||||
{%- set content = render_content(message.content, true)|trim %}
|
||||
{%- if message.role == "system" or message.role == "developer" %}
|
||||
{{- raise_exception('System message must be at the beginning.') }}
|
||||
{%- elif message.role == "user" %}
|
||||
{{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
|
||||
{%- elif message.role == "assistant" %}
|
||||
{%- set reasoning_content = '' %}
|
||||
{%- if message.reasoning_content is string %}
|
||||
{%- set reasoning_content = message.reasoning_content %}
|
||||
{%- endif %}
|
||||
{%- set reasoning_content = reasoning_content|trim %}
|
||||
{%- if preserve_thinking is undefined or preserve_thinking is true or loop.index0 > ns.last_query_index %}
|
||||
{{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content + '\n</think>\n\n' + content }}
|
||||
{%- else %}
|
||||
{{- '<|im_start|>' + message.role + '\n' + content }}
|
||||
{%- endif %}
|
||||
{%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}
|
||||
{%- for tool_call in message.tool_calls %}
|
||||
{%- if tool_call.function is defined %}
|
||||
{%- set tool_call = tool_call.function %}
|
||||
{%- endif %}
|
||||
{%- if tool_call.name is not defined or tool_call.name is none %}
|
||||
{{- raise_exception('Tool call is missing a function name.') }}
|
||||
{%- endif %}
|
||||
{%- if loop.first %}
|
||||
{%- if content|trim %}
|
||||
{{- '\n\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
|
||||
{%- else %}
|
||||
{{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
|
||||
{%- endif %}
|
||||
{%- else %}
|
||||
{{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
|
||||
{%- endif %}
|
||||
{%- if tool_call.arguments is mapping %}
|
||||
{%- for args_name, args_value in tool_call.arguments|items %}
|
||||
{{- '<parameter=' + args_name + '>\n' }}
|
||||
{%- set args_value = args_value | string if args_value is string else args_value | tojson | safe %}
|
||||
{{- args_value }}
|
||||
{{- '\n</parameter>\n' }}
|
||||
{%- endfor %}
|
||||
{%- elif tool_call.arguments is string %}
|
||||
{%- if tool_call.arguments|trim %}
|
||||
{{- raise_exception('Tool call arguments for function "' + (tool_call.name | string) + '" were passed as a JSON string. Parse them into an object before calling apply_chat_template.') }}
|
||||
{%- endif %}
|
||||
{%- elif tool_call.arguments is defined and tool_call.arguments is not none %}
|
||||
{{- raise_exception('Tool call arguments for function "' + (tool_call.name | string) + '" must be an object/mapping or a JSON string.') }}
|
||||
{%- endif %}
|
||||
{{- '</function>\n</tool_call>' }}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
{{- '<|im_end|>\n' }}
|
||||
{%- elif message.role == "tool" %}
|
||||
{%- if loop.previtem and loop.previtem.role != "tool" %}
|
||||
{{- '<|im_start|>user' }}
|
||||
{%- endif %}
|
||||
{{- '\n<tool_response>\n' }}
|
||||
{{- content }}
|
||||
{{- '\n</tool_response>' }}
|
||||
{%- if not loop.last and loop.nextitem.role != "tool" %}
|
||||
{{- '<|im_end|>\n' }}
|
||||
{%- elif loop.last %}
|
||||
{{- '<|im_end|>\n' }}
|
||||
{%- endif %}
|
||||
{%- else %}
|
||||
{{- raise_exception('Unexpected message role.') }}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{%- if add_generation_prompt %}
|
||||
{{- '<|im_start|>assistant\n' }}
|
||||
{%- if enable_thinking is defined and enable_thinking is false %}
|
||||
{{- '<think>\n\n</think>\n\n' }}
|
||||
{%- else %}
|
||||
{{- '<think>\n' }}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{#- Unsloth fixes - developer role, merged system messages, tool calling #}
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
{
|
||||
"protocol_id": "strata-qwen38-thinking-v1-draft.1",
|
||||
"bundle": "standard-v1",
|
||||
"profile": "qwen3.8-flash-next-thinking",
|
||||
"tokenizer_files": {
|
||||
"vocab.json": "4ba64f0332abcfb0b600b7df1537d1e836e68009d5fb7cdb77339738dc6365c4",
|
||||
"merges.txt": "080cf95432173729d160346b7882bd2a644b07f7a3813a3eb73f4e66787bfde9",
|
||||
"token_type.json": "5088c8c298fc06af8382ddb3b76c888703ac2634e82263b97eedcc2ad202738b"
|
||||
},
|
||||
"template": {
|
||||
"path": "serve/chat_template.jinja",
|
||||
"sha256": "ae9644304511ef71f28e036b6f65b4c709880bc5a23b7d0b42125ec63a8c70ca"
|
||||
},
|
||||
"template_kwargs": {
|
||||
"enable_thinking": true,
|
||||
"preserve_thinking": true,
|
||||
"reasoning_effort": "xhigh"
|
||||
},
|
||||
"record_format": "record{:06d}: alpha beta gamma delta epsilon zeta eta theta.",
|
||||
"short_task": "Explain how a hash table finds a value, including collisions and one small example.",
|
||||
"summary_question": "Summarize the structure of these records and explain how to look up a record by its ID.",
|
||||
"record_counts": {
|
||||
"prompt-4k": {
|
||||
"records": 211,
|
||||
"target_tokens": 4096
|
||||
},
|
||||
"prompt-16k": {
|
||||
"records": 858,
|
||||
"target_tokens": 16384
|
||||
}
|
||||
},
|
||||
"length_tolerance": 0.02,
|
||||
"requests": [
|
||||
{
|
||||
"id": "warmup",
|
||||
"input_tokens": 75,
|
||||
"max_shared_prefix_tokens": 47
|
||||
},
|
||||
{
|
||||
"id": "performance-short-r1",
|
||||
"input_tokens": 77,
|
||||
"max_shared_prefix_tokens": 50
|
||||
},
|
||||
{
|
||||
"id": "performance-prompt-4k-r1",
|
||||
"input_tokens": 4093,
|
||||
"max_shared_prefix_tokens": 54
|
||||
},
|
||||
{
|
||||
"id": "performance-prompt-16k-r1",
|
||||
"input_tokens": 16387,
|
||||
"max_shared_prefix_tokens": 55
|
||||
},
|
||||
{
|
||||
"id": "performance-short-r2",
|
||||
"input_tokens": 77,
|
||||
"max_shared_prefix_tokens": 50
|
||||
},
|
||||
{
|
||||
"id": "performance-prompt-4k-r2",
|
||||
"input_tokens": 4093,
|
||||
"max_shared_prefix_tokens": 54
|
||||
},
|
||||
{
|
||||
"id": "performance-prompt-16k-r2",
|
||||
"input_tokens": 16387,
|
||||
"max_shared_prefix_tokens": 55
|
||||
},
|
||||
{
|
||||
"id": "performance-short-r3",
|
||||
"input_tokens": 77,
|
||||
"max_shared_prefix_tokens": 50
|
||||
},
|
||||
{
|
||||
"id": "performance-prompt-4k-r3",
|
||||
"input_tokens": 4093,
|
||||
"max_shared_prefix_tokens": 54
|
||||
},
|
||||
{
|
||||
"id": "performance-prompt-16k-r3",
|
||||
"input_tokens": 16387,
|
||||
"max_shared_prefix_tokens": 55
|
||||
},
|
||||
{
|
||||
"id": "sanity-I1",
|
||||
"input_tokens": 82,
|
||||
"max_shared_prefix_tokens": 49
|
||||
},
|
||||
{
|
||||
"id": "sanity-I2",
|
||||
"input_tokens": 90,
|
||||
"max_shared_prefix_tokens": 49
|
||||
},
|
||||
{
|
||||
"id": "sanity-I3",
|
||||
"input_tokens": 85,
|
||||
"max_shared_prefix_tokens": 49
|
||||
},
|
||||
{
|
||||
"id": "sanity-I4",
|
||||
"input_tokens": 71,
|
||||
"max_shared_prefix_tokens": 49
|
||||
},
|
||||
{
|
||||
"id": "sanity-R1",
|
||||
"input_tokens": 4089,
|
||||
"max_shared_prefix_tokens": 49
|
||||
},
|
||||
{
|
||||
"id": "sanity-R2",
|
||||
"input_tokens": 4089,
|
||||
"max_shared_prefix_tokens": 49
|
||||
},
|
||||
{
|
||||
"id": "sanity-R3",
|
||||
"input_tokens": 16382,
|
||||
"max_shared_prefix_tokens": 49
|
||||
},
|
||||
{
|
||||
"id": "sanity-R4",
|
||||
"input_tokens": 16390,
|
||||
"max_shared_prefix_tokens": 49
|
||||
},
|
||||
{
|
||||
"id": "sanity-T1",
|
||||
"input_tokens": 325,
|
||||
"max_shared_prefix_tokens": 61
|
||||
},
|
||||
{
|
||||
"id": "sanity-T2",
|
||||
"input_tokens": 335,
|
||||
"max_shared_prefix_tokens": 62
|
||||
},
|
||||
{
|
||||
"id": "sanity-T3",
|
||||
"input_tokens": 352,
|
||||
"max_shared_prefix_tokens": 61
|
||||
},
|
||||
{
|
||||
"id": "sanity-T4",
|
||||
"input_tokens": 339,
|
||||
"max_shared_prefix_tokens": 62
|
||||
},
|
||||
{
|
||||
"id": "concurrency-r1-c1",
|
||||
"input_tokens": 78,
|
||||
"max_shared_prefix_tokens": 51
|
||||
},
|
||||
{
|
||||
"id": "concurrency-r1-c2",
|
||||
"input_tokens": 78,
|
||||
"max_shared_prefix_tokens": 51
|
||||
},
|
||||
{
|
||||
"id": "concurrency-r1-c3",
|
||||
"input_tokens": 78,
|
||||
"max_shared_prefix_tokens": 51
|
||||
},
|
||||
{
|
||||
"id": "concurrency-r1-c4",
|
||||
"input_tokens": 78,
|
||||
"max_shared_prefix_tokens": 51
|
||||
},
|
||||
{
|
||||
"id": "concurrency-r2-c1",
|
||||
"input_tokens": 78,
|
||||
"max_shared_prefix_tokens": 51
|
||||
},
|
||||
{
|
||||
"id": "concurrency-r2-c2",
|
||||
"input_tokens": 78,
|
||||
"max_shared_prefix_tokens": 51
|
||||
},
|
||||
{
|
||||
"id": "concurrency-r2-c3",
|
||||
"input_tokens": 78,
|
||||
"max_shared_prefix_tokens": 51
|
||||
},
|
||||
{
|
||||
"id": "concurrency-r2-c4",
|
||||
"input_tokens": 78,
|
||||
"max_shared_prefix_tokens": 51
|
||||
},
|
||||
{
|
||||
"id": "concurrency-r3-c1",
|
||||
"input_tokens": 78,
|
||||
"max_shared_prefix_tokens": 51
|
||||
},
|
||||
{
|
||||
"id": "concurrency-r3-c2",
|
||||
"input_tokens": 78,
|
||||
"max_shared_prefix_tokens": 51
|
||||
},
|
||||
{
|
||||
"id": "concurrency-r3-c3",
|
||||
"input_tokens": 78,
|
||||
"max_shared_prefix_tokens": 51
|
||||
},
|
||||
{
|
||||
"id": "concurrency-r3-c4",
|
||||
"input_tokens": 78,
|
||||
"max_shared_prefix_tokens": 51
|
||||
}
|
||||
],
|
||||
"note": "input_tokens counts the rendered thinking request with the generation prompt. max_shared_prefix_tokens is the longest token prefix this request shares with any other request of the bundle: cache reuse above it means the user payload was read from the cache."
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
bc6c1f81d86863c3dc36a31f58010701d8ab05180f69f86c6978214409e43af2 build.json
|
||||
f724855774dfbea89fade712a70d534a085c88cda6abc123db419fd79eefab49 requests/concurrency-r1-c1.json
|
||||
2c85f877abbf144ca065c58efc2a7568aaf2c8dc317ad8266e78c149f8245abb requests/concurrency-r1-c2.json
|
||||
264097f61645a0d5089562dc003b7748b1c20339c2df1290ad6664fc1834068a requests/concurrency-r1-c3.json
|
||||
65895c5a5fab53c06e9358d88d921f1bfc9dc7cc8b6e8a2521ac90de080ff470 requests/concurrency-r1-c4.json
|
||||
2c08d50e0c95e345512ec918f58fa716780b79900f09bbfabc37519c8d86543a requests/concurrency-r2-c1.json
|
||||
8aba49693701b9315b2bc5ad55e97a8b03496baedd05f370c49c2c77f45ce6cd requests/concurrency-r2-c2.json
|
||||
7cfb4a5a8fb5702244ce42e0dba9ccd3064d4023d700b409a1a472790a82809c requests/concurrency-r2-c3.json
|
||||
3ab56fbba5fd405945e53cff01b59c3ef966b0f08a0a1b0b50633d26e90cad62 requests/concurrency-r2-c4.json
|
||||
3adb684cf2cb84d4e514de30a4c6f8ccebdd8471160cfcdfc0cdb8ca19ac2137 requests/concurrency-r3-c1.json
|
||||
650379ef9a4da667f6581ae9573eb9af2db2a1fc617701e50536590b18f70077 requests/concurrency-r3-c2.json
|
||||
111415388c5e69ecbd9ab66efe85eb8f3db9142201e1548d03203d3acc05eca1 requests/concurrency-r3-c3.json
|
||||
691a8d27fd06e9beab57e6efb3d82c2a7ed9fc26635fd51b1b1d017117440c1e requests/concurrency-r3-c4.json
|
||||
06776ef38485dc4bb7e18809534fe16a57d421d163f81d4d1532c4b08225968e requests/performance-prompt-16k-r1.json
|
||||
4995c5f3c47c1682f4dc753a9f3a1fba6920fea14691e1ded2da6208ef5fe628 requests/performance-prompt-16k-r2.json
|
||||
7af4c49f99192007be27110296b5a0bcc2efbc91c7ff1c337a6cac6732050559 requests/performance-prompt-16k-r3.json
|
||||
bda0e60c2de0703899ac1bd5711d793b9b325c7f8ba6be6c967f22dd07d8dda8 requests/performance-prompt-4k-r1.json
|
||||
e898eec0ee22499473179a719505c9feb9d3772658833f607dffb34c3727150e requests/performance-prompt-4k-r2.json
|
||||
6d3b3300eace318574490f4379119409920cbe7e88fdaff28eb6f5c79430c3e3 requests/performance-prompt-4k-r3.json
|
||||
483b4f18fcc64ca802fde3377c59bf7b114ec5a5c96fe781bd2be4665ef7b898 requests/performance-short-r1.json
|
||||
b0ec85f40117303d19c8c17a4813625c8554f59caddbe69e7d8aafdcf51d501a requests/performance-short-r2.json
|
||||
e210da20b80c61147da5102fcba65131dafbbb4ceee952fa6b91ce2bb06581ed requests/performance-short-r3.json
|
||||
70ebcd31af5ae1bc7573136d9a257469d8ad9e85b5d0233980998e957c36d6c2 requests/sanity-I1.json
|
||||
1cc6e4bc56b5a22043b6e384b3036170dc6d3edeb403be391a8b802b5613f4cb requests/sanity-I2.json
|
||||
c29b13e6e31b674ff733202b3c0b1f9410768b9ea9368a4a6a13430b3e011fb8 requests/sanity-I3.json
|
||||
780fbd1d02f119323cf50ac4dabcfffbd23dd84115efbe18152e7ee9112b041a requests/sanity-I4.json
|
||||
3c1e0718ad06e591e47cf8df9c36bab2d13332d44b05e440ec17d0402bf9facc requests/sanity-R1.json
|
||||
352e054c48e08e11d339d3a02a14f4f50125071710679b8d04b77aba711fbafc requests/sanity-R2.json
|
||||
36770434f1ea046ec84b32691d644e8416e89fbcbb04b8f6032da66016d78291 requests/sanity-R3.json
|
||||
a63df33c784732cc6b4d8a0f2258b7226c8df5a3b4f6818f9e886bbf09604999 requests/sanity-R4.json
|
||||
6b272ef684b43c1e0004a40b958c807b84dcd9de579d4b2d8819e39ae4196fb2 requests/sanity-T1.json
|
||||
8253ee4aa6b19e61e28d64406807218c9aa10cf2cf682101e8a2d6a50564de46 requests/sanity-T2.json
|
||||
aa2be8761793f51eab6ef04bc54c96e018f6981b7a6a26b9e50cc3c7ac856803 requests/sanity-T3.json
|
||||
04043bdec397df487bebd2999e280a9fae5925e5efec1a10fa96055ec3430faa requests/sanity-T4.json
|
||||
d3365c57db8be1fd0dd9771f74f264b1f6994a396e472c3e5fa4ede2641ffb5c requests/warmup.json
|
||||
+1719
File diff suppressed because it is too large.
Load diff
+86
@@ -0,0 +1,86 @@
|
||||
# Strata standard benchmark: NVIDIA GeForce RTX 3060
|
||||
|
||||
Protocol `strata-qwen38-thinking-v1-draft.1` (draft), run `20261020T082538Z-6302f9ef`, started 2026-10-20T08:25:38Z. **Status: complete.**
|
||||
|
||||
Written by `strata bench run`. The machine-readable result is [results.json](results.json); every request, its raw usage and timings and the client event offsets are in [outputs.jsonl](outputs.jsonl).
|
||||
|
||||
## Hardware and software
|
||||
|
||||
- GPU: NVIDIA GeForce RTX 3060 (12.0 GiB dedicated)
|
||||
- CPU: Intel(R) Core(TM) Ultra 7 265; RAM: 46.2 GiB
|
||||
- Storage of the model files: nvme Lexar SSD NM1090 PRO 2TB (nvme)
|
||||
- OS: Ubuntu 24.04.5 LTS 7.0.0-38-generic; drivers: gpu-driver 615.71.09; runtime: cuda 13.4
|
||||
- Strata commit: `a7bdbfdabc9a3e7c83e3dbc2441d0933569d773e`; engine 0.1.46; release build
|
||||
- Background work: none besides sshd
|
||||
|
||||
## Model and configuration
|
||||
|
||||
- Model: qwen3.8-flash-next-iq3_xxs; quantization IQ3_XXS; revision not measured; source revision not measured
|
||||
- Files: `Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00001-of-00002.gguf` `219ea929900dfa9e...`; `Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00002-of-00002.gguf` `316b46f3a2dbd68c...`; `expert-profile.bin` `8f59b4aa8873209d...`
|
||||
- Track: **custom** (release 0.2.1 terminal+engine 0.1.46 cuda12, hand-written run config mirroring setup defaults (IQ3_XXS, int8 KV, MTP spec 4); box: p2003)
|
||||
- Context 32768 tokens; server capacity 1 slot(s)
|
||||
- Sampling: temperature 1.0, top_p 0.95, top_k 20, min_p 0.0, presence_penalty 0.0, repetition_penalty 1.0, reasoning_effort xhigh; thinking on, preserved
|
||||
- Engine options: `config.json`; chat template: `provenance/chat_template.jinja`
|
||||
|
||||
## Method
|
||||
|
||||
The frozen requests of `bench/prompts/standard-v1` (corpus `e75eff8b9feea04f...`) were sent over loopback HTTP, streaming, one at a time, in this order: 1 warm-up (excluded), 3 repetitions of the short, 4K and 16K prompts, and the twelve sanity checks. Prompt and decode speed are the engine's own `timings`; time to first token is the client's monotonic clock from dispatch to the first nonempty reasoning, answer or tool-call delta. No request was retried. Time after the model loaded: 3 min 22 s.
|
||||
|
||||
## Results
|
||||
|
||||
| Case | Prompt tok/s | Decode tok/s | TTFT s | First answer s | Total s |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| short | 183.0 (181.5-188.4) n=3 | 59.1 (58.2-63.0) n=3 | 0.45 (0.43-0.46) n=3 | 3.97 (1.74-4.43) n=3 | 8.7 (5.6-9.1) n=3 |
|
||||
| prompt-4k | 961.5 (958.7-962.0) n=3 | 54.6 (51.1-62.3) n=3 | 4.30 (4.29-4.32) n=3 | 7.69 (7.69-7.69) n=1 | 13.6 (12.5-14.3) n=3 |
|
||||
| prompt-16k | 1100.0 (1098.3-1103.7) n=3 | 53.2 (52.3-54.1) n=3 | 14.94 (14.88-14.95) n=3 | 23.29 (23.29-23.29) n=1 | 24.5 (24.4-24.7) n=3 |
|
||||
|
||||
Cells are median (min-max) n=samples.
|
||||
|
||||
| Phase | Planned | Completed | Truncated | Failed | Skipped |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| warmup | 1 | 0 | 1 | 0 | 0 |
|
||||
| performance | 9 | 3 | 6 | 0 | 0 |
|
||||
| sanity | 12 | 12 | 0 | 0 | 0 |
|
||||
| concurrency | 12 | 3 | 9 | 0 | 0 |
|
||||
|
||||
Sanity checks: **12 of 12**.
|
||||
|
||||
| Case | Result | Reason |
|
||||
| --- | --- | --- |
|
||||
| I1 | pass | the expected JSON value |
|
||||
| I2 | pass | the expected JSON value |
|
||||
| I3 | pass | the expected JSON value |
|
||||
| I4 | pass | the exact expected text |
|
||||
| R1 | pass | the exact expected text |
|
||||
| R2 | pass | the exact expected text |
|
||||
| R3 | pass | the exact expected text |
|
||||
| R4 | pass | the expected JSON value |
|
||||
| T1 | pass | the expected tool call |
|
||||
| T2 | pass | the expected tool call |
|
||||
| T3 | pass | the expected tool call |
|
||||
| T4 | pass | the expected tool call |
|
||||
|
||||
Concurrency rounds (four requests released together):
|
||||
|
||||
| Round | Wall s | Output tokens | Aggregate tok/s | Failed |
|
||||
| ---: | ---: | ---: | ---: | ---: |
|
||||
| 1 | 38.693398 | 2048 | 52.928926 | 0 |
|
||||
| 2 | 32.637462 | 1840 | 56.376933 | 0 |
|
||||
| 3 | 38.39188 | 1994 | 51.938066 | 0 |
|
||||
|
||||
## Failures, exclusions and limitations
|
||||
|
||||
- No request failed.
|
||||
- Excluded from speed summaries: concurrency-r1-c1: first_answer_s is not available for this request
|
||||
- Excluded from speed summaries: concurrency-r1-c4: first_answer_s is not available for this request
|
||||
- Excluded from speed summaries: concurrency-r2-c3: first_answer_s is not available for this request
|
||||
- Excluded from speed summaries: concurrency-r3-c1: first_answer_s is not available for this request
|
||||
- Excluded from speed summaries: concurrency-r3-c3: first_answer_s is not available for this request
|
||||
- Excluded from speed summaries: performance-prompt-16k-r1: first_answer_s is not available for this request
|
||||
- Excluded from speed summaries: performance-prompt-16k-r3: first_answer_s is not available for this request
|
||||
- Excluded from speed summaries: performance-prompt-4k-r2: first_answer_s is not available for this request
|
||||
- Excluded from speed summaries: performance-prompt-4k-r3: first_answer_s is not available for this request
|
||||
- Prompt-prefix reuse is the shared template prefix only; the cache was not emptied between requests, and the expert cache stays warm and evolves. These are not cold-model measurements.
|
||||
- Output caps include reasoning; a request that reaches its cap is `truncated` and fails its sanity check.
|
||||
- The runner cannot see other programs on the machine, the repeat-stop and loop-recovery switches of the server, or the quantization repository's revision; see `missing` in results.json for every value it could not read.
|
||||
- The server had answered no request before this run (a freshly loaded engine).
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"engine_options": {
|
||||
"--pack": "iq3_xxs",
|
||||
"--native": "Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00001-of-00002.gguf",
|
||||
"--ple-gguf": "Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00002-of-00002.gguf",
|
||||
"--expert-profile": "expert-profile.bin",
|
||||
"--expert-cache": "auto",
|
||||
"--prefill": "auto",
|
||||
"--spec": "4",
|
||||
"--spec-min-p": "0.5",
|
||||
"--mtp": "rt",
|
||||
"--max-context": "32768",
|
||||
"--kv": "int8",
|
||||
"--resident-experts": true,
|
||||
"--pool-workers": "13"
|
||||
},
|
||||
"other_engine_options": {
|
||||
"--pack": "iq3_xxs",
|
||||
"--native": "Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00001-of-00002.gguf",
|
||||
"--ple-gguf": "Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS-00002-of-00002.gguf",
|
||||
"--max-context": "32768",
|
||||
"--resident-experts": true
|
||||
},
|
||||
"server_defaults": {
|
||||
"temperature": 1.0,
|
||||
"top_p": 0.95,
|
||||
"top_k": 20,
|
||||
"n_predict": -1
|
||||
},
|
||||
"total_slots": 1,
|
||||
"context": 32768,
|
||||
"source": "effective engine arguments of the profile"
|
||||
}
|
||||
Loaded 100 of 329 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user