WaveCut commited on
Commit
3fd9ca4
·
verified ·
1 Parent(s): 55c2c6a

Release v1.1.3 scored WebGPU evidence

Browse files

Adds browser state-drift scoring and release evidence, refreshes the pinned wllama artifacts, and keeps DFlash inspection-only and disabled by default.

.gitattributes CHANGED
@@ -1 +1,3 @@
1
  public/wasm/*.wasm filter=lfs diff=lfs merge=lfs -text
 
 
 
1
  public/wasm/*.wasm filter=lfs diff=lfs merge=lfs -text
2
+ dist/wasm/wllama-compat.wasm filter=lfs diff=lfs merge=lfs -text
3
+ dist/wasm/wllama.wasm filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -71,8 +71,29 @@ deep-link keeps the benchmark executable under the same COOP/COEP headers. It
71
  loads nothing until **Run benchmark** is pressed, then records first-load and
72
  verified-cache reload wall time, TTFT,
73
  prefill/decode throughput, graph placement, device limits, engine revisions,
74
- the exact runtime artifact hash, and a fixed temperature-0 prompt. The report
75
- exports as JSON; generated model text is deliberately excluded.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  `n_batch`, `n_ubatch`, Flash Attention, paired K/V cache types, and the pinned
78
  JSPI/compat WASM flavor are available there as explicitly experimental
@@ -83,14 +104,21 @@ and Q4_0 KV: every row retained one graph, 29/29 GPU layers, zero CPU ops, and
83
  the same short deterministic output hash. Observed KV allocation fell from
84
  112 MiB for F16 to 59.5 MiB for Q8_0 and 31.5 MiB for Q4_0.
85
 
 
 
 
 
 
 
86
  A same-Chrome A/B with one warmup and three alternating measured runs per arm
87
  also proved explicit JSPI/compat switching. Median warm load was 2,257.7 ms for
88
  JSPI and 2,799.1 ms for compat; all eight outputs and graph tripwires matched.
89
  These are experimental single-device results, not release recommendations;
90
  normal chat keeps Flash off, F16/F16, and automatic WASM selection.
91
 
92
- The public prewarmed-cache baseline below is one Apple `metal-3` / headless
93
- Chrome sample, not a cross-device study and not a first-download claim:
 
94
 
95
  | Tier | Backend | Load | TTFT | Decode |
96
  | --- | --- | ---: | ---: | ---: |
@@ -102,10 +130,11 @@ Chrome sample, not a cross-device study and not a first-download claim:
102
  ## DFlash research lane
103
 
104
  DFlash is visible only as an inspection-only experiment. Its BF16 source and
105
- Q8_0 native GGUF reference are pinned in the manifest. Shape, tokenizer, target
106
- taps, conversion, and a 32-token temperature-0 native Bonsai-27B parity smoke
107
- pass. The custom WASM contains the DFlash llama.cpp architecture, but the
108
- browser load API does not yet plumb `spec_type=draft-dflash`; the Q8
 
109
  `fc.weight` also exceeds the 128 MiB baseline WebGPU binding limit. It remains
110
  disabled and unavailable until a lower-bit candidate, browser parity,
111
  acceptance, and positive speed gates all pass.
@@ -125,10 +154,10 @@ stay in the browser; only model/static asset downloads contact Hugging Face.
125
  - Dawn native WebGPU validation: `18eb229ef5f707c1464cc581252e7603c73a3ef0`
126
  - custom wllama source: `912c18b75d4358c1405a64646b8dbe43a205943b`
127
  - custom nested llama.cpp: `00fa7cb284cbf133fc426733bd64238a3588a33e`
128
- - custom patch set: `ad8786a295eaeb75ee03752d7f84d30f14ea373407844d5c589f7ab46800bb4b`
129
- - custom ESM: `24bcef8aea8e27fb7b7e2d9e6ea94ba8ced7bfbffd09a0675821b9eb1b4a4c9f`
130
- - JSPI WASM: `dd71f58c75f32c677a64eeb2e56efab7dedbe7395f730bb08bc80021bd182caa`
131
- - compat WASM: `eca9754d0d8a490b5c7b58cb384d67e31aa8ab26dd330e4ad1460982df6dd82e`
132
  - compat worker: `0b667db536815fb9e5dc9ffbfbd6ba29affdda38a4ba892d5372ddb41ce9a8c8`
133
 
134
  The vendored runtime exposes the validated token-embedding WebGPU placement
@@ -139,9 +168,11 @@ backend tripwire disagrees. Its exact JS/WASM hashes are checked during
139
  `THIRD_PARTY_NOTICES.md` and `public/licenses/`.
140
 
141
  The pinned WebGPU source implements Flash Attention with Q4_0/Q8_0 K/V. The
142
- benchmark lane has short deterministic-output, memory, and graph-placement
143
- evidence for those combinations, but long-state, 27B, and cross-device gates
144
- remain open. Flash Attention therefore remains off for the release path.
 
 
145
 
146
  ## Licenses and attribution
147
 
 
71
  loads nothing until **Run benchmark** is pressed, then records first-load and
72
  verified-cache reload wall time, TTFT,
73
  prefill/decode throughput, graph placement, device limits, engine revisions,
74
+ the exact runtime artifact hash, and a fixed temperature-0 prompt. Schema v3
75
+ exports the exact messages, sampling controls, raw generated text, pinned model
76
+ source/shard hashes, and an opt-in sampled-token trace. Normal chat does not
77
+ request logprobs and returns no token-id trace.
78
+
79
+ The default `field-core-v1` workload remains short. The separate
80
+ `state-drift-1k-v1` evidence workload locks greedy sampling and refuses to
81
+ export unless all 1,024 sampled token IDs and the 64/128/256/512/768/1,024
82
+ uint32-LE prefix hashes are present. Its diagnostic-only 27B WebGPU path also
83
+ validates the pinned native CPU prompt/reference fixture, then teacher-forces
84
+ that fixed 1,024-token sequence through raw token-ID prefixes. Schema v3 keeps
85
+ the forced reference logprob, natural top five, mean NLL, and perplexity
86
+ separate from the natural browser generation. This scoring method is not
87
+ available to normal chat and does not activate DFlash.
88
+
89
+ On the pinned Apple `metal-3` release path, the final JSPI runtime completed the
90
+ natural 1,024-token 27B run at 11.3 tok/s with one graph, zero CPU ops, and
91
+ 65/65 GPU layers. Exact CPU/browser token parity remains `NO-GO` because the
92
+ first natural mismatch is a reciprocal 13/198 top-one/top-two flip at position
93
+ 30. The separate numerical gate passes: browser and native CPU mean NLL are
94
+ 0.0456380321 and 0.0455182718 (absolute delta 0.0001197604 against a 0.01
95
+ limit), with near-tie margins 0.006506 and 0.003605. This supports a bounded
96
+ GPU floating-point tolerance; it is not a bit-exact parity claim.
97
 
98
  `n_batch`, `n_ubatch`, Flash Attention, paired K/V cache types, and the pinned
99
  JSPI/compat WASM flavor are available there as explicitly experimental
 
104
  the same short deterministic output hash. Observed KV allocation fell from
105
  112 MiB for F16 to 59.5 MiB for Q8_0 and 31.5 MiB for Q4_0.
106
 
107
+ The separate final-artifact 27B >8K smoke used an 8,448-token context with
108
+ 8,314 prompt tokens, Flash auto, and Q4_0/Q4_0 KV. It measured 7.55 prefill
109
+ tok/s and 6.42 decode tok/s, retained one graph with zero CPU ops and 65/65
110
+ GPU layers, and exported all 8 requested top-five token records. This is one
111
+ local Apple `metal-3` result; it does not establish a cross-device default.
112
+
113
  A same-Chrome A/B with one warmup and three alternating measured runs per arm
114
  also proved explicit JSPI/compat switching. Median warm load was 2,257.7 ms for
115
  JSPI and 2,799.1 ms for compat; all eight outputs and graph tripwires matched.
116
  These are experimental single-device results, not release recommendations;
117
  normal chat keeps Flash off, F16/F16, and automatic WASM selection.
118
 
119
+ The historical public v1.1.2 prewarmed-cache baseline below is one Apple
120
+ `metal-3` / headless Chrome sample, not a cross-device study and not a
121
+ first-download claim:
122
 
123
  | Tier | Backend | Load | TTFT | Decode |
124
  | --- | --- | ---: | ---: | ---: |
 
130
  ## DFlash research lane
131
 
132
  DFlash is visible only as an inspection-only experiment. Its BF16 source and
133
+ Q8_0 native GGUF reference are pinned in the manifest. The strict D1 audit
134
+ passes all 39 shape, tokenizer, target-tap, conversion, revision, and evidence
135
+ checks, including a 32-token temperature-0 native Bonsai-27B parity smoke. The
136
+ custom WASM contains the DFlash llama.cpp architecture, but the browser load
137
+ API does not yet plumb `spec_type=draft-dflash`; the Q8
138
  `fc.weight` also exceeds the 128 MiB baseline WebGPU binding limit. It remains
139
  disabled and unavailable until a lower-bit candidate, browser parity,
140
  acceptance, and positive speed gates all pass.
 
154
  - Dawn native WebGPU validation: `18eb229ef5f707c1464cc581252e7603c73a3ef0`
155
  - custom wllama source: `912c18b75d4358c1405a64646b8dbe43a205943b`
156
  - custom nested llama.cpp: `00fa7cb284cbf133fc426733bd64238a3588a33e`
157
+ - custom patch set: `27b96fa48c6fd66b44fe70e83cc775902e29ec5877261c7eb04fecabc529d2f9`
158
+ - custom ESM: `10c3811776e34092a225632929b97046af4215a4c91156169105c886be5cba5d`
159
+ - JSPI WASM: `b292bf670a25e1ecf02c71dd92b49196f3336842135f1f85efd5a3fd5f36ea8a`
160
+ - compat WASM: `ea0623d2ca1758287a569169b4ad5a69c22e41bce3e5d022c7d3e4fb5c37918c`
161
  - compat worker: `0b667db536815fb9e5dc9ffbfbd6ba29affdda38a4ba892d5372ddb41ce9a8c8`
162
 
163
  The vendored runtime exposes the validated token-embedding WebGPU placement
 
168
  `THIRD_PARTY_NOTICES.md` and `public/licenses/`.
169
 
170
  The pinned WebGPU source implements Flash Attention with Q4_0/Q8_0 K/V. The
171
+ benchmark lane has short 1.7B deterministic-output and memory evidence for
172
+ both quantized types, a 27B long-state result only for Flash-off F16, and a
173
+ 27B >8K smoke only for Flash-auto Q4_0. No 27B Q8_0 long-state result is
174
+ claimed. Repeated and cross-device gates remain open, so Flash Attention
175
+ remains off and F16/F16 remains the release default.
176
 
177
  ## Licenses and attribution
178
 
dist/assets/index-DIzN0g8r.js ADDED
The diff for this file is too large to render. See raw diff
 
dist/assets/index-jtL5gUVS.js DELETED
The diff for this file is too large to render. See raw diff
 
dist/assets/{worker-gvajdY5k.js → worker-CUYMhoYj.js} RENAMED
The diff for this file is too large to render. See raw diff
 
dist/fixtures/state-drift-27b-cpu-reference.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"schemaVersion":1,"kind":"bonsai-state-drift-reference","workload":{"id":"state-drift-1k-v1","messages":[{"role":"user","content":"Output consecutive integers starting at 1, separated only by a comma and one space. Continue without explanation until you reach 500."}]},"provenance":{"sourceEvidence":"results/space-model/27b-native-cpu-state-drift-1024-b32-u16.json","engineRevision":"00fa7cb284cbf133fc426733bd64238a3588a33e","nativeBinary":{"bytes":65392,"sha256":"e8f23d8612ed27739f21a6aca1df618699da902f6fc42cbdb3f9c978f6da04fb"},"model":{"file":"Bonsai-27B-Q1_0.gguf","bytes":3803452480,"sha256":"17ef842e47450caeb8eaa3ebfbbab5d2f2278b62b79be107985fb69a2f819aa0"},"renderedPromptSha256":"7c46d0de443e6f1235ddaa7c0a55c9da710eb73958d5671247b6f0e3b7d189c7","execution":{"backend":"cpu","contextSize":2048,"batchSize":32,"microBatchSize":16}},"tokenEncoding":"uint32-le","promptTokenIds":[248045,846,198,4935,23173,24959,5732,506,220,16,11,18101,1132,539,264,30644,321,799,3433,13,14569,1973,15673,2980,488,5372,220,20,15,15,13,248046,198,248045,74455,198,248068,198],"promptTokenIdsSha256":"e7af7d66c96f24cb148575bd45cef079920669eb0d193fdc830ce61dfec85451","referenceTokenIds":[8160,579,264,7047,1817,25,271,16,13,220,2972,2014,53983,2570,5396,64700,198,256,471,2972,6065,64700,8984,23173,24959,5732,506,220,16,13,198,256,471,2972,3925,64700,32001,639,1132,539,264,30644,321,799,3433,318,72,1673,2487,328,16,11,220,17,11,220,18,11,2423,1764,198,256,471,2972,17355,64700,14569,1973,15673,2980,18176,220,20,15,15,13,198,256,471,2972,57545,64700,561,2468,1220,381,264,3074,18677,886,20296,314,4947,494,220,16,310,220,20,15,15,11,22405,6681,430,5024,13,271,17,13,220,2972,27382,1386,5141,84457,64700,198,256,471,4980,506,220,16,198,256,471,3839,506,220,20,15,15,198,256,471,75604,25,3544,328,318,43871,478,3433,8,198,256,471,2233,39492,11,1066,279,4947,198,256,471,1160,82819,24959,271,18,13,220,2972,35,23544,8984,14606,64700,198,256,561,2468,1220,1353,1040,25,198,256,1510,16,11,220,17,11,220,18,11,220,19,11,220,20,11,58317,220,19,24,24,11,220,20,15,15,63,271,19,13,220,2972,30097,279,27929,64700,198,256,353,1144,310,6707,4947,494,220,16,310,220,20,15,15,11,10542,539,3544,5748,198,256,8439,353,2688,449,14791,11,353,628,6707,411,53624,6804,303,821,1965,13,353,3172,8972,424,15060,13,271,256,6558,579,9845,279,4581,3443,25,328,16,11,220,17,11,220,18,11,58317,220,20,15,15,1,198,256,353,3172,6707,424,5774,13,271,256,6817,25,353,1144,310,1236,2617,1017,579,874,4799,1414,11,874,48794,35358,421,2493,38556,11,1066,279,6891,1414,430,10897,13,561,9640,2640,328,323,47930,1132,539,264,30644,321,799,3433,487,748,353,3172,24660,1732,421,13,271,256,353,3172,6707,279,2400,8240,1381,13,271,256,27929,25,220,16,11,220,17,11,220,18,11,220,19,11,220,20,11,220,21,11,220,22,11,220,23,11,220,24,11,220,16,15,11,220,16,16,11,220,16,17,11,220,16,18,11,220,16,19,11,220,16,20,11,220,16,21,11,220,16,22,11,220,16,23,11,220,16,24,11,220,17,15,11,220,17,16,11,220,17,17,11,220,17,18,11,220,17,19,11,220,17,20,11,220,17,21,11,220,17,22,11,220,17,23,11,220,17,24,11,220,18,15,11,220,18,16,11,220,18,17,11,220,18,18,11,220,18,19,11,220,18,20,11,220,18,21,11,220,18,22,11,220,18,23,11,220,18,24,11,220,19,15,11,220,19,16,11,220,19,17,11,220,19,18,11,220,19,19,11,220,19,20,11,220,19,21,11,220,19,22,11,220,19,23,11,220,19,24,11,220,20,15,11,220,20,16,11,220,20,17,11,220,20,18,11,220,20,19,11,220,20,20,11,220,20,21,11,220,20,22,11,220,20,23,11,220,20,24,11,220,21,15,11,220,21,16,11,220,21,17,11,220,21,18,11,220,21,19,11,220,21,20,11,220,21,21,11,220,21,22,11,220,21,23,11,220,21,24,11,220,22,15,11,220,22,16,11,220,22,17,11,220,22,18,11,220,22,19,11,220,22,20,11,220,22,21,11,220,22,22,11,220,22,23,11,220,22,24,11,220,23,15,11,220,23,16,11,220,23,17,11,220,23,18,11,220,23,19,11,220,23,20,11,220,23,21,11,220,23,22,11,220,23,23,11,220,23,24,11,220,24,15,11,220,24,16,11,220,24,17,11,220,24,18,11,220,24,19,11,220,24,20,11,220,24,21,11,220,24,22,11,220,24,23,11,220,24,24,11,220,16,15,15,11,220,16,15,16,11,220,16,15,17,11,220,16,15,18,11,220,16,15,19,11,220,16,15,20,11,220,16,15,21,11,220,16,15,22,11,220,16,15,23,11,220,16,15,24,11,220,16,16,15,11,220,16,16,16,11,220,16,16,17,11,220,16,16,18,11,220,16,16,19,11,220,16,16,20,11,220,16,16,21,11,220,16,16,22,11,220,16,16,23,11,220,16,16,24,11,220,16,17,15,11,220,16,17,16,11,220,16,17,17,11,220,16,17,18,11,220,16,17,19,11,220,16,17,20,11,220,16,17,21,11,220,16,17,22,11,220,16,17,23,11,220,16,17,24,11,220,16,18,15,11,220,16,18,16,11,220,16,18,17,11,220,16,18,18,11,220,16,18,19,11,220,16,18,20,11,220,16,18,21,11,220,16,18,22,11,220,16,18,23,11,220,16,18,24,11,220,16,19,15,11,220,16,19,16,11,220,16,19,17,11,220,16,19,18,11,220,16,19,19,11,220,16,19,20,11,220,16,19,21,11,220,16,19,22,11,220,16,19,23,11,220,16,19,24,11,220,16,20,15,11,220,16,20,16,11,220,16,20,17,11,220,16,20,18,11,220,16,20,19,11,220,16,20,20,11,220,16],"referenceTokenIdsSha256":"c503b2db0dcf10daecfda4f19a5f466d1699b31688076d6c32f7c29daefcef9b","checkpointPrefixes":[{"tokens":64,"sha256":"f6398ce3b2c935435615b8ba33d0416c03228e4a1ab0a9ff3f4229418558a130"},{"tokens":128,"sha256":"0efb5a50141c6b4140846e69df0e3ed3b86491040fa52f3cc4fb1ab561c153a5"},{"tokens":256,"sha256":"f4e94ebe35a375348e5156732df7fc858060cd350b4e2f366e6c249758db7b9e"},{"tokens":512,"sha256":"11acf787b85658d6a8db7a4d872faf9145684291aba790b39afd184ecf48e7b9"},{"tokens":768,"sha256":"faa10cea44ae4d461f355ed31f1599b5b67c6ac121b9296c2ed24af8af92aec7"},{"tokens":1024,"sha256":"c503b2db0dcf10daecfda4f19a5f466d1699b31688076d6c32f7c29daefcef9b"}]}
dist/index.html CHANGED
@@ -10,7 +10,7 @@
10
  content="Bonsai local inference field station — private WebGPU language models in your browser."
11
  />
12
  <title>Bonsai Field Station</title>
13
- <script type="module" crossorigin src="/assets/index-jtL5gUVS.js"></script>
14
  <link rel="stylesheet" crossorigin href="/assets/index-C-2tm91j.css">
15
  </head>
16
  <body>
 
10
  content="Bonsai local inference field station — private WebGPU language models in your browser."
11
  />
12
  <title>Bonsai Field Station</title>
13
+ <script type="module" crossorigin src="/assets/index-DIzN0g8r.js"></script>
14
  <link rel="stylesheet" crossorigin href="/assets/index-C-2tm91j.css">
15
  </head>
16
  <body>
dist/wasm/wllama-compat.wasm CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:eca9754d0d8a490b5c7b58cb384d67e31aa8ab26dd330e4ad1460982df6dd82e
3
- size 14864188
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ea0623d2ca1758287a569169b4ad5a69c22e41bce3e5d022c7d3e4fb5c37918c
3
+ size 14865338
dist/wasm/wllama.wasm CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:dd71f58c75f32c677a64eeb2e56efab7dedbe7395f730bb08bc80021bd182caa
3
- size 8017169
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b292bf670a25e1ecf02c71dd92b49196f3336842135f1f85efd5a3fd5f36ea8a
3
+ size 8017784
public/fixtures/state-drift-27b-cpu-reference.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"schemaVersion":1,"kind":"bonsai-state-drift-reference","workload":{"id":"state-drift-1k-v1","messages":[{"role":"user","content":"Output consecutive integers starting at 1, separated only by a comma and one space. Continue without explanation until you reach 500."}]},"provenance":{"sourceEvidence":"results/space-model/27b-native-cpu-state-drift-1024-b32-u16.json","engineRevision":"00fa7cb284cbf133fc426733bd64238a3588a33e","nativeBinary":{"bytes":65392,"sha256":"e8f23d8612ed27739f21a6aca1df618699da902f6fc42cbdb3f9c978f6da04fb"},"model":{"file":"Bonsai-27B-Q1_0.gguf","bytes":3803452480,"sha256":"17ef842e47450caeb8eaa3ebfbbab5d2f2278b62b79be107985fb69a2f819aa0"},"renderedPromptSha256":"7c46d0de443e6f1235ddaa7c0a55c9da710eb73958d5671247b6f0e3b7d189c7","execution":{"backend":"cpu","contextSize":2048,"batchSize":32,"microBatchSize":16}},"tokenEncoding":"uint32-le","promptTokenIds":[248045,846,198,4935,23173,24959,5732,506,220,16,11,18101,1132,539,264,30644,321,799,3433,13,14569,1973,15673,2980,488,5372,220,20,15,15,13,248046,198,248045,74455,198,248068,198],"promptTokenIdsSha256":"e7af7d66c96f24cb148575bd45cef079920669eb0d193fdc830ce61dfec85451","referenceTokenIds":[8160,579,264,7047,1817,25,271,16,13,220,2972,2014,53983,2570,5396,64700,198,256,471,2972,6065,64700,8984,23173,24959,5732,506,220,16,13,198,256,471,2972,3925,64700,32001,639,1132,539,264,30644,321,799,3433,318,72,1673,2487,328,16,11,220,17,11,220,18,11,2423,1764,198,256,471,2972,17355,64700,14569,1973,15673,2980,18176,220,20,15,15,13,198,256,471,2972,57545,64700,561,2468,1220,381,264,3074,18677,886,20296,314,4947,494,220,16,310,220,20,15,15,11,22405,6681,430,5024,13,271,17,13,220,2972,27382,1386,5141,84457,64700,198,256,471,4980,506,220,16,198,256,471,3839,506,220,20,15,15,198,256,471,75604,25,3544,328,318,43871,478,3433,8,198,256,471,2233,39492,11,1066,279,4947,198,256,471,1160,82819,24959,271,18,13,220,2972,35,23544,8984,14606,64700,198,256,561,2468,1220,1353,1040,25,198,256,1510,16,11,220,17,11,220,18,11,220,19,11,220,20,11,58317,220,19,24,24,11,220,20,15,15,63,271,19,13,220,2972,30097,279,27929,64700,198,256,353,1144,310,6707,4947,494,220,16,310,220,20,15,15,11,10542,539,3544,5748,198,256,8439,353,2688,449,14791,11,353,628,6707,411,53624,6804,303,821,1965,13,353,3172,8972,424,15060,13,271,256,6558,579,9845,279,4581,3443,25,328,16,11,220,17,11,220,18,11,58317,220,20,15,15,1,198,256,353,3172,6707,424,5774,13,271,256,6817,25,353,1144,310,1236,2617,1017,579,874,4799,1414,11,874,48794,35358,421,2493,38556,11,1066,279,6891,1414,430,10897,13,561,9640,2640,328,323,47930,1132,539,264,30644,321,799,3433,487,748,353,3172,24660,1732,421,13,271,256,353,3172,6707,279,2400,8240,1381,13,271,256,27929,25,220,16,11,220,17,11,220,18,11,220,19,11,220,20,11,220,21,11,220,22,11,220,23,11,220,24,11,220,16,15,11,220,16,16,11,220,16,17,11,220,16,18,11,220,16,19,11,220,16,20,11,220,16,21,11,220,16,22,11,220,16,23,11,220,16,24,11,220,17,15,11,220,17,16,11,220,17,17,11,220,17,18,11,220,17,19,11,220,17,20,11,220,17,21,11,220,17,22,11,220,17,23,11,220,17,24,11,220,18,15,11,220,18,16,11,220,18,17,11,220,18,18,11,220,18,19,11,220,18,20,11,220,18,21,11,220,18,22,11,220,18,23,11,220,18,24,11,220,19,15,11,220,19,16,11,220,19,17,11,220,19,18,11,220,19,19,11,220,19,20,11,220,19,21,11,220,19,22,11,220,19,23,11,220,19,24,11,220,20,15,11,220,20,16,11,220,20,17,11,220,20,18,11,220,20,19,11,220,20,20,11,220,20,21,11,220,20,22,11,220,20,23,11,220,20,24,11,220,21,15,11,220,21,16,11,220,21,17,11,220,21,18,11,220,21,19,11,220,21,20,11,220,21,21,11,220,21,22,11,220,21,23,11,220,21,24,11,220,22,15,11,220,22,16,11,220,22,17,11,220,22,18,11,220,22,19,11,220,22,20,11,220,22,21,11,220,22,22,11,220,22,23,11,220,22,24,11,220,23,15,11,220,23,16,11,220,23,17,11,220,23,18,11,220,23,19,11,220,23,20,11,220,23,21,11,220,23,22,11,220,23,23,11,220,23,24,11,220,24,15,11,220,24,16,11,220,24,17,11,220,24,18,11,220,24,19,11,220,24,20,11,220,24,21,11,220,24,22,11,220,24,23,11,220,24,24,11,220,16,15,15,11,220,16,15,16,11,220,16,15,17,11,220,16,15,18,11,220,16,15,19,11,220,16,15,20,11,220,16,15,21,11,220,16,15,22,11,220,16,15,23,11,220,16,15,24,11,220,16,16,15,11,220,16,16,16,11,220,16,16,17,11,220,16,16,18,11,220,16,16,19,11,220,16,16,20,11,220,16,16,21,11,220,16,16,22,11,220,16,16,23,11,220,16,16,24,11,220,16,17,15,11,220,16,17,16,11,220,16,17,17,11,220,16,17,18,11,220,16,17,19,11,220,16,17,20,11,220,16,17,21,11,220,16,17,22,11,220,16,17,23,11,220,16,17,24,11,220,16,18,15,11,220,16,18,16,11,220,16,18,17,11,220,16,18,18,11,220,16,18,19,11,220,16,18,20,11,220,16,18,21,11,220,16,18,22,11,220,16,18,23,11,220,16,18,24,11,220,16,19,15,11,220,16,19,16,11,220,16,19,17,11,220,16,19,18,11,220,16,19,19,11,220,16,19,20,11,220,16,19,21,11,220,16,19,22,11,220,16,19,23,11,220,16,19,24,11,220,16,20,15,11,220,16,20,16,11,220,16,20,17,11,220,16,20,18,11,220,16,20,19,11,220,16,20,20,11,220,16],"referenceTokenIdsSha256":"c503b2db0dcf10daecfda4f19a5f466d1699b31688076d6c32f7c29daefcef9b","checkpointPrefixes":[{"tokens":64,"sha256":"f6398ce3b2c935435615b8ba33d0416c03228e4a1ab0a9ff3f4229418558a130"},{"tokens":128,"sha256":"0efb5a50141c6b4140846e69df0e3ed3b86491040fa52f3cc4fb1ab561c153a5"},{"tokens":256,"sha256":"f4e94ebe35a375348e5156732df7fc858060cd350b4e2f366e6c249758db7b9e"},{"tokens":512,"sha256":"11acf787b85658d6a8db7a4d872faf9145684291aba790b39afd184ecf48e7b9"},{"tokens":768,"sha256":"faa10cea44ae4d461f355ed31f1599b5b67c6ac121b9296c2ed24af8af92aec7"},{"tokens":1024,"sha256":"c503b2db0dcf10daecfda4f19a5f466d1699b31688076d6c32f7c29daefcef9b"}]}
public/wasm/wllama-compat.wasm CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:eca9754d0d8a490b5c7b58cb384d67e31aa8ab26dd330e4ad1460982df6dd82e
3
- size 14864188
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ea0623d2ca1758287a569169b4ad5a69c22e41bce3e5d022c7d3e4fb5c37918c
3
+ size 14865338
public/wasm/wllama.wasm CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:dd71f58c75f32c677a64eeb2e56efab7dedbe7395f730bb08bc80021bd182caa
3
- size 8017169
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b292bf670a25e1ecf02c71dd92b49196f3336842135f1f85efd5a3fd5f36ea8a
3
+ size 8017784
scripts/verify-wllama-assets.mjs CHANGED
@@ -44,6 +44,10 @@ const wllamaDeclarations = await readFile(resolve(root, 'vendor/wllama-bonsai/es
44
  if (!wllamaDeclarations.includes('forceCompat?: boolean')) {
45
  throw new Error('Vendored Bonsai wllama declarations lack forceCompat');
46
  }
 
 
 
 
47
  const moduleSource = await readFile(resolve(root, 'vendor/wllama-bonsai/esm/index.js'), 'utf8');
48
  if (!moduleSource.includes('n_ubatch: params.n_ubatch')) {
49
  throw new Error('Vendored Bonsai wllama module does not forward n_ubatch');
 
44
  if (!wllamaDeclarations.includes('forceCompat?: boolean')) {
45
  throw new Error('Vendored Bonsai wllama declarations lack forceCompat');
46
  }
47
+ const oaiDeclarations = await readFile(resolve(root, 'vendor/wllama-bonsai/esm/types/oai-compat.d.ts'), 'utf8');
48
+ if (!oaiDeclarations.includes('export interface ChatCompletionLogprob {\n id: number;')) {
49
+ throw new Error('Vendored Bonsai wllama declarations lack sampled token ids');
50
+ }
51
  const moduleSource = await readFile(resolve(root, 'vendor/wllama-bonsai/esm/index.js'), 'utf8');
52
  if (!moduleSource.includes('n_ubatch: params.n_ubatch')) {
53
  throw new Error('Vendored Bonsai wllama module does not forward n_ubatch');
src/bench/BenchApp.tsx CHANGED
@@ -2,25 +2,33 @@ import { useEffect, useMemo, useRef, useState } from 'react';
2
  import {
3
  BrowserEngineClient,
4
  EngineClientError,
 
5
  evaluateModelGate,
6
  loadModelManifestV2,
7
  type BenchmarkFlashMode,
8
  type BenchmarkKvCacheType,
9
  type BenchmarkWasmFlavor,
10
  type EngineCapabilities,
 
11
  type ModelManifestV2,
12
  type ModelTierId,
13
  type RequestedBackend,
14
  } from '../engine';
15
  import { formatBytes } from '../lib/format';
16
  import {
17
- BENCHMARK_PROMPT,
18
- BENCHMARK_PROMPT_ID,
19
  benchmarkReportFilename,
20
  buildBenchmarkReport,
21
  serializeBenchmarkReport,
22
  type BenchmarkReport,
 
 
23
  } from './report';
 
 
 
 
24
 
25
  const MANIFEST_PATH = 'manifest/models.json';
26
  const DEFAULT_MODEL: ModelTierId = '1_7b';
@@ -43,6 +51,13 @@ function manifestUrl(): string {
43
  return new URL(`${import.meta.env.BASE_URL}${MANIFEST_PATH}`, document.baseURI).href;
44
  }
45
 
 
 
 
 
 
 
 
46
  function errorMessage(error: unknown): string {
47
  if (error instanceof EngineClientError) return `${error.code}: ${error.message}`;
48
  return error instanceof Error ? error.message : String(error);
@@ -89,6 +104,7 @@ export function BenchApp() {
89
  const [client] = useState(getBenchClient);
90
  const [manifest, setManifest] = useState<ModelManifestV2 | null>(null);
91
  const [capabilities, setCapabilities] = useState<EngineCapabilities | null>(null);
 
92
  const [modelId, setModelId] = useState<ModelTierId>(DEFAULT_MODEL);
93
  const [backend, setBackend] = useState<RequestedBackend>('auto');
94
  const [contextSize, setContextSize] = useState(4_096);
@@ -146,6 +162,7 @@ export function BenchApp() {
146
  () => manifest?.models.find((candidate) => candidate.id === modelId) ?? null,
147
  [manifest, modelId],
148
  );
 
149
  const deviceGate = useMemo(() => {
150
  if (!model || !capabilities) return null;
151
  return evaluateModelGate(model, backend, capabilities.webgpu, {
@@ -159,9 +176,15 @@ export function BenchApp() {
159
  return evaluateModelGate(model, backend, capabilities.webgpu, capabilities.storage);
160
  }, [backend, capabilities, model]);
161
 
162
- const contextLimit = model?.id === '27b'
163
- ? model.defaultContext
164
- : model?.contextLength ?? 4_096;
 
 
 
 
 
 
165
  const nBatch = parseOptionalInteger(nBatchInput);
166
  const nUbatch = parseOptionalInteger(nUbatchInput);
167
  const batchTuningError = nBatchInput.trim() !== '' && nBatch === null
@@ -179,13 +202,27 @@ export function BenchApp() {
179
  const wasmTuningError = wasmFlavor === 'jspi' && capabilities?.runtime.wasmFlavor === 'compat'
180
  ? 'JSPI is unavailable in this browser; use auto or compat.'
181
  : null;
182
- const configurationTuningError = batchTuningError ?? runtimeTuningError ?? wasmTuningError;
183
- const configurationValid = Number.isSafeInteger(contextSize)
184
- && contextSize > 0
185
- && contextSize <= contextLimit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  && Number.isSafeInteger(maxTokens)
187
  && maxTokens >= 8
188
- && maxTokens <= 512
189
  && configurationTuningError === null;
190
  const canRun = Boolean(manifest && capabilities && model && deviceGate?.allowed && configurationValid);
191
 
@@ -299,9 +336,8 @@ export function BenchApp() {
299
  let firstTokenAt: number | null = null;
300
  let streamedTokenEvents = 0;
301
  const generationStarted = performance.now();
302
- setProgress({ label: `Generating fixed prompt · ${BENCHMARK_PROMPT_ID}`, loadedBytes: 0, totalBytes: maxTokens });
303
- const generationResult = await client.generate({
304
- messages: [{ role: 'user', content: BENCHMARK_PROMPT }],
305
  maxTokens,
306
  temperature: 0,
307
  topK: 1,
@@ -309,7 +345,10 @@ export function BenchApp() {
309
  seed: 42,
310
  toolChoice: 'none',
311
  cachePrompt: false,
312
- }, {
 
 
 
313
  signal: controller.signal,
314
  onToken: (text, reasoningDelta) => {
315
  if (!text && !reasoningDelta) return;
@@ -324,6 +363,40 @@ export function BenchApp() {
324
  });
325
  const generationCompleted = performance.now();
326
  throwIfAborted(controller.signal);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  const backendReport = await client.backendReport();
328
  throwIfAborted(controller.signal);
329
 
@@ -335,7 +408,8 @@ export function BenchApp() {
335
  capabilities,
336
  requestedBackend: backend,
337
  contextSize: warmLoad.context.size,
338
- maxTokens,
 
339
  coldLoadMs,
340
  warmLoadMs,
341
  coldCachedBytes,
@@ -346,9 +420,10 @@ export function BenchApp() {
346
  timeToFirstTokenMs: firstTokenAt === null ? null : firstTokenAt - generationStarted,
347
  streamedTokenEvents,
348
  backendReport,
 
349
  });
350
  setReport(nextReport);
351
- setOutput(generationResult.text);
352
  setProgress({
353
  label: 'Benchmark complete · JSON report is ready',
354
  loadedBytes: 1,
@@ -398,6 +473,26 @@ export function BenchApp() {
398
  <p className="bench-control-intro">The fixed prompt and temperature stay locked so exported runs remain comparable.</p>
399
 
400
  <form onSubmit={(event) => { event.preventDefault(); runBenchmark(); }}>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  <label className="bench-field">
402
  <span>Model tier</span>
403
  <select
@@ -451,7 +546,7 @@ export function BenchApp() {
451
  <input
452
  type="number"
453
  min="8"
454
- max="512"
455
  step="8"
456
  value={maxTokens}
457
  disabled={running}
@@ -460,7 +555,7 @@ export function BenchApp() {
460
  clearPreviousRun();
461
  }}
462
  />
463
- <small>8–512</small>
464
  </label>
465
  </div>
466
 
@@ -642,6 +737,8 @@ export function BenchApp() {
642
  <div><dt>Decode</dt><dd>{formatRate(report?.generation.decodeTokensPerSecond)}</dd></div>
643
  <div><dt>Token count</dt><dd>{report?.generation.completionTokens ?? '—'}</dd></div>
644
  <div><dt>Finish</dt><dd>{report?.generation.finishReason ?? '—'}</dd></div>
 
 
645
  </dl>
646
  </article>
647
  <article className="bench-data-card">
@@ -677,11 +774,11 @@ export function BenchApp() {
677
  <section className="bench-output">
678
  <div>
679
  <p className="eyebrow ink">Fixed specimen</p>
680
- <h3>{BENCHMARK_PROMPT_ID}</h3>
681
- <p>{BENCHMARK_PROMPT}</p>
682
  </div>
683
  <div>
684
- <p className="eyebrow ink">Generated text · excluded from export</p>
685
  <p data-testid="bench-generated-output">{output || 'Run the assay to inspect the generated specimen.'}</p>
686
  </div>
687
  </section>
 
2
  import {
3
  BrowserEngineClient,
4
  EngineClientError,
5
+ evaluateModelContextPolicy,
6
  evaluateModelGate,
7
  loadModelManifestV2,
8
  type BenchmarkFlashMode,
9
  type BenchmarkKvCacheType,
10
  type BenchmarkWasmFlavor,
11
  type EngineCapabilities,
12
+ type GenerateParams,
13
  type ModelManifestV2,
14
  type ModelTierId,
15
  type RequestedBackend,
16
  } from '../engine';
17
  import { formatBytes } from '../lib/format';
18
  import {
19
+ BENCHMARK_WORKLOADS,
20
+ DEFAULT_BENCHMARK_WORKLOAD_ID,
21
  benchmarkReportFilename,
22
  buildBenchmarkReport,
23
  serializeBenchmarkReport,
24
  type BenchmarkReport,
25
+ type BenchmarkWorkloadId,
26
+ type TeacherForcedReferenceScoreEvidence,
27
  } from './report';
28
+ import {
29
+ STATE_DRIFT_REFERENCE_PATH,
30
+ validateStateDriftReferenceFixture,
31
+ } from './state-drift-reference';
32
 
33
  const MANIFEST_PATH = 'manifest/models.json';
34
  const DEFAULT_MODEL: ModelTierId = '1_7b';
 
51
  return new URL(`${import.meta.env.BASE_URL}${MANIFEST_PATH}`, document.baseURI).href;
52
  }
53
 
54
+ function stateDriftReferenceUrl(): string {
55
+ return new URL(
56
+ `${import.meta.env.BASE_URL}${STATE_DRIFT_REFERENCE_PATH}`,
57
+ document.baseURI,
58
+ ).href;
59
+ }
60
+
61
  function errorMessage(error: unknown): string {
62
  if (error instanceof EngineClientError) return `${error.code}: ${error.message}`;
63
  return error instanceof Error ? error.message : String(error);
 
104
  const [client] = useState(getBenchClient);
105
  const [manifest, setManifest] = useState<ModelManifestV2 | null>(null);
106
  const [capabilities, setCapabilities] = useState<EngineCapabilities | null>(null);
107
+ const [workloadId, setWorkloadId] = useState<BenchmarkWorkloadId>(DEFAULT_BENCHMARK_WORKLOAD_ID);
108
  const [modelId, setModelId] = useState<ModelTierId>(DEFAULT_MODEL);
109
  const [backend, setBackend] = useState<RequestedBackend>('auto');
110
  const [contextSize, setContextSize] = useState(4_096);
 
162
  () => manifest?.models.find((candidate) => candidate.id === modelId) ?? null,
163
  [manifest, modelId],
164
  );
165
+ const workload = BENCHMARK_WORKLOADS[workloadId];
166
  const deviceGate = useMemo(() => {
167
  if (!model || !capabilities) return null;
168
  return evaluateModelGate(model, backend, capabilities.webgpu, {
 
176
  return evaluateModelGate(model, backend, capabilities.webgpu, capabilities.storage);
177
  }, [backend, capabilities, model]);
178
 
179
+ const contextPolicy = model
180
+ ? evaluateModelContextPolicy(model, contextSize, {
181
+ tuningScope: 'benchmark',
182
+ requestedBackend: backend,
183
+ flashMode,
184
+ kvCacheType,
185
+ })
186
+ : null;
187
+ const contextLimit = contextPolicy?.limit ?? 4_096;
188
  const nBatch = parseOptionalInteger(nBatchInput);
189
  const nUbatch = parseOptionalInteger(nUbatchInput);
190
  const batchTuningError = nBatchInput.trim() !== '' && nBatch === null
 
202
  const wasmTuningError = wasmFlavor === 'jspi' && capabilities?.runtime.wasmFlavor === 'compat'
203
  ? 'JSPI is unavailable in this browser; use auto or compat.'
204
  : null;
205
+ const workloadError = workload.requiredMaxTokens !== null && maxTokens !== workload.requiredMaxTokens
206
+ ? `${workload.id} requires exactly ${workload.requiredMaxTokens.toLocaleString()} max tokens.`
207
+ : null;
208
+ const stateDriftReferenceError = workload.id === 'state-drift-1k-v1' && (
209
+ model?.id !== '27b'
210
+ || backend !== 'webgpu'
211
+ || contextSize !== 2_048
212
+ || nBatch !== 32
213
+ || nUbatch !== 16
214
+ )
215
+ ? 'state-drift-1k-v1 reference scoring requires 27B, explicit WebGPU, context 2,048, nBatch 32, and nUbatch 16.'
216
+ : null;
217
+ const configurationTuningError = batchTuningError
218
+ ?? runtimeTuningError
219
+ ?? wasmTuningError
220
+ ?? workloadError
221
+ ?? stateDriftReferenceError;
222
+ const configurationValid = contextPolicy?.allowed === true
223
  && Number.isSafeInteger(maxTokens)
224
  && maxTokens >= 8
225
+ && maxTokens <= 1_024
226
  && configurationTuningError === null;
227
  const canRun = Boolean(manifest && capabilities && model && deviceGate?.allowed && configurationValid);
228
 
 
336
  let firstTokenAt: number | null = null;
337
  let streamedTokenEvents = 0;
338
  const generationStarted = performance.now();
339
+ const generationRequest = {
340
+ messages: workload.messages.map((message) => ({ ...message })),
 
341
  maxTokens,
342
  temperature: 0,
343
  topK: 1,
 
345
  seed: 42,
346
  toolChoice: 'none',
347
  cachePrompt: false,
348
+ returnTokenIds: true,
349
+ } satisfies GenerateParams;
350
+ setProgress({ label: `Generating fixed workload · ${workload.id}`, loadedBytes: 0, totalBytes: maxTokens });
351
+ const generationResult = await client.generate(generationRequest, {
352
  signal: controller.signal,
353
  onToken: (text, reasoningDelta) => {
354
  if (!text && !reasoningDelta) return;
 
363
  });
364
  const generationCompleted = performance.now();
365
  throwIfAborted(controller.signal);
366
+ let teacherForcedReferenceScore: TeacherForcedReferenceScoreEvidence | null = null;
367
+ if (workload.id === 'state-drift-1k-v1') {
368
+ setProgress({
369
+ label: 'Loading and validating pinned CPU reference',
370
+ loadedBytes: 0,
371
+ totalBytes: 1,
372
+ });
373
+ const fixtureResponse = await fetch(stateDriftReferenceUrl(), {
374
+ signal: controller.signal,
375
+ cache: 'no-cache',
376
+ });
377
+ if (!fixtureResponse.ok) {
378
+ throw new Error(
379
+ `CPU reference fixture request failed with HTTP ${fixtureResponse.status}.`,
380
+ );
381
+ }
382
+ const fixture = validateStateDriftReferenceFixture(
383
+ await fixtureResponse.json() as unknown,
384
+ { model, capabilities, loadResult: warmLoad },
385
+ );
386
+ throwIfAborted(controller.signal);
387
+ setProgress({
388
+ label: 'Teacher-forcing the 1,024-token CPU reference',
389
+ loadedBytes: 0,
390
+ totalBytes: fixture.referenceTokenIds.length,
391
+ });
392
+ const score = await client.scoreSequence({
393
+ promptTokenIds: fixture.promptTokenIds,
394
+ referenceTokenIds: fixture.referenceTokenIds,
395
+ topK: 5,
396
+ }, { signal: controller.signal });
397
+ teacherForcedReferenceScore = { fixture, score };
398
+ throwIfAborted(controller.signal);
399
+ }
400
  const backendReport = await client.backendReport();
401
  throwIfAborted(controller.signal);
402
 
 
408
  capabilities,
409
  requestedBackend: backend,
410
  contextSize: warmLoad.context.size,
411
+ workloadId,
412
+ generationRequest,
413
  coldLoadMs,
414
  warmLoadMs,
415
  coldCachedBytes,
 
420
  timeToFirstTokenMs: firstTokenAt === null ? null : firstTokenAt - generationStarted,
421
  streamedTokenEvents,
422
  backendReport,
423
+ teacherForcedReferenceScore,
424
  });
425
  setReport(nextReport);
426
+ setOutput(generationResult.text || generationResult.reasoningText);
427
  setProgress({
428
  label: 'Benchmark complete · JSON report is ready',
429
  loadedBytes: 1,
 
473
  <p className="bench-control-intro">The fixed prompt and temperature stay locked so exported runs remain comparable.</p>
474
 
475
  <form onSubmit={(event) => { event.preventDefault(); runBenchmark(); }}>
476
+ <label className="bench-field">
477
+ <span>Workload</span>
478
+ <select
479
+ value={workloadId}
480
+ disabled={running}
481
+ onChange={(event) => {
482
+ const nextId = event.target.value as BenchmarkWorkloadId;
483
+ const next = BENCHMARK_WORKLOADS[nextId];
484
+ setWorkloadId(nextId);
485
+ setMaxTokens(next.requiredMaxTokens ?? 64);
486
+ clearPreviousRun();
487
+ }}
488
+ >
489
+ {Object.values(BENCHMARK_WORKLOADS).map((candidate) => (
490
+ <option key={candidate.id} value={candidate.id}>{candidate.label}</option>
491
+ ))}
492
+ </select>
493
+ <small>{workload.description}</small>
494
+ </label>
495
+
496
  <label className="bench-field">
497
  <span>Model tier</span>
498
  <select
 
546
  <input
547
  type="number"
548
  min="8"
549
+ max="1024"
550
  step="8"
551
  value={maxTokens}
552
  disabled={running}
 
555
  clearPreviousRun();
556
  }}
557
  />
558
+ <small>8–1,024</small>
559
  </label>
560
  </div>
561
 
 
737
  <div><dt>Decode</dt><dd>{formatRate(report?.generation.decodeTokensPerSecond)}</dd></div>
738
  <div><dt>Token count</dt><dd>{report?.generation.completionTokens ?? '—'}</dd></div>
739
  <div><dt>Finish</dt><dd>{report?.generation.finishReason ?? '—'}</dd></div>
740
+ <div><dt>CPU-ref PPL</dt><dd>{report?.generation.teacherForcedReferenceScore?.score.summary.perplexity.toFixed(4) ?? '—'}</dd></div>
741
+ <div><dt>CPU-ref mean NLL</dt><dd>{report?.generation.teacherForcedReferenceScore?.score.summary.meanNll.toFixed(6) ?? '—'}</dd></div>
742
  </dl>
743
  </article>
744
  <article className="bench-data-card">
 
774
  <section className="bench-output">
775
  <div>
776
  <p className="eyebrow ink">Fixed specimen</p>
777
+ <h3>{workload.id}</h3>
778
+ <p>{workload.messages.map((message) => message.content ?? '').join('\n')}</p>
779
  </div>
780
  <div>
781
+ <p className="eyebrow ink">Generated output · visible text, else reasoning trace</p>
782
  <p data-testid="bench-generated-output">{output || 'Run the assay to inspect the generated specimen.'}</p>
783
  </div>
784
  </section>
src/bench/report.test.ts CHANGED
@@ -2,16 +2,21 @@ import { describe, expect, it } from 'vitest';
2
  import type {
3
  BackendReport,
4
  EngineCapabilities,
 
5
  LoadModelResult,
6
  ManifestModelV2,
7
  ModelManifestV2,
8
  } from '../engine';
9
  import {
 
 
 
10
  benchmarkReportFilename,
11
  buildBenchmarkReport,
12
  classifyCacheState,
13
  serializeBenchmarkReport,
14
  type BenchmarkObservation,
 
15
  } from './report';
16
 
17
  const JSPI_WASM_SHA256 = 'd'.repeat(64);
@@ -33,6 +38,18 @@ const model = {
33
  id: '8b',
34
  displayName: 'Bonsai 8B',
35
  architecture: 'qwen3next',
 
 
 
 
 
 
 
 
 
 
 
 
36
  downloadBytes: 1_000,
37
  } as ManifestModelV2;
38
 
@@ -150,6 +167,96 @@ const loadResult = {
150
  backendReport,
151
  } satisfies LoadModelResult;
152
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  function observation(overrides: Partial<BenchmarkObservation> = {}): BenchmarkObservation {
154
  return {
155
  startedAt: '2026-07-15T10:00:00.000Z',
@@ -159,7 +266,11 @@ function observation(overrides: Partial<BenchmarkObservation> = {}): BenchmarkOb
159
  capabilities,
160
  requestedBackend: 'auto',
161
  contextSize: 4_096,
162
- maxTokens: 64,
 
 
 
 
163
  coldLoadMs: 1_234.56,
164
  warmLoadMs: 234.54,
165
  coldCachedBytes: 250,
@@ -167,6 +278,9 @@ function observation(overrides: Partial<BenchmarkObservation> = {}): BenchmarkOb
167
  loadResult,
168
  generationResult: {
169
  text: 'fixture',
 
 
 
170
  finishReason: 'stop',
171
  toolCalls: [],
172
  usage: { promptTokens: 20, completionTokens: 8, totalTokens: 28 },
@@ -180,6 +294,87 @@ function observation(overrides: Partial<BenchmarkObservation> = {}): BenchmarkOb
180
  };
181
  }
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  describe('benchmark report', () => {
184
  it('classifies the shard cache observed at the first load boundary', () => {
185
  expect(classifyCacheState(null, 1_000)).toBe('unknown');
@@ -191,6 +386,11 @@ describe('benchmark report', () => {
191
  it('builds a shareable report with pinned runtime evidence and fixed-safe policy', () => {
192
  const report = buildBenchmarkReport(observation());
193
 
 
 
 
 
 
194
  expect(report.load).toMatchObject({
195
  tuning: {
196
  scope: 'release-defaults',
@@ -219,12 +419,38 @@ describe('benchmark report', () => {
219
  warm: { durationMs: 234.5 },
220
  });
221
  expect(report.generation).toMatchObject({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  promptTokensPerSecond: 123.46,
223
  decodeTokensPerSecond: 45.68,
224
  engineCompletionTokens: 8,
225
  streamedTokenEvents: 5,
226
  completionTokens: 8,
227
- tokenCountSource: 'engine-usage',
228
  });
229
  expect(report.execution).toMatchObject({
230
  selectedBackend: 'webgpu',
@@ -345,6 +571,17 @@ describe('benchmark report', () => {
345
  });
346
  });
347
 
 
 
 
 
 
 
 
 
 
 
 
348
  it('exports the selected model DFlash pairing for unsupported 1.7B and conditional 27B', () => {
349
  const reportFor = (modelId: ManifestModelV2['id']) => buildBenchmarkReport(observation({
350
  model: {
@@ -378,11 +615,14 @@ describe('benchmark report', () => {
378
  expect(exportedPairing.targetLayerIdsZeroBased).not.toBe(dflash27bPairing.targetLayerIdsZeroBased);
379
  });
380
 
381
- it('uses streamed events when final engine usage is present but incomplete', () => {
382
  const report = buildBenchmarkReport(observation({
383
  streamedTokenEvents: 64,
384
  generationResult: {
385
  text: 'fixture',
 
 
 
386
  finishReason: 'length',
387
  toolCalls: [],
388
  usage: { promptTokens: 5, completionTokens: 0, totalTokens: 5 },
@@ -396,16 +636,22 @@ describe('benchmark report', () => {
396
  completionTokens: 64,
397
  engineTotalTokens: 5,
398
  totalTokens: 69,
399
- tokenCountSource: 'stream-events-fallback',
400
  });
 
 
 
401
  });
402
 
403
- it('falls back to streamed events without emitting non-finite JSON metrics', () => {
404
  const report = buildBenchmarkReport(observation({
405
  generationElapsedMs: Number.NaN,
406
  timeToFirstTokenMs: Number.POSITIVE_INFINITY,
407
  generationResult: {
408
  text: 'fixture',
 
 
 
409
  finishReason: 'length',
410
  toolCalls: [],
411
  usage: null,
@@ -418,12 +664,228 @@ describe('benchmark report', () => {
418
  timeToFirstTokenMs: null,
419
  decodeTokensPerSecond: null,
420
  completionTokens: 5,
421
- tokenCountSource: 'stream-events-fallback',
422
  });
423
  expect(serializeBenchmarkReport(report)).not.toContain('NaN');
424
  expect(serializeBenchmarkReport(report)).toMatch(/\n$/);
425
  });
426
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  it('uses the model, selected backend, and UTC completion time in the export name', () => {
428
  const report = buildBenchmarkReport(observation());
429
  expect(benchmarkReportFilename(report)).toBe(
 
2
  import type {
3
  BackendReport,
4
  EngineCapabilities,
5
+ GenerateParams,
6
  LoadModelResult,
7
  ManifestModelV2,
8
  ModelManifestV2,
9
  } from '../engine';
10
  import {
11
+ BENCHMARK_WORKLOADS,
12
+ DEFAULT_BENCHMARK_WORKLOAD_ID,
13
+ TOKEN_ID_CHECKPOINTS,
14
  benchmarkReportFilename,
15
  buildBenchmarkReport,
16
  classifyCacheState,
17
  serializeBenchmarkReport,
18
  type BenchmarkObservation,
19
+ type TeacherForcedReferenceScoreEvidence,
20
  } from './report';
21
 
22
  const JSPI_WASM_SHA256 = 'd'.repeat(64);
 
38
  id: '8b',
39
  displayName: 'Bonsai 8B',
40
  architecture: 'qwen3next',
41
+ source: {
42
+ repo: 'prism-ml/Bonsai-8B-gguf',
43
+ revision: '0123456789abcdef0123456789abcdef01234567',
44
+ file: 'Bonsai-8B-Q1_0.gguf',
45
+ bytes: 1_000,
46
+ sha256: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
47
+ },
48
+ files: [{
49
+ path: '8b/Bonsai-8B-Q1_0.gguf',
50
+ bytes: 1_000,
51
+ sha256: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
52
+ }],
53
  downloadBytes: 1_000,
54
  } as ManifestModelV2;
55
 
 
167
  backendReport,
168
  } satisfies LoadModelResult;
169
 
170
+ const fieldCoreGenerationRequest: GenerateParams = {
171
+ messages: BENCHMARK_WORKLOADS[DEFAULT_BENCHMARK_WORKLOAD_ID].messages.map((message) => ({
172
+ ...message,
173
+ })),
174
+ maxTokens: 64,
175
+ temperature: 0,
176
+ topP: 1,
177
+ topK: 1,
178
+ seed: 42,
179
+ toolChoice: 'none',
180
+ cachePrompt: false,
181
+ returnTokenIds: true,
182
+ };
183
+
184
+ function sampledTokenTrace(tokenIds: readonly number[]) {
185
+ return tokenIds.map((id) => ({
186
+ selected: { id, logprob: -0.1 },
187
+ topCandidates: [
188
+ { id, logprob: -0.1 },
189
+ { id: id + 10_001, logprob: -1 },
190
+ { id: id + 10_002, logprob: -2 },
191
+ { id: id + 10_003, logprob: -3 },
192
+ { id: id + 10_004, logprob: -4 },
193
+ ],
194
+ }));
195
+ }
196
+
197
+ function teacherForcedReferenceScore(
198
+ referenceTokenIds: readonly number[],
199
+ ): TeacherForcedReferenceScoreEvidence {
200
+ const entries = referenceTokenIds.map((id, index) => ({
201
+ index,
202
+ selectedReference: { id, logprob: -0.1 },
203
+ naturalTop1: { id, logprob: -0.1 },
204
+ topCandidates: [
205
+ { id, logprob: -0.1 },
206
+ { id: id + 10_001, logprob: -1 },
207
+ { id: id + 10_002, logprob: -2 },
208
+ { id: id + 10_003, logprob: -3 },
209
+ { id: id + 10_004, logprob: -4 },
210
+ ],
211
+ referenceRankInTopCandidatesZeroBased: 0,
212
+ top1Top2Margin: 0.9,
213
+ }));
214
+ return {
215
+ fixture: {
216
+ schemaVersion: 1,
217
+ kind: 'bonsai-state-drift-reference',
218
+ workload: {
219
+ id: 'state-drift-1k-v1',
220
+ messages: BENCHMARK_WORKLOADS['state-drift-1k-v1'].messages.map((message) => ({
221
+ role: 'user',
222
+ content: message.content ?? '',
223
+ })),
224
+ },
225
+ provenance: {
226
+ sourceEvidence: 'results/space-model/fixture.json',
227
+ engineRevision: capabilities.runtime.llamaCppRevision,
228
+ nativeBinary: { bytes: 1, sha256: '1'.repeat(64) },
229
+ model: { file: model.source.file, bytes: model.source.bytes, sha256: model.source.sha256 },
230
+ renderedPromptSha256: '2'.repeat(64),
231
+ execution: { backend: 'cpu', contextSize: 4_096, batchSize: 2_048, microBatchSize: 512 },
232
+ },
233
+ tokenEncoding: 'uint32-le',
234
+ promptTokenIds: Array.from({ length: 38 }, (_, index) => index + 1),
235
+ promptTokenIdsSha256: '3'.repeat(64),
236
+ referenceTokenIds: [...referenceTokenIds],
237
+ referenceTokenIdsSha256: '4'.repeat(64),
238
+ checkpointPrefixes: TOKEN_ID_CHECKPOINTS.map((tokens) => ({
239
+ tokens,
240
+ sha256: '5'.repeat(64),
241
+ })),
242
+ },
243
+ score: {
244
+ method: {
245
+ promptMode: 'raw-token-id-prefix',
246
+ maxTokensPerStep: 1,
247
+ temperature: 0,
248
+ topK: 1,
249
+ reportedTopLogprobs: 5,
250
+ logitBias: 1_000,
251
+ cachePromptFirst: false,
252
+ cachePromptSubsequent: true,
253
+ },
254
+ entries,
255
+ summary: { tokenCount: 1_024, meanNll: 0.1, perplexity: Math.exp(0.1) },
256
+ },
257
+ };
258
+ }
259
+
260
  function observation(overrides: Partial<BenchmarkObservation> = {}): BenchmarkObservation {
261
  return {
262
  startedAt: '2026-07-15T10:00:00.000Z',
 
266
  capabilities,
267
  requestedBackend: 'auto',
268
  contextSize: 4_096,
269
+ workloadId: DEFAULT_BENCHMARK_WORKLOAD_ID,
270
+ generationRequest: {
271
+ ...fieldCoreGenerationRequest,
272
+ messages: fieldCoreGenerationRequest.messages.map((message) => ({ ...message })),
273
+ },
274
  coldLoadMs: 1_234.56,
275
  warmLoadMs: 234.54,
276
  coldCachedBytes: 250,
 
278
  loadResult,
279
  generationResult: {
280
  text: 'fixture',
281
+ reasoningText: '',
282
+ tokenIds: [101, 102, 103, 104, 105, 106, 107, 108],
283
+ tokenTrace: sampledTokenTrace([101, 102, 103, 104, 105, 106, 107, 108]),
284
  finishReason: 'stop',
285
  toolCalls: [],
286
  usage: { promptTokens: 20, completionTokens: 8, totalTokens: 28 },
 
294
  };
295
  }
296
 
297
+ function longContextObservation(): BenchmarkObservation {
298
+ const longContextBackendReport = {
299
+ ...backendReport,
300
+ layersGpu: { offloaded: 65, total: 65 },
301
+ flashAttention: true,
302
+ cacheTypeK: 'q4_0',
303
+ cacheTypeV: 'q4_0',
304
+ webgpuKvBufferBytes: 155_713_536,
305
+ } satisfies BackendReport;
306
+ const longContextLoadResult = {
307
+ ...loadResult,
308
+ modelId: '27b',
309
+ gate: {
310
+ ...loadResult.gate,
311
+ requestedBackend: 'webgpu',
312
+ selectedBackend: 'webgpu',
313
+ },
314
+ context: { ...loadResult.context, size: 8_448, layerCount: 65 },
315
+ tuning: {
316
+ scope: 'benchmark',
317
+ requested: {
318
+ nBatch: 32,
319
+ nUbatch: 16,
320
+ flashMode: 'auto',
321
+ cacheTypeK: 'q4_0',
322
+ cacheTypeV: 'q4_0',
323
+ wasmFlavor: 'auto',
324
+ },
325
+ observed: {
326
+ nBatch: 32,
327
+ nUbatch: 16,
328
+ flashAttention: true,
329
+ cacheTypeK: 'q4_0',
330
+ cacheTypeV: 'q4_0',
331
+ kvBufferBytes: 155_713_536,
332
+ wasmFlavor: 'jspi',
333
+ wasmSha256: JSPI_WASM_SHA256,
334
+ compatWorkerSha256: null,
335
+ },
336
+ applied: true,
337
+ },
338
+ backendReport: longContextBackendReport,
339
+ } satisfies LoadModelResult;
340
+
341
+ return observation({
342
+ model: {
343
+ ...model,
344
+ id: '27b',
345
+ displayName: 'Bonsai 27B',
346
+ architecture: 'qwen35',
347
+ } as ManifestModelV2,
348
+ requestedBackend: 'webgpu',
349
+ contextSize: 8_448,
350
+ workloadId: 'context-prefill-8k-v1',
351
+ generationRequest: {
352
+ messages: BENCHMARK_WORKLOADS['context-prefill-8k-v1'].messages.map((message) => ({ ...message })),
353
+ maxTokens: 8,
354
+ temperature: 0,
355
+ topP: 1,
356
+ topK: 1,
357
+ seed: 42,
358
+ toolChoice: 'none',
359
+ cachePrompt: false,
360
+ returnTokenIds: true,
361
+ },
362
+ loadResult: longContextLoadResult,
363
+ backendReport: longContextBackendReport,
364
+ streamedTokenEvents: 8,
365
+ generationResult: {
366
+ text: 'fixture',
367
+ reasoningText: '',
368
+ tokenIds: [201, 202, 203, 204, 205, 206, 207, 208],
369
+ tokenTrace: sampledTokenTrace([201, 202, 203, 204, 205, 206, 207, 208]),
370
+ finishReason: 'length',
371
+ toolCalls: [],
372
+ usage: { promptTokens: 8_314, completionTokens: 8, totalTokens: 8_322 },
373
+ timings: { promptTokensPerSecond: 20, predictedTokensPerSecond: 30 },
374
+ },
375
+ });
376
+ }
377
+
378
  describe('benchmark report', () => {
379
  it('classifies the shard cache observed at the first load boundary', () => {
380
  expect(classifyCacheState(null, 1_000)).toBe('unknown');
 
386
  it('builds a shareable report with pinned runtime evidence and fixed-safe policy', () => {
387
  const report = buildBenchmarkReport(observation());
388
 
389
+ expect(report.schemaVersion).toBe(3);
390
+ expect(report.model).toMatchObject({
391
+ source: model.source,
392
+ shards: model.files,
393
+ });
394
  expect(report.load).toMatchObject({
395
  tuning: {
396
  scope: 'release-defaults',
 
419
  warm: { durationMs: 234.5 },
420
  });
421
  expect(report.generation).toMatchObject({
422
+ workload: {
423
+ id: 'field-core-v1',
424
+ messages: BENCHMARK_WORKLOADS['field-core-v1'].messages,
425
+ },
426
+ sampling: {
427
+ maxTokens: 64,
428
+ temperature: 0,
429
+ topP: 1,
430
+ topK: 1,
431
+ minP: null,
432
+ seed: 42,
433
+ toolChoice: 'none',
434
+ cachePrompt: false,
435
+ },
436
+ tokenTrace: {
437
+ returnTokenIds: true,
438
+ logprobs: true,
439
+ topLogprobs: 5,
440
+ topAlternatives: 4,
441
+ encoding: 'uint32-le',
442
+ tokenIds: [101, 102, 103, 104, 105, 106, 107, 108],
443
+ entries: sampledTokenTrace([101, 102, 103, 104, 105, 106, 107, 108]),
444
+ checkpointPrefixes: [],
445
+ },
446
+ rawText: 'fixture',
447
+ rawReasoningText: '',
448
  promptTokensPerSecond: 123.46,
449
  decodeTokensPerSecond: 45.68,
450
  engineCompletionTokens: 8,
451
  streamedTokenEvents: 5,
452
  completionTokens: 8,
453
+ tokenCountSource: 'token-id-trace',
454
  });
455
  expect(report.execution).toMatchObject({
456
  selectedBackend: 'webgpu',
 
571
  });
572
  });
573
 
574
+ it('exports reasoning-only output separately from visible content', () => {
575
+ const input = observation();
576
+ input.generationResult.text = '';
577
+ input.generationResult.reasoningText = 'reasoning-only fixture';
578
+
579
+ const report = buildBenchmarkReport(input);
580
+
581
+ expect(report.generation.rawText).toBe('');
582
+ expect(report.generation.rawReasoningText).toBe('reasoning-only fixture');
583
+ });
584
+
585
  it('exports the selected model DFlash pairing for unsupported 1.7B and conditional 27B', () => {
586
  const reportFor = (modelId: ManifestModelV2['id']) => buildBenchmarkReport(observation({
587
  model: {
 
615
  expect(exportedPairing.targetLayerIdsZeroBased).not.toBe(dflash27bPairing.targetLayerIdsZeroBased);
616
  });
617
 
618
+ it('uses the exact token-id trace when final engine usage is present but incomplete', () => {
619
  const report = buildBenchmarkReport(observation({
620
  streamedTokenEvents: 64,
621
  generationResult: {
622
  text: 'fixture',
623
+ reasoningText: '',
624
+ tokenIds: Array.from({ length: 64 }, (_, index) => index + 1),
625
+ tokenTrace: sampledTokenTrace(Array.from({ length: 64 }, (_, index) => index + 1)),
626
  finishReason: 'length',
627
  toolCalls: [],
628
  usage: { promptTokens: 5, completionTokens: 0, totalTokens: 5 },
 
636
  completionTokens: 64,
637
  engineTotalTokens: 5,
638
  totalTokens: 69,
639
+ tokenCountSource: 'token-id-trace',
640
  });
641
+ expect(report.generation.tokenTrace.checkpointPrefixes).toEqual([
642
+ { tokens: 64, sha256: '0c8f462927e331f28e3f1a6d342957cd27118febc309bd3b2f646e2dfbaeec32' },
643
+ ]);
644
  });
645
 
646
+ it('uses the trace without emitting non-finite JSON metrics', () => {
647
  const report = buildBenchmarkReport(observation({
648
  generationElapsedMs: Number.NaN,
649
  timeToFirstTokenMs: Number.POSITIVE_INFINITY,
650
  generationResult: {
651
  text: 'fixture',
652
+ reasoningText: '',
653
+ tokenIds: [1, 2, 3, 4, 5],
654
+ tokenTrace: sampledTokenTrace([1, 2, 3, 4, 5]),
655
  finishReason: 'length',
656
  toolCalls: [],
657
  usage: null,
 
664
  timeToFirstTokenMs: null,
665
  decodeTokensPerSecond: null,
666
  completionTokens: 5,
667
+ tokenCountSource: 'token-id-trace',
668
  });
669
  expect(serializeBenchmarkReport(report)).not.toContain('NaN');
670
  expect(serializeBenchmarkReport(report)).toMatch(/\n$/);
671
  });
672
 
673
+ it('records all locked state-drift checkpoints from the exact token-id prefixes', () => {
674
+ const tokenIds = Array.from({ length: 1_024 }, (_, index) => index + 1);
675
+ const report = buildBenchmarkReport(observation({
676
+ workloadId: 'state-drift-1k-v1',
677
+ generationRequest: {
678
+ messages: BENCHMARK_WORKLOADS['state-drift-1k-v1'].messages.map((message) => ({ ...message })),
679
+ maxTokens: 1_024,
680
+ temperature: 0,
681
+ topP: 1,
682
+ topK: 1,
683
+ seed: 42,
684
+ toolChoice: 'none',
685
+ cachePrompt: false,
686
+ returnTokenIds: true,
687
+ },
688
+ streamedTokenEvents: 1_024,
689
+ generationResult: {
690
+ text: '1, 2, 3, …',
691
+ reasoningText: '',
692
+ tokenIds,
693
+ tokenTrace: sampledTokenTrace(tokenIds),
694
+ finishReason: 'length',
695
+ toolCalls: [],
696
+ usage: { promptTokens: 20, completionTokens: 1_024, totalTokens: 1_044 },
697
+ timings: { promptTokensPerSecond: 20, predictedTokensPerSecond: 30 },
698
+ },
699
+ teacherForcedReferenceScore: teacherForcedReferenceScore(tokenIds),
700
+ }));
701
+
702
+ expect(report.generation.workload).toEqual({
703
+ id: 'state-drift-1k-v1',
704
+ messages: BENCHMARK_WORKLOADS['state-drift-1k-v1'].messages,
705
+ });
706
+ expect(report.generation.rawText).toBe('1, 2, 3, …');
707
+ expect(report.generation.rawReasoningText).toBe('');
708
+ expect(report.generation.tokenTrace.tokenIds).toEqual(tokenIds);
709
+ expect(report.generation.tokenTrace.entries).toHaveLength(1_024);
710
+ expect(report.generation.teacherForcedReferenceScore).toMatchObject({
711
+ fixture: { kind: 'bonsai-state-drift-reference' },
712
+ score: { summary: { tokenCount: 1_024, meanNll: 0.1 } },
713
+ });
714
+ expect(report.generation.tokenTrace.checkpointPrefixes).toEqual([
715
+ { tokens: 64, sha256: '0c8f462927e331f28e3f1a6d342957cd27118febc309bd3b2f646e2dfbaeec32' },
716
+ { tokens: 128, sha256: '24f9ac547baae524ba0ea5220692d48f7526cdb1df5e99edcbb1f32239a8d5f5' },
717
+ { tokens: 256, sha256: '80fa0f6d1caca9aad2b012051399b33bcd1976b145f3f3eea0f7ba10637761b0' },
718
+ { tokens: 512, sha256: '8ab9b2cf36ec9e9d68711df73334731d2fee0552f5d95d76538b1d2cdcefd564' },
719
+ { tokens: 768, sha256: '23905fc64dc47867f49672959cddf676ca257967f5786eefaf8583ddf7ddf3e9' },
720
+ { tokens: 1_024, sha256: '6b8b6bd30ff821daf5db90cc071525f5696e366efd1aeb30d0bf60f785a3c26d' },
721
+ ]);
722
+ });
723
+
724
+ it('exports null teacher-forced scoring for non-state-drift workloads', () => {
725
+ expect(buildBenchmarkReport(observation()).generation.teacherForcedReferenceScore).toBeNull();
726
+ });
727
+
728
+ it('rejects incomplete state-drift traces instead of exporting partial evidence', () => {
729
+ expect(() => buildBenchmarkReport(observation({
730
+ workloadId: 'state-drift-1k-v1',
731
+ generationRequest: {
732
+ messages: BENCHMARK_WORKLOADS['state-drift-1k-v1'].messages.map((message) => ({ ...message })),
733
+ maxTokens: 1_024,
734
+ temperature: 0,
735
+ topP: 1,
736
+ topK: 1,
737
+ seed: 42,
738
+ toolChoice: 'none',
739
+ cachePrompt: false,
740
+ returnTokenIds: true,
741
+ },
742
+ generationResult: {
743
+ text: 'partial fixture',
744
+ reasoningText: '',
745
+ tokenIds: Array.from({ length: 1_023 }, (_, index) => index + 1),
746
+ tokenTrace: sampledTokenTrace(Array.from({ length: 1_023 }, (_, index) => index + 1)),
747
+ finishReason: 'length',
748
+ toolCalls: [],
749
+ usage: { promptTokens: 20, completionTokens: 1_023, totalTokens: 1_043 },
750
+ timings: null,
751
+ },
752
+ }))).toThrow('requires exactly 1,024 sampled token ids');
753
+ });
754
+
755
+ it('rejects state-drift traces that stop before exhausting the locked budget', () => {
756
+ const input = observation({
757
+ workloadId: 'state-drift-1k-v1',
758
+ generationRequest: {
759
+ messages: BENCHMARK_WORKLOADS['state-drift-1k-v1'].messages.map((message) => ({ ...message })),
760
+ maxTokens: 1_024,
761
+ temperature: 0,
762
+ topP: 1,
763
+ topK: 1,
764
+ seed: 42,
765
+ toolChoice: 'none',
766
+ cachePrompt: false,
767
+ returnTokenIds: true,
768
+ },
769
+ generationResult: {
770
+ text: 'early stop',
771
+ reasoningText: '',
772
+ tokenIds: Array.from({ length: 1_024 }, (_, index) => index + 1),
773
+ tokenTrace: sampledTokenTrace(Array.from({ length: 1_024 }, (_, index) => index + 1)),
774
+ finishReason: 'stop',
775
+ toolCalls: [],
776
+ usage: { promptTokens: 20, completionTokens: 1_024, totalTokens: 1_044 },
777
+ timings: null,
778
+ },
779
+ });
780
+
781
+ expect(() => buildBenchmarkReport(input)).toThrow('requires finishReason=length');
782
+ });
783
+
784
+ it('exports long-context evidence only after processing more than 8K prompt tokens', () => {
785
+ const report = buildBenchmarkReport(longContextObservation());
786
+
787
+ expect(BENCHMARK_WORKLOADS['context-prefill-8k-v1'].messages).toEqual([{
788
+ role: 'user',
789
+ content: `Continue the pattern.${' x'.repeat(8_300)}`,
790
+ }]);
791
+ expect(report).toMatchObject({
792
+ model: { id: '27b', contextSize: 8_448 },
793
+ generation: {
794
+ workload: { id: 'context-prefill-8k-v1' },
795
+ sampling: { maxTokens: 8 },
796
+ promptTokens: 8_314,
797
+ completionTokens: 8,
798
+ },
799
+ load: {
800
+ tuning: {
801
+ scope: 'benchmark',
802
+ requested: { flashMode: 'auto', cacheTypeK: 'q4_0', cacheTypeV: 'q4_0' },
803
+ observed: { flashAttention: true, cacheTypeK: 'q4_0', cacheTypeV: 'q4_0' },
804
+ applied: true,
805
+ },
806
+ },
807
+ execution: { requestedBackend: 'webgpu', selectedBackend: 'webgpu' },
808
+ });
809
+ expect(report.generation.tokenTrace.tokenIds).toHaveLength(8);
810
+ });
811
+
812
+ it('rejects long-context evidence at or below 8,192 engine-counted prompt tokens', () => {
813
+ const input = longContextObservation();
814
+ input.generationResult.usage = { promptTokens: 8_192, completionTokens: 8, totalTokens: 8_200 };
815
+
816
+ expect(() => buildBenchmarkReport(input)).toThrow(
817
+ 'requires more than 8,192 engine-counted prompt tokens',
818
+ );
819
+ });
820
+
821
+ it('rejects a long-context trace that stops before exhausting the 8-token budget', () => {
822
+ const input = longContextObservation();
823
+ input.generationResult.finishReason = 'stop';
824
+
825
+ expect(() => buildBenchmarkReport(input)).toThrow('requires finishReason=length');
826
+ });
827
+
828
+ it('rejects long-context evidence with the wrong context or runtime tuning', () => {
829
+ const wrongContext = longContextObservation();
830
+ wrongContext.contextSize = 8_447;
831
+ wrongContext.loadResult = {
832
+ ...wrongContext.loadResult,
833
+ context: { ...wrongContext.loadResult.context, size: 8_447 },
834
+ };
835
+ expect(() => buildBenchmarkReport(wrongContext)).toThrow(
836
+ 'requires Bonsai 27B at context 8,448',
837
+ );
838
+
839
+ const wrongTuning = longContextObservation();
840
+ wrongTuning.loadResult = {
841
+ ...wrongTuning.loadResult,
842
+ tuning: {
843
+ ...wrongTuning.loadResult.tuning,
844
+ requested: {
845
+ ...wrongTuning.loadResult.tuning.requested,
846
+ cacheTypeK: 'q8_0',
847
+ cacheTypeV: 'q8_0',
848
+ },
849
+ observed: {
850
+ ...wrongTuning.loadResult.tuning.observed,
851
+ cacheTypeK: 'q8_0',
852
+ cacheTypeV: 'q8_0',
853
+ },
854
+ },
855
+ };
856
+ expect(() => buildBenchmarkReport(wrongTuning)).toThrow(
857
+ 'requires Bonsai 27B at context 8,448',
858
+ );
859
+ });
860
+
861
+ it('rejects missing and malformed token-id traces', () => {
862
+ const missingTrace = observation();
863
+ missingTrace.generationResult.tokenIds = null;
864
+ expect(() => buildBenchmarkReport(missingTrace)).toThrow('without a token-id trace');
865
+
866
+ const malformedTrace = observation();
867
+ malformedTrace.generationResult.tokenIds = [1, 2.5];
868
+ expect(() => buildBenchmarkReport(malformedTrace)).toThrow('token id 1 is invalid');
869
+ });
870
+
871
+ it('rejects missing, mismatched, and unsorted sampled-token logprob traces', () => {
872
+ const missingTrace = observation();
873
+ missingTrace.generationResult.tokenTrace = null;
874
+ expect(() => buildBenchmarkReport(missingTrace)).toThrow('without a sampled-token logprob trace');
875
+
876
+ const mismatchedTrace = observation();
877
+ mismatchedTrace.generationResult.tokenTrace?.pop();
878
+ expect(() => buildBenchmarkReport(mismatchedTrace)).toThrow(
879
+ 'sampled-token trace length 7 does not match token-id length 8',
880
+ );
881
+
882
+ const unsortedTrace = observation();
883
+ const candidates = unsortedTrace.generationResult.tokenTrace?.[0]?.topCandidates;
884
+ if (!candidates) throw new Error('Unsorted trace fixture requires top candidates.');
885
+ [candidates[0], candidates[1]] = [candidates[1]!, candidates[0]!];
886
+ expect(() => buildBenchmarkReport(unsortedTrace)).toThrow('candidates are not sorted');
887
+ });
888
+
889
  it('uses the model, selected backend, and UTC completion time in the export name', () => {
890
  const report = buildBenchmarkReport(observation());
891
  expect(benchmarkReportFilename(report)).toBe(
src/bench/report.ts CHANGED
@@ -1,15 +1,65 @@
 
 
1
  import type {
2
  BackendReport,
3
  EngineCapabilities,
 
 
 
4
  GenerateResult,
5
  LoadModelResult,
6
  ManifestModelV2,
7
  ModelManifestV2,
8
  RequestedBackend,
 
9
  } from '../engine';
 
10
 
11
- export const BENCHMARK_PROMPT_ID = 'bonsai-field-core-v1';
12
- export const BENCHMARK_PROMPT = 'Describe how a tree survives one dry season in exactly eight short, factual sentences.';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  export type CacheState = 'empty' | 'partial' | 'cached' | 'unknown';
15
 
@@ -21,7 +71,8 @@ export interface BenchmarkObservation {
21
  capabilities: EngineCapabilities;
22
  requestedBackend: RequestedBackend;
23
  contextSize: number;
24
- maxTokens: number;
 
25
  coldLoadMs: number;
26
  warmLoadMs: number;
27
  coldCachedBytes: number | null;
@@ -32,10 +83,16 @@ export interface BenchmarkObservation {
32
  timeToFirstTokenMs: number | null;
33
  streamedTokenEvents: number;
34
  backendReport: BackendReport;
 
 
 
 
 
 
35
  }
36
 
37
  export interface BenchmarkReport {
38
- schemaVersion: 2;
39
  kind: 'bonsai-browser-benchmark';
40
  startedAt: string;
41
  completedAt: string;
@@ -43,6 +100,8 @@ export interface BenchmarkReport {
43
  id: ManifestModelV2['id'];
44
  displayName: string;
45
  architecture: string;
 
 
46
  downloadBytes: number;
47
  contextSize: number;
48
  };
@@ -60,11 +119,36 @@ export interface BenchmarkReport {
60
  };
61
  };
62
  generation: {
63
- promptId: typeof BENCHMARK_PROMPT_ID;
64
- prompt: typeof BENCHMARK_PROMPT;
65
- maxTokens: number;
66
- temperature: 0;
67
- seed: 42;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  elapsedMs: number | null;
69
  timeToFirstTokenMs: number | null;
70
  promptTokensPerSecond: number | null;
@@ -75,7 +159,7 @@ export interface BenchmarkReport {
75
  completionTokens: number;
76
  engineTotalTokens: number | null;
77
  totalTokens: number | null;
78
- tokenCountSource: 'engine-usage' | 'stream-events-fallback';
79
  finishReason: GenerateResult['finishReason'];
80
  };
81
  execution: {
@@ -141,8 +225,289 @@ export function classifyCacheState(observedBytes: number | null, totalBytes: num
141
  return 'partial';
142
  }
143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  export function buildBenchmarkReport(observation: BenchmarkObservation): BenchmarkReport {
145
  const { generationResult, capabilities, backendReport, loadResult } = observation;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  const usage = generationResult.usage;
147
  const engineCompletionTokens = usage
148
  && Number.isFinite(usage.completionTokens)
@@ -152,10 +517,47 @@ export function buildBenchmarkReport(observation: BenchmarkObservation): Benchma
152
  const streamedTokenEvents = Number.isFinite(observation.streamedTokenEvents)
153
  ? Math.floor(Math.max(0, observation.streamedTokenEvents))
154
  : 0;
155
- const completionTokens = Math.max(engineCompletionTokens ?? 0, streamedTokenEvents);
156
  const promptTokens = usage && Number.isFinite(usage.promptTokens) && usage.promptTokens >= 0
157
  ? Math.floor(usage.promptTokens)
158
  : null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  const engineTotalTokens = usage && Number.isFinite(usage.totalTokens) && usage.totalTokens >= 0
160
  ? Math.floor(usage.totalTokens)
161
  : null;
@@ -172,7 +574,7 @@ export function buildBenchmarkReport(observation: BenchmarkObservation): Benchma
172
  const dflashPairing = observation.manifest.dflash.pairings[observation.model.id];
173
 
174
  return {
175
- schemaVersion: 2,
176
  kind: 'bonsai-browser-benchmark',
177
  startedAt: observation.startedAt,
178
  completedAt: observation.completedAt,
@@ -180,6 +582,8 @@ export function buildBenchmarkReport(observation: BenchmarkObservation): Benchma
180
  id: observation.model.id,
181
  displayName: observation.model.displayName,
182
  architecture: observation.model.architecture,
 
 
183
  downloadBytes: observation.model.downloadBytes,
184
  contextSize: observation.contextSize,
185
  },
@@ -202,11 +606,33 @@ export function buildBenchmarkReport(observation: BenchmarkObservation): Benchma
202
  },
203
  },
204
  generation: {
205
- promptId: BENCHMARK_PROMPT_ID,
206
- prompt: BENCHMARK_PROMPT,
207
- maxTokens: observation.maxTokens,
208
- temperature: 0,
209
- seed: 42,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  elapsedMs: roundedNonNegative(observation.generationElapsedMs, 1),
211
  timeToFirstTokenMs: roundedNonNegative(observation.timeToFirstTokenMs, 1),
212
  promptTokensPerSecond: roundedNonNegative(
@@ -220,9 +646,7 @@ export function buildBenchmarkReport(observation: BenchmarkObservation): Benchma
220
  completionTokens,
221
  engineTotalTokens,
222
  totalTokens,
223
- tokenCountSource: engineCompletionTokens !== null && engineCompletionTokens >= streamedTokenEvents
224
- ? 'engine-usage'
225
- : 'stream-events-fallback',
226
  finishReason: generationResult.finishReason,
227
  },
228
  execution: {
 
1
+ import { sha256 } from '@noble/hashes/sha2.js';
2
+ import { bytesToHex } from '@noble/hashes/utils.js';
3
  import type {
4
  BackendReport,
5
  EngineCapabilities,
6
+ EngineChatMessage,
7
+ EngineSampledTokenTraceEntry,
8
+ GenerateParams,
9
  GenerateResult,
10
  LoadModelResult,
11
  ManifestModelV2,
12
  ModelManifestV2,
13
  RequestedBackend,
14
+ ScoreSequenceResult,
15
  } from '../engine';
16
+ import type { StateDriftReferenceFixture } from './state-drift-reference';
17
 
18
+ export const BENCHMARK_WORKLOADS = {
19
+ 'field-core-v1': {
20
+ id: 'field-core-v1',
21
+ label: 'Field core · short',
22
+ description: 'Stable throughput workload; completion length remains operator-selected.',
23
+ requiredMaxTokens: null,
24
+ messages: [{
25
+ role: 'user',
26
+ content: 'Describe how a tree survives one dry season in exactly eight short, factual sentences.',
27
+ }],
28
+ },
29
+ 'state-drift-1k-v1': {
30
+ id: 'state-drift-1k-v1',
31
+ label: 'State drift · 1K',
32
+ description: 'P2.5 recurrent-state evidence; requires an exact 1,024-token trace.',
33
+ requiredMaxTokens: 1_024,
34
+ messages: [{
35
+ role: 'user',
36
+ content: 'Output consecutive integers starting at 1, separated only by a comma and one space. Continue without explanation until you reach 500.',
37
+ }],
38
+ },
39
+ 'context-prefill-8k-v1': {
40
+ id: 'context-prefill-8k-v1',
41
+ label: 'Context prefill · >8K',
42
+ description: 'P2.5 long-context evidence; requires more than 8,192 engine-counted prompt tokens.',
43
+ requiredMaxTokens: 8,
44
+ messages: [{
45
+ role: 'user',
46
+ content: `Continue the pattern.${' x'.repeat(8_300)}`,
47
+ }],
48
+ },
49
+ } as const satisfies Record<string, {
50
+ id: string;
51
+ label: string;
52
+ description: string;
53
+ requiredMaxTokens: number | null;
54
+ messages: readonly EngineChatMessage[];
55
+ }>;
56
+
57
+ export type BenchmarkWorkloadId = keyof typeof BENCHMARK_WORKLOADS;
58
+ export const DEFAULT_BENCHMARK_WORKLOAD_ID: BenchmarkWorkloadId = 'field-core-v1';
59
+ export const BENCHMARK_PROMPT_ID = DEFAULT_BENCHMARK_WORKLOAD_ID;
60
+ export const BENCHMARK_PROMPT = BENCHMARK_WORKLOADS[DEFAULT_BENCHMARK_WORKLOAD_ID].messages[0].content;
61
+ export const TOKEN_ID_CHECKPOINTS = [64, 128, 256, 512, 768, 1_024] as const;
62
+ export type TokenIdCheckpointSize = typeof TOKEN_ID_CHECKPOINTS[number];
63
 
64
  export type CacheState = 'empty' | 'partial' | 'cached' | 'unknown';
65
 
 
71
  capabilities: EngineCapabilities;
72
  requestedBackend: RequestedBackend;
73
  contextSize: number;
74
+ workloadId: BenchmarkWorkloadId;
75
+ generationRequest: GenerateParams;
76
  coldLoadMs: number;
77
  warmLoadMs: number;
78
  coldCachedBytes: number | null;
 
83
  timeToFirstTokenMs: number | null;
84
  streamedTokenEvents: number;
85
  backendReport: BackendReport;
86
+ teacherForcedReferenceScore?: TeacherForcedReferenceScoreEvidence | null;
87
+ }
88
+
89
+ export interface TeacherForcedReferenceScoreEvidence {
90
+ fixture: StateDriftReferenceFixture;
91
+ score: ScoreSequenceResult;
92
  }
93
 
94
  export interface BenchmarkReport {
95
+ schemaVersion: 3;
96
  kind: 'bonsai-browser-benchmark';
97
  startedAt: string;
98
  completedAt: string;
 
100
  id: ManifestModelV2['id'];
101
  displayName: string;
102
  architecture: string;
103
+ source: ManifestModelV2['source'];
104
+ shards: Array<Pick<ManifestModelV2['files'][number], 'path' | 'bytes' | 'sha256'>>;
105
  downloadBytes: number;
106
  contextSize: number;
107
  };
 
119
  };
120
  };
121
  generation: {
122
+ workload: {
123
+ id: BenchmarkWorkloadId;
124
+ messages: EngineChatMessage[];
125
+ };
126
+ sampling: {
127
+ maxTokens: number;
128
+ temperature: number;
129
+ topP: number | null;
130
+ topK: number | null;
131
+ minP: number | null;
132
+ seed: number;
133
+ toolChoice: GenerateParams['toolChoice'] | null;
134
+ cachePrompt: boolean;
135
+ };
136
+ tokenTrace: {
137
+ returnTokenIds: true;
138
+ logprobs: true;
139
+ topLogprobs: 5;
140
+ topAlternatives: 4;
141
+ encoding: 'uint32-le';
142
+ tokenIds: number[];
143
+ entries: EngineSampledTokenTraceEntry[];
144
+ checkpointPrefixes: Array<{
145
+ tokens: TokenIdCheckpointSize;
146
+ sha256: string;
147
+ }>;
148
+ };
149
+ teacherForcedReferenceScore: TeacherForcedReferenceScoreEvidence | null;
150
+ rawText: string;
151
+ rawReasoningText: string;
152
  elapsedMs: number | null;
153
  timeToFirstTokenMs: number | null;
154
  promptTokensPerSecond: number | null;
 
159
  completionTokens: number;
160
  engineTotalTokens: number | null;
161
  totalTokens: number | null;
162
+ tokenCountSource: 'token-id-trace';
163
  finishReason: GenerateResult['finishReason'];
164
  };
165
  execution: {
 
225
  return 'partial';
226
  }
227
 
228
+ function cloneMessages(messages: readonly EngineChatMessage[]): EngineChatMessage[] {
229
+ return messages.map((message) => message.role === 'assistant' && message.tool_calls
230
+ ? {
231
+ ...message,
232
+ tool_calls: message.tool_calls.map((call) => ({
233
+ ...call,
234
+ function: { ...call.function },
235
+ })),
236
+ }
237
+ : { ...message });
238
+ }
239
+
240
+ function checkpointPrefixSha256(tokenIds: readonly number[], tokens: number): string {
241
+ const bytes = new Uint8Array(tokens * Uint32Array.BYTES_PER_ELEMENT);
242
+ const view = new DataView(bytes.buffer);
243
+ for (let index = 0; index < tokens; index += 1) {
244
+ view.setUint32(index * Uint32Array.BYTES_PER_ELEMENT, tokenIds[index]!, true);
245
+ }
246
+ return bytesToHex(sha256(bytes));
247
+ }
248
+
249
+ function buildCheckpointPrefixes(tokenIds: readonly number[]): BenchmarkReport['generation']['tokenTrace']['checkpointPrefixes'] {
250
+ return TOKEN_ID_CHECKPOINTS
251
+ .filter((tokens) => tokens <= tokenIds.length)
252
+ .map((tokens) => ({ tokens, sha256: checkpointPrefixSha256(tokenIds, tokens) }));
253
+ }
254
+
255
+ function cloneValidatedTokenTrace(
256
+ value: GenerateResult['tokenTrace'],
257
+ tokenIds: readonly number[],
258
+ ): EngineSampledTokenTraceEntry[] {
259
+ if (value === null) {
260
+ throw new Error('Benchmark generation completed without a sampled-token logprob trace.');
261
+ }
262
+ if (value.length !== tokenIds.length) {
263
+ throw new Error(
264
+ `Benchmark sampled-token trace length ${value.length} does not match token-id length ${tokenIds.length}.`,
265
+ );
266
+ }
267
+ return value.map((entry, index) => {
268
+ const selected = { ...entry.selected };
269
+ if (selected.id !== tokenIds[index]) {
270
+ throw new Error(`Benchmark sampled-token trace entry ${index} does not match its token id.`);
271
+ }
272
+ if (typeof selected.logprob !== 'number' || !Number.isFinite(selected.logprob)) {
273
+ throw new Error(`Benchmark sampled-token trace entry ${index} has an invalid selected logprob.`);
274
+ }
275
+ if (!Array.isArray(entry.topCandidates) || entry.topCandidates.length !== 5) {
276
+ throw new Error(`Benchmark sampled-token trace entry ${index} must contain exactly five candidates.`);
277
+ }
278
+ const seenIds = new Set<number>();
279
+ const topCandidates = entry.topCandidates.map((candidate, candidateIndex) => {
280
+ if (!Number.isSafeInteger(candidate.id) || candidate.id < 0 || candidate.id > 0xffff_ffff) {
281
+ throw new Error(
282
+ `Benchmark sampled-token trace entry ${index} candidate ${candidateIndex} has an invalid id.`,
283
+ );
284
+ }
285
+ if (seenIds.has(candidate.id)) {
286
+ throw new Error(`Benchmark sampled-token trace entry ${index} has duplicate candidate ids.`);
287
+ }
288
+ seenIds.add(candidate.id);
289
+ if (typeof candidate.logprob !== 'number' || !Number.isFinite(candidate.logprob)) {
290
+ throw new Error(
291
+ `Benchmark sampled-token trace entry ${index} candidate ${candidateIndex} has an invalid logprob.`,
292
+ );
293
+ }
294
+ const previous = entry.topCandidates[candidateIndex - 1];
295
+ if (previous && (
296
+ previous.logprob < candidate.logprob
297
+ || (previous.logprob === candidate.logprob && previous.id > candidate.id)
298
+ )) {
299
+ throw new Error(`Benchmark sampled-token trace entry ${index} candidates are not sorted.`);
300
+ }
301
+ return { ...candidate };
302
+ });
303
+ const selectedCandidate = topCandidates.find((candidate) => candidate.id === selected.id);
304
+ if (!selectedCandidate || selectedCandidate.logprob !== selected.logprob) {
305
+ throw new Error(
306
+ `Benchmark sampled-token trace entry ${index} does not include its selected token and logprob.`,
307
+ );
308
+ }
309
+ return { selected, topCandidates };
310
+ });
311
+ }
312
+
313
+ function cloneTeacherForcedReferenceScore(
314
+ value: TeacherForcedReferenceScoreEvidence | null | undefined,
315
+ workloadId: BenchmarkWorkloadId,
316
+ ): TeacherForcedReferenceScoreEvidence | null {
317
+ if (workloadId !== 'state-drift-1k-v1') {
318
+ if (value != null) {
319
+ throw new Error('Teacher-forced reference scoring is only valid for state-drift-1k-v1.');
320
+ }
321
+ return null;
322
+ }
323
+ if (value == null) {
324
+ throw new Error('state-drift-1k-v1 requires a complete teacher-forced CPU reference score.');
325
+ }
326
+ const { fixture, score } = value;
327
+ if (
328
+ fixture.schemaVersion !== 1
329
+ || fixture.kind !== 'bonsai-state-drift-reference'
330
+ || fixture.workload.id !== 'state-drift-1k-v1'
331
+ || fixture.tokenEncoding !== 'uint32-le'
332
+ || fixture.promptTokenIds.length !== 38
333
+ || fixture.referenceTokenIds.length !== 1_024
334
+ ) {
335
+ throw new Error('state-drift-1k-v1 received an invalid CPU reference fixture.');
336
+ }
337
+ const expectedMethod: ScoreSequenceResult['method'] = {
338
+ promptMode: 'raw-token-id-prefix',
339
+ maxTokensPerStep: 1,
340
+ temperature: 0,
341
+ topK: 1,
342
+ reportedTopLogprobs: 5,
343
+ logitBias: 1_000,
344
+ cachePromptFirst: false,
345
+ cachePromptSubsequent: true,
346
+ };
347
+ if (JSON.stringify(score.method) !== JSON.stringify(expectedMethod)) {
348
+ throw new Error('state-drift-1k-v1 teacher-forced method metadata is not locked.');
349
+ }
350
+ if (score.entries.length !== 1_024 || score.summary.tokenCount !== 1_024) {
351
+ throw new Error('state-drift-1k-v1 requires exactly 1,024 teacher-forced score entries.');
352
+ }
353
+ const entries = score.entries.map((entry, index) => {
354
+ const referenceTokenId = fixture.referenceTokenIds[index];
355
+ if (entry.index !== index || entry.selectedReference.id !== referenceTokenId) {
356
+ throw new Error(`Teacher-forced score entry ${index} does not select its CPU reference token.`);
357
+ }
358
+ if (!Number.isFinite(entry.selectedReference.logprob)) {
359
+ throw new Error(`Teacher-forced score entry ${index} has a non-finite reference logprob.`);
360
+ }
361
+ if (!Array.isArray(entry.topCandidates) || entry.topCandidates.length !== 5) {
362
+ throw new Error(`Teacher-forced score entry ${index} must contain five natural candidates.`);
363
+ }
364
+ const seenIds = new Set<number>();
365
+ const topCandidates = entry.topCandidates.map((candidate, candidateIndex) => {
366
+ if (
367
+ !Number.isSafeInteger(candidate.id)
368
+ || candidate.id < 0
369
+ || !Number.isFinite(candidate.logprob)
370
+ || seenIds.has(candidate.id)
371
+ ) {
372
+ throw new Error(`Teacher-forced score entry ${index} candidate ${candidateIndex} is invalid.`);
373
+ }
374
+ seenIds.add(candidate.id);
375
+ const previous = entry.topCandidates[candidateIndex - 1];
376
+ if (previous && (
377
+ previous.logprob < candidate.logprob
378
+ || (previous.logprob === candidate.logprob && previous.id > candidate.id)
379
+ )) {
380
+ throw new Error(`Teacher-forced score entry ${index} candidates are not sorted.`);
381
+ }
382
+ return { ...candidate };
383
+ });
384
+ if (
385
+ entry.naturalTop1.id !== topCandidates[0]?.id
386
+ || entry.naturalTop1.logprob !== topCandidates[0]?.logprob
387
+ ) {
388
+ throw new Error(`Teacher-forced score entry ${index} has inconsistent natural top-1 data.`);
389
+ }
390
+ const referenceRank = topCandidates.findIndex((candidate) => candidate.id === referenceTokenId);
391
+ const normalizedReferenceRank = referenceRank === -1 ? null : referenceRank;
392
+ if (entry.referenceRankInTopCandidatesZeroBased !== normalizedReferenceRank) {
393
+ throw new Error(`Teacher-forced score entry ${index} has an invalid reference rank.`);
394
+ }
395
+ const margin = topCandidates[0]!.logprob - topCandidates[1]!.logprob;
396
+ if (!Number.isFinite(entry.top1Top2Margin) || Math.abs(entry.top1Top2Margin - margin) > 1e-12) {
397
+ throw new Error(`Teacher-forced score entry ${index} has an invalid top-1/top-2 margin.`);
398
+ }
399
+ return {
400
+ index,
401
+ selectedReference: { ...entry.selectedReference },
402
+ naturalTop1: { ...entry.naturalTop1 },
403
+ topCandidates,
404
+ referenceRankInTopCandidatesZeroBased: normalizedReferenceRank,
405
+ top1Top2Margin: entry.top1Top2Margin,
406
+ };
407
+ });
408
+ const meanNll = -entries.reduce(
409
+ (sum, entry) => sum + entry.selectedReference.logprob,
410
+ 0,
411
+ ) / entries.length;
412
+ const perplexity = Math.exp(meanNll);
413
+ if (
414
+ !Number.isFinite(score.summary.meanNll)
415
+ || !Number.isFinite(score.summary.perplexity)
416
+ || Math.abs(score.summary.meanNll - meanNll) > 1e-12
417
+ || Math.abs(score.summary.perplexity - perplexity) > Math.max(1, perplexity) * 1e-12
418
+ ) {
419
+ throw new Error('Teacher-forced score summary does not match its reference logprobs.');
420
+ }
421
+
422
+ return {
423
+ fixture: {
424
+ schemaVersion: 1,
425
+ kind: 'bonsai-state-drift-reference',
426
+ workload: {
427
+ id: 'state-drift-1k-v1',
428
+ messages: fixture.workload.messages.map((message) => ({ ...message })),
429
+ },
430
+ provenance: {
431
+ sourceEvidence: fixture.provenance.sourceEvidence,
432
+ engineRevision: fixture.provenance.engineRevision,
433
+ nativeBinary: { ...fixture.provenance.nativeBinary },
434
+ model: { ...fixture.provenance.model },
435
+ renderedPromptSha256: fixture.provenance.renderedPromptSha256,
436
+ execution: { ...fixture.provenance.execution },
437
+ },
438
+ tokenEncoding: 'uint32-le',
439
+ promptTokenIds: [...fixture.promptTokenIds],
440
+ promptTokenIdsSha256: fixture.promptTokenIdsSha256,
441
+ referenceTokenIds: [...fixture.referenceTokenIds],
442
+ referenceTokenIdsSha256: fixture.referenceTokenIdsSha256,
443
+ checkpointPrefixes: fixture.checkpointPrefixes.map((checkpoint) => ({ ...checkpoint })),
444
+ },
445
+ score: {
446
+ method: { ...expectedMethod },
447
+ entries,
448
+ summary: {
449
+ tokenCount: 1_024,
450
+ meanNll: score.summary.meanNll,
451
+ perplexity: score.summary.perplexity,
452
+ },
453
+ },
454
+ };
455
+ }
456
+
457
  export function buildBenchmarkReport(observation: BenchmarkObservation): BenchmarkReport {
458
  const { generationResult, capabilities, backendReport, loadResult } = observation;
459
+ const workload = BENCHMARK_WORKLOADS[observation.workloadId];
460
+ const generationRequest = observation.generationRequest;
461
+ const maxTokens = generationRequest.maxTokens ?? 512;
462
+ const temperature = generationRequest.temperature ?? 0;
463
+ const topP = generationRequest.topP ?? null;
464
+ const topK = generationRequest.topK ?? 1;
465
+ const minP = generationRequest.minP ?? null;
466
+ const seed = generationRequest.seed ?? 42;
467
+ const toolChoice = generationRequest.toolChoice ?? null;
468
+ const cachePrompt = generationRequest.cachePrompt ?? true;
469
+ if (generationRequest.returnTokenIds !== true) {
470
+ throw new Error('Benchmark reports require returnTokenIds=true.');
471
+ }
472
+ if (JSON.stringify(generationRequest.messages) !== JSON.stringify(workload.messages)) {
473
+ throw new Error(`Benchmark messages do not match workload ${workload.id}.`);
474
+ }
475
+ if (generationResult.tokenIds === null) {
476
+ throw new Error('Benchmark generation completed without a token-id trace.');
477
+ }
478
+ const tokenIds = [...generationResult.tokenIds];
479
+ for (const [index, tokenId] of tokenIds.entries()) {
480
+ if (!Number.isSafeInteger(tokenId) || tokenId < 0 || tokenId > 0xffff_ffff) {
481
+ throw new Error(`Benchmark token id ${index} is invalid.`);
482
+ }
483
+ }
484
+ const tokenTrace = cloneValidatedTokenTrace(generationResult.tokenTrace, tokenIds);
485
+ if (typeof generationResult.reasoningText !== 'string') {
486
+ throw new Error('Benchmark generation completed without a reasoning-text channel.');
487
+ }
488
+ const checkpointPrefixes = buildCheckpointPrefixes(tokenIds);
489
+ if (workload.requiredMaxTokens !== null) {
490
+ if (
491
+ maxTokens !== workload.requiredMaxTokens
492
+ || temperature !== 0
493
+ || topP !== 1
494
+ || topK !== 1
495
+ || minP !== null
496
+ || seed !== 42
497
+ || toolChoice !== 'none'
498
+ || cachePrompt !== false
499
+ ) {
500
+ throw new Error(`${workload.id} requires its locked greedy sampling configuration.`);
501
+ }
502
+ }
503
+ if (workload.id === 'state-drift-1k-v1') {
504
+ if (tokenIds.length !== workload.requiredMaxTokens || checkpointPrefixes.length !== TOKEN_ID_CHECKPOINTS.length) {
505
+ throw new Error(`${workload.id} requires exactly 1,024 sampled token ids and all checkpoint hashes.`);
506
+ }
507
+ if (generationResult.finishReason !== 'length') {
508
+ throw new Error(`${workload.id} requires finishReason=length after exhausting its 1,024-token budget.`);
509
+ }
510
+ }
511
  const usage = generationResult.usage;
512
  const engineCompletionTokens = usage
513
  && Number.isFinite(usage.completionTokens)
 
517
  const streamedTokenEvents = Number.isFinite(observation.streamedTokenEvents)
518
  ? Math.floor(Math.max(0, observation.streamedTokenEvents))
519
  : 0;
520
+ const completionTokens = tokenIds.length;
521
  const promptTokens = usage && Number.isFinite(usage.promptTokens) && usage.promptTokens >= 0
522
  ? Math.floor(usage.promptTokens)
523
  : null;
524
+ if (workload.id === 'context-prefill-8k-v1') {
525
+ const tuning = loadResult.tuning;
526
+ const exactRuntimePolicy = observation.model.id === '27b'
527
+ && loadResult.modelId === '27b'
528
+ && observation.contextSize === 8_448
529
+ && loadResult.context.size === 8_448
530
+ && observation.requestedBackend === 'webgpu'
531
+ && loadResult.backend === 'webgpu'
532
+ && tuning.scope === 'benchmark'
533
+ && tuning.requested.flashMode === 'auto'
534
+ && tuning.requested.cacheTypeK === 'q4_0'
535
+ && tuning.requested.cacheTypeV === 'q4_0'
536
+ && tuning.observed.flashAttention === true
537
+ && tuning.observed.cacheTypeK === 'q4_0'
538
+ && tuning.observed.cacheTypeV === 'q4_0'
539
+ && tuning.observed.kvBufferBytes !== null
540
+ && tuning.observed.kvBufferBytes > 0
541
+ && tuning.applied;
542
+ if (!exactRuntimePolicy) {
543
+ throw new Error(
544
+ `${workload.id} requires Bonsai 27B at context 8,448 with explicit WebGPU benchmark Flash auto and applied Q4_0 K/V.`,
545
+ );
546
+ }
547
+ if (tokenIds.length !== 8) {
548
+ throw new Error(`${workload.id} requires exactly 8 sampled token ids.`);
549
+ }
550
+ if (generationResult.finishReason !== 'length') {
551
+ throw new Error(`${workload.id} requires finishReason=length after exhausting its 8-token budget.`);
552
+ }
553
+ if (promptTokens === null || promptTokens <= 8_192) {
554
+ throw new Error(`${workload.id} requires more than 8,192 engine-counted prompt tokens.`);
555
+ }
556
+ }
557
+ const teacherForcedReferenceScore = cloneTeacherForcedReferenceScore(
558
+ observation.teacherForcedReferenceScore,
559
+ workload.id,
560
+ );
561
  const engineTotalTokens = usage && Number.isFinite(usage.totalTokens) && usage.totalTokens >= 0
562
  ? Math.floor(usage.totalTokens)
563
  : null;
 
574
  const dflashPairing = observation.manifest.dflash.pairings[observation.model.id];
575
 
576
  return {
577
+ schemaVersion: 3,
578
  kind: 'bonsai-browser-benchmark',
579
  startedAt: observation.startedAt,
580
  completedAt: observation.completedAt,
 
582
  id: observation.model.id,
583
  displayName: observation.model.displayName,
584
  architecture: observation.model.architecture,
585
+ source: { ...observation.model.source },
586
+ shards: observation.model.files.map(({ path, bytes, sha256 }) => ({ path, bytes, sha256 })),
587
  downloadBytes: observation.model.downloadBytes,
588
  contextSize: observation.contextSize,
589
  },
 
606
  },
607
  },
608
  generation: {
609
+ workload: {
610
+ id: workload.id,
611
+ messages: cloneMessages(generationRequest.messages),
612
+ },
613
+ sampling: {
614
+ maxTokens,
615
+ temperature,
616
+ topP,
617
+ topK,
618
+ minP,
619
+ seed,
620
+ toolChoice,
621
+ cachePrompt,
622
+ },
623
+ tokenTrace: {
624
+ returnTokenIds: true,
625
+ logprobs: true,
626
+ topLogprobs: 5,
627
+ topAlternatives: 4,
628
+ encoding: 'uint32-le',
629
+ tokenIds,
630
+ entries: tokenTrace,
631
+ checkpointPrefixes,
632
+ },
633
+ teacherForcedReferenceScore,
634
+ rawText: generationResult.text,
635
+ rawReasoningText: generationResult.reasoningText,
636
  elapsedMs: roundedNonNegative(observation.generationElapsedMs, 1),
637
  timeToFirstTokenMs: roundedNonNegative(observation.timeToFirstTokenMs, 1),
638
  promptTokensPerSecond: roundedNonNegative(
 
646
  completionTokens,
647
  engineTotalTokens,
648
  totalTokens,
649
+ tokenCountSource: 'token-id-trace',
 
 
650
  finishReason: generationResult.finishReason,
651
  },
652
  execution: {
src/bench/state-drift-reference.test.ts ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, it } from 'vitest';
2
+ import fixtureJson from '../../public/fixtures/state-drift-27b-cpu-reference.json';
3
+ import type {
4
+ EngineCapabilities,
5
+ LoadModelResult,
6
+ ManifestModelV2,
7
+ } from '../engine';
8
+ import { validateStateDriftReferenceFixture } from './state-drift-reference';
9
+
10
+ const fixture: unknown = fixtureJson;
11
+
12
+ const model = {
13
+ id: '27b',
14
+ source: {
15
+ repo: 'WaveCut/Bonsai-web-GGUF',
16
+ revision: 'd85382fa09fe868c0242d81488dfc2edd8d3729b',
17
+ file: 'Bonsai-27B-Q1_0.gguf',
18
+ bytes: 3_803_452_480,
19
+ sha256: '17ef842e47450caeb8eaa3ebfbbab5d2f2278b62b79be107985fb69a2f819aa0',
20
+ },
21
+ } as ManifestModelV2;
22
+
23
+ const capabilities = {
24
+ runtime: {
25
+ llamaCppRevision: '00fa7cb284cbf133fc426733bd64238a3588a33e',
26
+ },
27
+ } as EngineCapabilities;
28
+
29
+ const loadResult = {
30
+ modelId: '27b',
31
+ backend: 'webgpu',
32
+ gate: { requestedBackend: 'webgpu' },
33
+ context: {
34
+ size: 2_048,
35
+ batchSize: 32,
36
+ microBatchSize: 16,
37
+ vocabularySize: 248_320,
38
+ },
39
+ tuning: {
40
+ scope: 'benchmark',
41
+ requested: { nBatch: 32, nUbatch: 16 },
42
+ observed: { nBatch: 32, nUbatch: 16 },
43
+ applied: true,
44
+ },
45
+ } as LoadModelResult;
46
+
47
+ describe('state-drift CPU reference fixture', () => {
48
+ it('validates the pinned fixture against the loaded model, runtime, context, and batching', () => {
49
+ const result = validateStateDriftReferenceFixture(fixture, {
50
+ model,
51
+ capabilities,
52
+ loadResult,
53
+ });
54
+
55
+ expect(result.promptTokenIds).toHaveLength(38);
56
+ expect(result.referenceTokenIds).toHaveLength(1_024);
57
+ expect(result.referenceTokenIdsSha256).toBe(
58
+ 'c503b2db0dcf10daecfda4f19a5f466d1699b31688076d6c32f7c29daefcef9b',
59
+ );
60
+ expect(result.checkpointPrefixes).toHaveLength(6);
61
+ });
62
+
63
+ it('rejects provenance or token-hash drift before scoring', () => {
64
+ const wrongModel = structuredClone(fixture) as Record<string, unknown>;
65
+ const provenance = wrongModel.provenance as { model: { sha256: string } };
66
+ provenance.model.sha256 = '0'.repeat(64);
67
+ expect(() => validateStateDriftReferenceFixture(wrongModel, {
68
+ model,
69
+ capabilities,
70
+ loadResult,
71
+ })).toThrow('model source sha256');
72
+
73
+ const wrongTokens = structuredClone(fixture) as { referenceTokenIds: number[] };
74
+ wrongTokens.referenceTokenIds[29] = wrongTokens.referenceTokenIds[29]! + 1;
75
+ expect(() => validateStateDriftReferenceFixture(wrongTokens, {
76
+ model,
77
+ capabilities,
78
+ loadResult,
79
+ })).toThrow('referenceTokenIdsSha256');
80
+ });
81
+ });
src/bench/state-drift-reference.ts ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { sha256 } from '@noble/hashes/sha2.js';
2
+ import { bytesToHex } from '@noble/hashes/utils.js';
3
+ import type {
4
+ EngineCapabilities,
5
+ LoadModelResult,
6
+ ManifestModelV2,
7
+ } from '../engine';
8
+ import { BENCHMARK_WORKLOADS, TOKEN_ID_CHECKPOINTS } from './report';
9
+
10
+ export const STATE_DRIFT_REFERENCE_PATH = 'fixtures/state-drift-27b-cpu-reference.json';
11
+ const STATE_DRIFT_SOURCE_EVIDENCE = 'results/space-model/27b-native-cpu-state-drift-1024-b32-u16.json';
12
+ const STATE_DRIFT_RENDERED_PROMPT_SHA256 = '7c46d0de443e6f1235ddaa7c0a55c9da710eb73958d5671247b6f0e3b7d189c7';
13
+ const STATE_DRIFT_PROMPT_TOKEN_IDS_SHA256 = 'e7af7d66c96f24cb148575bd45cef079920669eb0d193fdc830ce61dfec85451';
14
+ const STATE_DRIFT_REFERENCE_TOKEN_IDS_SHA256 = 'c503b2db0dcf10daecfda4f19a5f466d1699b31688076d6c32f7c29daefcef9b';
15
+
16
+ export interface StateDriftReferenceFixture {
17
+ schemaVersion: 1;
18
+ kind: 'bonsai-state-drift-reference';
19
+ workload: {
20
+ id: 'state-drift-1k-v1';
21
+ messages: Array<{ role: 'user'; content: string }>;
22
+ };
23
+ provenance: {
24
+ sourceEvidence: string;
25
+ engineRevision: string;
26
+ nativeBinary: { bytes: number; sha256: string };
27
+ model: { file: string; bytes: number; sha256: string };
28
+ renderedPromptSha256: string;
29
+ execution: {
30
+ backend: 'cpu';
31
+ contextSize: number;
32
+ batchSize: number;
33
+ microBatchSize: number;
34
+ };
35
+ };
36
+ tokenEncoding: 'uint32-le';
37
+ promptTokenIds: number[];
38
+ promptTokenIdsSha256: string;
39
+ referenceTokenIds: number[];
40
+ referenceTokenIdsSha256: string;
41
+ checkpointPrefixes: Array<{ tokens: number; sha256: string }>;
42
+ }
43
+
44
+ export interface StateDriftReferenceContext {
45
+ model: ManifestModelV2;
46
+ capabilities: EngineCapabilities;
47
+ loadResult: LoadModelResult;
48
+ }
49
+
50
+ function fail(message: string): never {
51
+ throw new Error(`Invalid state-drift CPU reference: ${message}`);
52
+ }
53
+
54
+ function record(value: unknown, label: string): Record<string, unknown> {
55
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
56
+ fail(`${label} must be an object.`);
57
+ }
58
+ return value as Record<string, unknown>;
59
+ }
60
+
61
+ function exact<T>(value: unknown, expected: T, label: string): T {
62
+ if (!Object.is(value, expected)) {
63
+ fail(`${label} must be ${JSON.stringify(expected)}; received ${JSON.stringify(value)}.`);
64
+ }
65
+ return expected;
66
+ }
67
+
68
+ function nonEmptyString(value: unknown, label: string): string {
69
+ if (typeof value !== 'string' || value.length === 0) fail(`${label} must be a non-empty string.`);
70
+ return value;
71
+ }
72
+
73
+ function positiveInteger(value: unknown, label: string): number {
74
+ if (!Number.isSafeInteger(value) || (value as number) <= 0) {
75
+ fail(`${label} must be a positive safe integer.`);
76
+ }
77
+ return value as number;
78
+ }
79
+
80
+ function sha256Digest(value: unknown, label: string): string {
81
+ const digest = nonEmptyString(value, label).toLowerCase();
82
+ if (!/^[0-9a-f]{64}$/.test(digest)) fail(`${label} must be a 64-character SHA256 digest.`);
83
+ return digest;
84
+ }
85
+
86
+ function gitRevision(value: unknown, label: string): string {
87
+ const revision = nonEmptyString(value, label).toLowerCase();
88
+ if (!/^[0-9a-f]{40}$/.test(revision)) fail(`${label} must be a pinned 40-character revision.`);
89
+ return revision;
90
+ }
91
+
92
+ function tokenIds(
93
+ value: unknown,
94
+ expectedLength: number,
95
+ vocabularySize: number,
96
+ label: string,
97
+ ): number[] {
98
+ if (!Array.isArray(value) || value.length !== expectedLength) {
99
+ fail(`${label} must contain exactly ${expectedLength} token ids.`);
100
+ }
101
+ return value.map((tokenId, index) => {
102
+ if (!Number.isSafeInteger(tokenId) || tokenId < 0 || tokenId >= vocabularySize) {
103
+ fail(`${label}[${index}] is outside the loaded vocabulary.`);
104
+ }
105
+ return tokenId;
106
+ });
107
+ }
108
+
109
+ function tokenIdsSha256(values: readonly number[], count = values.length): string {
110
+ const bytes = new Uint8Array(count * Uint32Array.BYTES_PER_ELEMENT);
111
+ const view = new DataView(bytes.buffer);
112
+ for (let index = 0; index < count; index += 1) {
113
+ view.setUint32(index * Uint32Array.BYTES_PER_ELEMENT, values[index]!, true);
114
+ }
115
+ return bytesToHex(sha256(bytes));
116
+ }
117
+
118
+ export function validateStateDriftReferenceFixture(
119
+ value: unknown,
120
+ context: StateDriftReferenceContext,
121
+ ): StateDriftReferenceFixture {
122
+ const root = record(value, 'root');
123
+ exact(root.schemaVersion, 1, 'schemaVersion');
124
+ exact(root.kind, 'bonsai-state-drift-reference', 'kind');
125
+
126
+ const workload = record(root.workload, 'workload');
127
+ exact(workload.id, 'state-drift-1k-v1', 'workload.id');
128
+ const expectedMessages = BENCHMARK_WORKLOADS['state-drift-1k-v1'].messages;
129
+ if (JSON.stringify(workload.messages) !== JSON.stringify(expectedMessages)) {
130
+ fail('workload.messages do not match the locked state-drift prompt.');
131
+ }
132
+
133
+ const { model, capabilities, loadResult } = context;
134
+ if (
135
+ model.id !== '27b'
136
+ || loadResult.modelId !== '27b'
137
+ || loadResult.backend !== 'webgpu'
138
+ || loadResult.gate.requestedBackend !== 'webgpu'
139
+ || loadResult.tuning.scope !== 'benchmark'
140
+ || !loadResult.tuning.applied
141
+ ) {
142
+ fail('the loaded runtime is not the explicit 27B WebGPU benchmark path.');
143
+ }
144
+
145
+ const provenance = record(root.provenance, 'provenance');
146
+ const sourceEvidence = nonEmptyString(provenance.sourceEvidence, 'provenance.sourceEvidence');
147
+ exact(sourceEvidence, STATE_DRIFT_SOURCE_EVIDENCE, 'provenance.sourceEvidence');
148
+ const engineRevision = gitRevision(provenance.engineRevision, 'provenance.engineRevision');
149
+ exact(engineRevision, capabilities.runtime.llamaCppRevision, 'runtime llama.cpp revision');
150
+
151
+ const nativeBinary = record(provenance.nativeBinary, 'provenance.nativeBinary');
152
+ const nativeBinaryBytes = positiveInteger(nativeBinary.bytes, 'provenance.nativeBinary.bytes');
153
+ const nativeBinarySha256 = sha256Digest(
154
+ nativeBinary.sha256,
155
+ 'provenance.nativeBinary.sha256',
156
+ );
157
+
158
+ const fixtureModel = record(provenance.model, 'provenance.model');
159
+ const modelFile = nonEmptyString(fixtureModel.file, 'provenance.model.file');
160
+ const modelBytes = positiveInteger(fixtureModel.bytes, 'provenance.model.bytes');
161
+ const modelSha256 = sha256Digest(fixtureModel.sha256, 'provenance.model.sha256');
162
+ exact(modelFile, model.source.file, 'model source file');
163
+ exact(modelBytes, model.source.bytes, 'model source bytes');
164
+ exact(modelSha256, model.source.sha256, 'model source sha256');
165
+
166
+ const renderedPromptSha256 = sha256Digest(
167
+ provenance.renderedPromptSha256,
168
+ 'provenance.renderedPromptSha256',
169
+ );
170
+ exact(
171
+ renderedPromptSha256,
172
+ STATE_DRIFT_RENDERED_PROMPT_SHA256,
173
+ 'provenance.renderedPromptSha256',
174
+ );
175
+ const execution = record(provenance.execution, 'provenance.execution');
176
+ exact(execution.backend, 'cpu', 'provenance.execution.backend');
177
+ const contextSize = positiveInteger(execution.contextSize, 'provenance.execution.contextSize');
178
+ const batchSize = positiveInteger(execution.batchSize, 'provenance.execution.batchSize');
179
+ const microBatchSize = positiveInteger(
180
+ execution.microBatchSize,
181
+ 'provenance.execution.microBatchSize',
182
+ );
183
+ exact(contextSize, loadResult.context.size, 'reference/loaded context size');
184
+ exact(batchSize, loadResult.context.batchSize, 'reference/loaded batch size');
185
+ exact(microBatchSize, loadResult.context.microBatchSize, 'reference/loaded micro-batch size');
186
+ exact(loadResult.tuning.requested.nBatch, batchSize, 'requested reference batch size');
187
+ exact(loadResult.tuning.requested.nUbatch, microBatchSize, 'requested reference micro-batch size');
188
+ exact(loadResult.tuning.observed.nBatch, batchSize, 'observed reference batch size');
189
+ exact(loadResult.tuning.observed.nUbatch, microBatchSize, 'observed reference micro-batch size');
190
+
191
+ exact(root.tokenEncoding, 'uint32-le', 'tokenEncoding');
192
+ const promptTokenIds = tokenIds(
193
+ root.promptTokenIds,
194
+ 38,
195
+ loadResult.context.vocabularySize,
196
+ 'promptTokenIds',
197
+ );
198
+ const promptTokenIdsSha256 = sha256Digest(root.promptTokenIdsSha256, 'promptTokenIdsSha256');
199
+ exact(promptTokenIdsSha256, tokenIdsSha256(promptTokenIds), 'promptTokenIdsSha256');
200
+ exact(
201
+ promptTokenIdsSha256,
202
+ STATE_DRIFT_PROMPT_TOKEN_IDS_SHA256,
203
+ 'pinned promptTokenIdsSha256',
204
+ );
205
+ const referenceTokenIds = tokenIds(
206
+ root.referenceTokenIds,
207
+ 1_024,
208
+ loadResult.context.vocabularySize,
209
+ 'referenceTokenIds',
210
+ );
211
+ const referenceTokenIdsSha256 = sha256Digest(
212
+ root.referenceTokenIdsSha256,
213
+ 'referenceTokenIdsSha256',
214
+ );
215
+ exact(referenceTokenIdsSha256, tokenIdsSha256(referenceTokenIds), 'referenceTokenIdsSha256');
216
+ exact(
217
+ referenceTokenIdsSha256,
218
+ STATE_DRIFT_REFERENCE_TOKEN_IDS_SHA256,
219
+ 'pinned referenceTokenIdsSha256',
220
+ );
221
+
222
+ if (!Array.isArray(root.checkpointPrefixes)
223
+ || root.checkpointPrefixes.length !== TOKEN_ID_CHECKPOINTS.length) {
224
+ fail(`checkpointPrefixes must contain exactly ${TOKEN_ID_CHECKPOINTS.length} entries.`);
225
+ }
226
+ const checkpointPrefixes = root.checkpointPrefixes.map((rawCheckpoint, index) => {
227
+ const checkpoint = record(rawCheckpoint, `checkpointPrefixes[${index}]`);
228
+ const tokens = TOKEN_ID_CHECKPOINTS[index]!;
229
+ exact(checkpoint.tokens, tokens, `checkpointPrefixes[${index}].tokens`);
230
+ const embeddedSha256 = sha256Digest(
231
+ checkpoint.sha256,
232
+ `checkpointPrefixes[${index}].sha256`,
233
+ );
234
+ exact(
235
+ embeddedSha256,
236
+ tokenIdsSha256(referenceTokenIds, tokens),
237
+ `checkpointPrefixes[${index}].sha256`,
238
+ );
239
+ return { tokens, sha256: embeddedSha256 };
240
+ });
241
+
242
+ return {
243
+ schemaVersion: 1,
244
+ kind: 'bonsai-state-drift-reference',
245
+ workload: {
246
+ id: 'state-drift-1k-v1',
247
+ messages: expectedMessages.map((message) => ({ ...message })) as Array<{
248
+ role: 'user';
249
+ content: string;
250
+ }>,
251
+ },
252
+ provenance: {
253
+ sourceEvidence,
254
+ engineRevision,
255
+ nativeBinary: { bytes: nativeBinaryBytes, sha256: nativeBinarySha256 },
256
+ model: { file: modelFile, bytes: modelBytes, sha256: modelSha256 },
257
+ renderedPromptSha256,
258
+ execution: {
259
+ backend: 'cpu',
260
+ contextSize,
261
+ batchSize,
262
+ microBatchSize,
263
+ },
264
+ },
265
+ tokenEncoding: 'uint32-le',
266
+ promptTokenIds,
267
+ promptTokenIdsSha256,
268
+ referenceTokenIds,
269
+ referenceTokenIdsSha256,
270
+ checkpointPrefixes,
271
+ };
272
+ }
src/engine/client.test.ts CHANGED
@@ -231,6 +231,50 @@ describe('BrowserEngineClient worker lifecycle', () => {
231
  client.close();
232
  });
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  it('invalidates the model when generation ignores abort beyond the grace period', async () => {
235
  vi.useFakeTimers();
236
  try {
 
231
  client.close();
232
  });
233
 
234
+ it('routes and softly aborts diagnostic sequence scoring', async () => {
235
+ const client = new BrowserEngineClient();
236
+ const worker = latestWorker();
237
+ const controller = new AbortController();
238
+ const scoring = client.scoreSequence({
239
+ promptTokenIds: Array.from({ length: 38 }, (_, index) => index + 1),
240
+ referenceTokenIds: Array.from({ length: 1_024 }, (_, index) => index + 1_000),
241
+ topK: 5,
242
+ }, {
243
+ requestId: 'soft-score-sequence',
244
+ signal: controller.signal,
245
+ });
246
+ const scoringFailure = expect(scoring).rejects.toMatchObject({ code: 'ABORTED' });
247
+
248
+ expect(worker.postMessage).toHaveBeenCalledWith(expect.objectContaining({
249
+ requestId: 'soft-score-sequence',
250
+ method: 'scoreSequence',
251
+ }));
252
+ controller.abort();
253
+ const abortRequest = worker.postMessage.mock.calls
254
+ .map((call) => call[0] as { requestId: string; method: string })
255
+ .find((request) => request.method === 'abort');
256
+ expect(abortRequest).toBeTruthy();
257
+ worker.send({
258
+ type: 'response',
259
+ requestId: 'soft-score-sequence',
260
+ method: 'scoreSequence',
261
+ ok: false,
262
+ error: { code: 'ABORTED', message: 'Sequence scoring stopped.' },
263
+ });
264
+ if (!abortRequest) throw new Error('Expected abort request.');
265
+ worker.send({
266
+ type: 'response',
267
+ requestId: abortRequest.requestId,
268
+ method: 'abort',
269
+ ok: true,
270
+ result: { targetRequestId: 'soft-score-sequence', aborted: true },
271
+ });
272
+
273
+ await scoringFailure;
274
+ expect(worker.terminate).not.toHaveBeenCalled();
275
+ client.close();
276
+ });
277
+
278
  it('invalidates the model when generation ignores abort beyond the grace period', async () => {
279
  vi.useFakeTimers();
280
  try {
src/engine/client.ts CHANGED
@@ -9,6 +9,7 @@ import type {
9
  EngineWorkerMessage,
10
  GenerateParams,
11
  LoadModelParams,
 
12
  } from './protocol';
13
 
14
  type ProgressEvent = Extract<EngineEvent, { event: 'progress' }>;
@@ -134,16 +135,20 @@ export class BrowserEngineClient {
134
  ));
135
  }
136
  }, LOAD_ABORT_CLEANUP_TIMEOUT_MS);
137
- } else if (method === 'generate') {
 
 
 
 
138
  pending.abortFallbackTimer = setTimeout(() => {
139
  if (this.pending.has(requestId)) {
140
  this.restart(new EngineClientError({
141
  code: 'ENGINE_WORKER_FAILED',
142
- message: 'Generation did not stop within 5 seconds. The browser engine worker was restarted and the loaded model was invalidated.',
143
  details: {
144
  recoverable: true,
145
  nextAction: 'reload-model',
146
- cause: 'generation-abort-timeout',
147
  graceMs: GENERATION_ABORT_GRACE_MS,
148
  },
149
  }));
@@ -176,6 +181,10 @@ export class BrowserEngineClient {
176
  return this.request('generate', params, options);
177
  }
178
 
 
 
 
 
179
  abort(targetRequestId: string) {
180
  return this.request('abort', { targetRequestId });
181
  }
 
9
  EngineWorkerMessage,
10
  GenerateParams,
11
  LoadModelParams,
12
+ ScoreSequenceParams,
13
  } from './protocol';
14
 
15
  type ProgressEvent = Extract<EngineEvent, { event: 'progress' }>;
 
135
  ));
136
  }
137
  }, LOAD_ABORT_CLEANUP_TIMEOUT_MS);
138
+ } else if (method === 'generate' || method === 'scoreSequence') {
139
+ const operationLabel = method === 'generate' ? 'Generation' : 'Sequence scoring';
140
+ const cause = method === 'generate'
141
+ ? 'generation-abort-timeout'
142
+ : 'score-sequence-abort-timeout';
143
  pending.abortFallbackTimer = setTimeout(() => {
144
  if (this.pending.has(requestId)) {
145
  this.restart(new EngineClientError({
146
  code: 'ENGINE_WORKER_FAILED',
147
+ message: `${operationLabel} did not stop within 5 seconds. The browser engine worker was restarted and the loaded model was invalidated.`,
148
  details: {
149
  recoverable: true,
150
  nextAction: 'reload-model',
151
+ cause,
152
  graceMs: GENERATION_ABORT_GRACE_MS,
153
  },
154
  }));
 
181
  return this.request('generate', params, options);
182
  }
183
 
184
+ scoreSequence(params: ScoreSequenceParams, options?: EngineRequestOptions) {
185
+ return this.request('scoreSequence', params, options);
186
+ }
187
+
188
  abort(targetRequestId: string) {
189
  return this.request('abort', { targetRequestId });
190
  }
src/engine/device-gate.test.ts CHANGED
@@ -1,6 +1,7 @@
1
  import { afterEach, describe, expect, it, vi } from 'vitest';
2
  import {
3
  assertBackendPolicy,
 
4
  evaluateModelGate,
5
  inspectWebGpuAdapter,
6
  supportsWllamaWebGpuRuntime,
@@ -164,6 +165,52 @@ describe('evaluateModelGate', () => {
164
  });
165
  });
166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  describe('assertBackendPolicy', () => {
168
  const strictModel = model('27b', false);
169
  const baseReport = {
 
1
  import { afterEach, describe, expect, it, vi } from 'vitest';
2
  import {
3
  assertBackendPolicy,
4
+ evaluateModelContextPolicy,
5
  evaluateModelGate,
6
  inspectWebGpuAdapter,
7
  supportsWllamaWebGpuRuntime,
 
165
  });
166
  });
167
 
168
+ describe('evaluateModelContextPolicy', () => {
169
+ const model27b = model('27b', false);
170
+ const longContextExperiment = {
171
+ tuningScope: 'benchmark',
172
+ requestedBackend: 'webgpu',
173
+ flashMode: 'auto',
174
+ kvCacheType: 'q4_0',
175
+ } as const;
176
+
177
+ it('allows the bounded 8,448-token 27B benchmark experiment', () => {
178
+ expect(evaluateModelContextPolicy(model27b, 8_448, longContextExperiment)).toEqual({
179
+ allowed: true,
180
+ limit: 8_448,
181
+ });
182
+ });
183
+
184
+ it('rejects 8,449 tokens even for the exact long-context experiment', () => {
185
+ expect(evaluateModelContextPolicy(model27b, 8_449, longContextExperiment)).toEqual({
186
+ allowed: false,
187
+ limit: 8_448,
188
+ });
189
+ });
190
+
191
+ it.each([
192
+ ['q8 KV', { ...longContextExperiment, kvCacheType: 'q8_0' as const }],
193
+ ['f16 KV', { ...longContextExperiment, kvCacheType: 'f16' as const }],
194
+ ['auto backend', { ...longContextExperiment, requestedBackend: 'auto' as const }],
195
+ ['release defaults', { ...longContextExperiment, tuningScope: 'release-defaults' as const }],
196
+ ])('keeps 27B at 2,048 for %s', (_label, input) => {
197
+ expect(evaluateModelContextPolicy(model27b, 8_448, input)).toEqual({
198
+ allowed: false,
199
+ limit: 2_048,
200
+ });
201
+ expect(evaluateModelContextPolicy(model27b, 2_048, input).allowed).toBe(true);
202
+ });
203
+
204
+ it('leaves dense-tier manifest limits unchanged', () => {
205
+ expect(evaluateModelContextPolicy(model('8b', true), 32_768, {
206
+ tuningScope: 'release-defaults',
207
+ requestedBackend: 'auto',
208
+ flashMode: 'off',
209
+ kvCacheType: 'f16',
210
+ })).toEqual({ allowed: true, limit: 32_768 });
211
+ });
212
+ });
213
+
214
  describe('assertBackendPolicy', () => {
215
  const strictModel = model('27b', false);
216
  const baseReport = {
src/engine/device-gate.ts CHANGED
@@ -1,5 +1,7 @@
1
  import type { ManifestModelV2 } from './manifest';
2
  import type {
 
 
3
  GateDecision,
4
  RequestedBackend,
5
  RuntimeBackend,
@@ -8,6 +10,21 @@ import type {
8
  } from './protocol';
9
  import type { BackendReport } from './native-log';
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  const KNOWN_LIMITS = [
12
  'maxBindGroups',
13
  'maxBindingsPerBindGroup',
@@ -161,6 +178,30 @@ function availableStorageBytes(storage: StorageEstimate, cachedModelBytes: numbe
161
  return Math.max(0, storage.quotaBytes - storage.usageBytes + cachedModelBytes);
162
  }
163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  function webGpuFailures(model: ManifestModelV2, adapter: WebGpuAdapterSnapshot): string[] {
165
  if (!adapter.available) {
166
  return ['WebGPU adapter is unavailable.'];
 
1
  import type { ManifestModelV2 } from './manifest';
2
  import type {
3
+ BenchmarkFlashMode,
4
+ BenchmarkKvCacheType,
5
  GateDecision,
6
  RequestedBackend,
7
  RuntimeBackend,
 
10
  } from './protocol';
11
  import type { BackendReport } from './native-log';
12
 
13
+ export const MODEL_27B_RELEASE_CONTEXT_LIMIT = 2_048;
14
+ export const MODEL_27B_BENCHMARK_CONTEXT_LIMIT = 8_448;
15
+
16
+ export interface ModelContextPolicyInput {
17
+ tuningScope: 'release-defaults' | 'benchmark';
18
+ requestedBackend: RequestedBackend;
19
+ flashMode: BenchmarkFlashMode;
20
+ kvCacheType: BenchmarkKvCacheType;
21
+ }
22
+
23
+ export interface ModelContextPolicyDecision {
24
+ allowed: boolean;
25
+ limit: number;
26
+ }
27
+
28
  const KNOWN_LIMITS = [
29
  'maxBindGroups',
30
  'maxBindingsPerBindGroup',
 
178
  return Math.max(0, storage.quotaBytes - storage.usageBytes + cachedModelBytes);
179
  }
180
 
181
+ export function evaluateModelContextPolicy(
182
+ model: ManifestModelV2,
183
+ contextSize: number,
184
+ input: ModelContextPolicyInput,
185
+ ): ModelContextPolicyDecision {
186
+ const longContextExperiment = model.id === '27b'
187
+ && input.tuningScope === 'benchmark'
188
+ && input.requestedBackend === 'webgpu'
189
+ && input.flashMode === 'auto'
190
+ && input.kvCacheType === 'q4_0';
191
+ const limit = model.id === '27b'
192
+ ? Math.min(
193
+ model.contextLength,
194
+ longContextExperiment
195
+ ? MODEL_27B_BENCHMARK_CONTEXT_LIMIT
196
+ : MODEL_27B_RELEASE_CONTEXT_LIMIT,
197
+ )
198
+ : model.contextLength;
199
+ return {
200
+ allowed: Number.isSafeInteger(contextSize) && contextSize > 0 && contextSize <= limit,
201
+ limit,
202
+ };
203
+ }
204
+
205
  function webGpuFailures(model: ManifestModelV2, adapter: WebGpuAdapterSnapshot): string[] {
206
  if (!adapter.available) {
207
  return ['WebGPU adapter is unavailable.'];
src/engine/errors.ts CHANGED
@@ -1,7 +1,7 @@
1
  export interface WebGpuDeviceLostDetails {
2
  recoverable: true;
3
  nextAction: 'reload-model';
4
- stage: 'load' | 'generate' | 'backend-report';
5
  modelId: string;
6
  cpuFallbackAvailable: boolean;
7
  signal: {
 
1
  export interface WebGpuDeviceLostDetails {
2
  recoverable: true;
3
  nextAction: 'reload-model';
4
+ stage: 'load' | 'generate' | 'score-sequence' | 'backend-report';
5
  modelId: string;
6
  cpuFallbackAvailable: boolean;
7
  signal: {
src/engine/protocol.ts CHANGED
@@ -133,6 +133,7 @@ export interface GenerateParams {
133
  tools?: EngineChatTool[];
134
  toolChoice?: 'none' | 'auto' | 'required';
135
  cachePrompt?: boolean;
 
136
  }
137
 
138
  export interface LoadModelResult {
@@ -180,8 +181,21 @@ export interface LoadModelResult {
180
  backendReport: BackendReport;
181
  }
182
 
 
 
 
 
 
 
 
 
 
 
183
  export interface GenerateResult {
184
  text: string;
 
 
 
185
  finishReason: 'stop' | 'length' | 'tool_calls' | 'content_filter' | null;
186
  toolCalls: EngineChatToolCall[];
187
  usage: {
@@ -195,6 +209,40 @@ export interface GenerateResult {
195
  } | null;
196
  }
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  export interface EmptyParams {
199
  readonly _empty?: never;
200
  }
@@ -203,6 +251,7 @@ export interface EngineRequestMap {
203
  capabilities: EmptyParams;
204
  loadModel: LoadModelParams;
205
  generate: GenerateParams;
 
206
  abort: { targetRequestId: string };
207
  unload: EmptyParams;
208
  backendReport: EmptyParams;
@@ -215,6 +264,7 @@ export interface EngineResponseMap {
215
  capabilities: EngineCapabilities;
216
  loadModel: LoadModelResult;
217
  generate: GenerateResult;
 
218
  abort: { targetRequestId: string; aborted: boolean };
219
  unload: { unloaded: true };
220
  backendReport: BackendReport;
 
133
  tools?: EngineChatTool[];
134
  toolChoice?: 'none' | 'auto' | 'required';
135
  cachePrompt?: boolean;
136
+ returnTokenIds?: boolean;
137
  }
138
 
139
  export interface LoadModelResult {
 
181
  backendReport: BackendReport;
182
  }
183
 
184
+ export interface EngineTokenLogprob {
185
+ id: number;
186
+ logprob: number;
187
+ }
188
+
189
+ export interface EngineSampledTokenTraceEntry {
190
+ selected: EngineTokenLogprob;
191
+ topCandidates: EngineTokenLogprob[];
192
+ }
193
+
194
  export interface GenerateResult {
195
  text: string;
196
+ reasoningText: string;
197
+ tokenIds: number[] | null;
198
+ tokenTrace: EngineSampledTokenTraceEntry[] | null;
199
  finishReason: 'stop' | 'length' | 'tool_calls' | 'content_filter' | null;
200
  toolCalls: EngineChatToolCall[];
201
  usage: {
 
209
  } | null;
210
  }
211
 
212
+ export interface ScoreSequenceParams {
213
+ promptTokenIds: number[];
214
+ referenceTokenIds: number[];
215
+ topK: 5;
216
+ }
217
+
218
+ export interface EngineTeacherForcedScoreEntry {
219
+ index: number;
220
+ selectedReference: EngineTokenLogprob;
221
+ naturalTop1: EngineTokenLogprob;
222
+ topCandidates: EngineTokenLogprob[];
223
+ referenceRankInTopCandidatesZeroBased: number | null;
224
+ top1Top2Margin: number;
225
+ }
226
+
227
+ export interface ScoreSequenceResult {
228
+ method: {
229
+ promptMode: 'raw-token-id-prefix';
230
+ maxTokensPerStep: 1;
231
+ temperature: 0;
232
+ topK: 1;
233
+ reportedTopLogprobs: 5;
234
+ logitBias: 1_000;
235
+ cachePromptFirst: false;
236
+ cachePromptSubsequent: true;
237
+ };
238
+ entries: EngineTeacherForcedScoreEntry[];
239
+ summary: {
240
+ tokenCount: 1_024;
241
+ meanNll: number;
242
+ perplexity: number;
243
+ };
244
+ }
245
+
246
  export interface EmptyParams {
247
  readonly _empty?: never;
248
  }
 
251
  capabilities: EmptyParams;
252
  loadModel: LoadModelParams;
253
  generate: GenerateParams;
254
+ scoreSequence: ScoreSequenceParams;
255
  abort: { targetRequestId: string };
256
  unload: EmptyParams;
257
  backendReport: EmptyParams;
 
264
  capabilities: EngineCapabilities;
265
  loadModel: LoadModelResult;
266
  generate: GenerateResult;
267
+ scoreSequence: ScoreSequenceResult;
268
  abort: { targetRequestId: string; aborted: boolean };
269
  unload: { unloaded: true };
270
  backendReport: BackendReport;
src/engine/runtime-generate.test.ts CHANGED
@@ -2,12 +2,16 @@ import { describe, expect, it } from 'vitest';
2
  import type { ChatCompletionChunk } from '../../vendor/wllama-bonsai/esm/index.js';
3
  import { BrowserEngineRuntime } from './runtime';
4
 
 
 
 
 
 
 
5
  interface RuntimeInternals {
6
  wllama: {
7
  isModelLoaded(): boolean;
8
- createChatCompletion(options: {
9
- onData(chunk: ChatCompletionChunk): void;
10
- }): Promise<void>;
11
  } | null;
12
  loaded: {
13
  manifest: unknown;
@@ -44,6 +48,46 @@ function chunk(
44
  };
45
  }
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  describe('BrowserEngineRuntime generation telemetry', () => {
48
  it('retains usage and timings when a trailing stream chunk omits metadata', async () => {
49
  const runtime = new BrowserEngineRuntime();
@@ -62,10 +106,12 @@ describe('BrowserEngineRuntime generation telemetry', () => {
62
  },
63
  },
64
  };
 
65
  internals.wllama = {
66
  isModelLoaded: () => true,
67
- createChatCompletion: async ({ onData }) => {
68
- onData(chunk('Ready', {
 
69
  usage: { prompt_tokens: 9, completion_tokens: 1, total_tokens: 10 },
70
  timings: {
71
  cache_n: 0,
@@ -79,7 +125,7 @@ describe('BrowserEngineRuntime generation telemetry', () => {
79
  predicted_per_second: 50,
80
  },
81
  }));
82
- onData(chunk(''));
83
  },
84
  };
85
 
@@ -92,6 +138,11 @@ describe('BrowserEngineRuntime generation telemetry', () => {
92
 
93
  expect(result.usage).toEqual({ promptTokens: 9, completionTokens: 1, totalTokens: 10 });
94
  expect(result.timings).toEqual({ promptTokensPerSecond: 100, predictedTokensPerSecond: 50 });
 
 
 
 
 
95
  });
96
 
97
  it('reconciles incomplete streamed usage with llama.cpp timing counts', async () => {
@@ -140,5 +191,310 @@ describe('BrowserEngineRuntime generation telemetry', () => {
140
  );
141
 
142
  expect(result.usage).toEqual({ promptTokens: 9, completionTokens: 64, totalTokens: 73 });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  });
144
  });
 
2
  import type { ChatCompletionChunk } from '../../vendor/wllama-bonsai/esm/index.js';
3
  import { BrowserEngineRuntime } from './runtime';
4
 
5
+ interface CompletionOptions {
6
+ onData(chunk: ChatCompletionChunk): void;
7
+ logprobs?: boolean;
8
+ top_logprobs?: number;
9
+ }
10
+
11
  interface RuntimeInternals {
12
  wllama: {
13
  isModelLoaded(): boolean;
14
+ createChatCompletion(options: CompletionOptions): Promise<void>;
 
 
15
  } | null;
16
  loaded: {
17
  manifest: unknown;
 
48
  };
49
  }
50
 
51
+ function tracedChunk(
52
+ content: string,
53
+ id: number,
54
+ metadata: Partial<Pick<ChatCompletionChunk, 'usage' | 'timings'>> = {},
55
+ ): ChatCompletionChunk {
56
+ const value = chunk(content, metadata);
57
+ const choice = value.choices[0];
58
+ if (!choice) throw new Error('Chunk fixture requires one choice.');
59
+ choice.logprobs = {
60
+ content: [{
61
+ id,
62
+ token: content,
63
+ logprob: -0.1,
64
+ bytes: null,
65
+ top_logprobs: [
66
+ { id: id + 1_004, token: 'fifth', logprob: -4, bytes: null },
67
+ { id: id + 1_002, token: 'third', logprob: -2, bytes: null },
68
+ { id, token: content, logprob: -0.1, bytes: null },
69
+ { id: id + 1_003, token: 'fourth', logprob: -3, bytes: null },
70
+ { id: id + 1_001, token: 'second', logprob: -1, bytes: null },
71
+ ],
72
+ }],
73
+ refusal: null,
74
+ };
75
+ return value;
76
+ }
77
+
78
+ function reasoningTracedChunk(
79
+ reasoningContent: string,
80
+ id: number,
81
+ metadata: Partial<Pick<ChatCompletionChunk, 'usage' | 'timings'>> = {},
82
+ ): ChatCompletionChunk {
83
+ const value = tracedChunk('', id, metadata);
84
+ const choice = value.choices[0];
85
+ if (!choice) throw new Error('Chunk fixture requires one choice.');
86
+ choice.finish_reason = null;
87
+ (choice.delta as unknown as Record<string, unknown>).reasoning_content = reasoningContent;
88
+ return value;
89
+ }
90
+
91
  describe('BrowserEngineRuntime generation telemetry', () => {
92
  it('retains usage and timings when a trailing stream chunk omits metadata', async () => {
93
  const runtime = new BrowserEngineRuntime();
 
106
  },
107
  },
108
  };
109
+ let completionOptions: CompletionOptions | null = null;
110
  internals.wllama = {
111
  isModelLoaded: () => true,
112
+ createChatCompletion: async (options) => {
113
+ completionOptions = options;
114
+ options.onData(chunk('Ready', {
115
  usage: { prompt_tokens: 9, completion_tokens: 1, total_tokens: 10 },
116
  timings: {
117
  cache_n: 0,
 
125
  predicted_per_second: 50,
126
  },
127
  }));
128
+ options.onData(chunk(''));
129
  },
130
  };
131
 
 
138
 
139
  expect(result.usage).toEqual({ promptTokens: 9, completionTokens: 1, totalTokens: 10 });
140
  expect(result.timings).toEqual({ promptTokensPerSecond: 100, predictedTokensPerSecond: 50 });
141
+ expect(result.reasoningText).toBe('');
142
+ expect(result.tokenIds).toBeNull();
143
+ expect(result.tokenTrace).toBeNull();
144
+ expect(completionOptions).not.toHaveProperty('logprobs');
145
+ expect(completionOptions).not.toHaveProperty('top_logprobs');
146
  });
147
 
148
  it('reconciles incomplete streamed usage with llama.cpp timing counts', async () => {
 
191
  );
192
 
193
  expect(result.usage).toEqual({ promptTokens: 9, completionTokens: 64, totalTokens: 73 });
194
+ expect(result.tokenIds).toBeNull();
195
+ expect(result.tokenTrace).toBeNull();
196
+ });
197
+
198
+ it('returns sampled token ids with selected logprobs and sorted top-five candidates', async () => {
199
+ const runtime = new BrowserEngineRuntime();
200
+ const internals = runtime as unknown as RuntimeInternals;
201
+ let completionOptions: CompletionOptions | null = null;
202
+ internals.loaded = {
203
+ manifest: {},
204
+ backend: 'wasm',
205
+ model: {
206
+ id: '1_7b',
207
+ displayName: 'Fixture Bonsai',
208
+ cpuFallback: true,
209
+ runtimePolicy: {
210
+ flashAttention: false,
211
+ tokenEmbeddingOnWebGPU: true,
212
+ requireSingleWebGPUGraph: false,
213
+ },
214
+ },
215
+ };
216
+ internals.wllama = {
217
+ isModelLoaded: () => true,
218
+ createChatCompletion: async (options) => {
219
+ completionOptions = options;
220
+ options.onData(tracedChunk('one', 101));
221
+ options.onData(tracedChunk(' two', 202, {
222
+ usage: { prompt_tokens: 4, completion_tokens: 2, total_tokens: 6 },
223
+ timings: {
224
+ cache_n: 0,
225
+ prompt_n: 4,
226
+ prompt_ms: 40,
227
+ prompt_per_token_ms: 10,
228
+ prompt_per_second: 100,
229
+ predicted_n: 2,
230
+ predicted_ms: 40,
231
+ predicted_per_token_ms: 20,
232
+ predicted_per_second: 50,
233
+ },
234
+ }));
235
+ options.onData(chunk(''));
236
+ },
237
+ };
238
+
239
+ const result = await runtime.generate(
240
+ 'request-token-ids',
241
+ { messages: [{ role: 'user', content: 'Count.' }], returnTokenIds: true },
242
+ new AbortController().signal,
243
+ () => undefined,
244
+ );
245
+
246
+ expect(result.tokenIds).toEqual([101, 202]);
247
+ expect(result.tokenTrace).toEqual([
248
+ {
249
+ selected: { id: 101, logprob: -0.1 },
250
+ topCandidates: [
251
+ { id: 101, logprob: -0.1 },
252
+ { id: 1_102, logprob: -1 },
253
+ { id: 1_103, logprob: -2 },
254
+ { id: 1_104, logprob: -3 },
255
+ { id: 1_105, logprob: -4 },
256
+ ],
257
+ },
258
+ {
259
+ selected: { id: 202, logprob: -0.1 },
260
+ topCandidates: [
261
+ { id: 202, logprob: -0.1 },
262
+ { id: 1_203, logprob: -1 },
263
+ { id: 1_204, logprob: -2 },
264
+ { id: 1_205, logprob: -3 },
265
+ { id: 1_206, logprob: -4 },
266
+ ],
267
+ },
268
+ ]);
269
+ expect(completionOptions).toHaveProperty('logprobs', true);
270
+ expect(completionOptions).toHaveProperty('top_logprobs', 5);
271
+ });
272
+
273
+ it('keeps reasoning-only output separate from visible text while preserving its token trace', async () => {
274
+ const runtime = new BrowserEngineRuntime();
275
+ const internals = runtime as unknown as RuntimeInternals;
276
+ internals.loaded = {
277
+ manifest: {},
278
+ backend: 'wasm',
279
+ model: {
280
+ id: '1_7b',
281
+ displayName: 'Fixture Bonsai',
282
+ cpuFallback: true,
283
+ runtimePolicy: {
284
+ flashAttention: false,
285
+ tokenEmbeddingOnWebGPU: true,
286
+ requireSingleWebGPUGraph: false,
287
+ },
288
+ },
289
+ };
290
+ internals.wllama = {
291
+ isModelLoaded: () => true,
292
+ createChatCompletion: async ({ onData }) => {
293
+ onData(reasoningTracedChunk('private reasoning', 303, {
294
+ usage: { prompt_tokens: 4, completion_tokens: 1, total_tokens: 5 },
295
+ }));
296
+ onData(chunk(''));
297
+ },
298
+ };
299
+ const streamed: Array<{ text: string; reasoningDelta?: string }> = [];
300
+
301
+ const result = await runtime.generate(
302
+ 'request-reasoning-trace',
303
+ { messages: [{ role: 'user', content: 'Think.' }], returnTokenIds: true },
304
+ new AbortController().signal,
305
+ (event) => {
306
+ if (event.event === 'token') streamed.push(event);
307
+ },
308
+ );
309
+
310
+ expect(result.text).toBe('');
311
+ expect(result.reasoningText).toBe('private reasoning');
312
+ expect(result.tokenIds).toEqual([303]);
313
+ expect(result.tokenTrace).toHaveLength(1);
314
+ expect(streamed).toEqual([{ type: 'event', requestId: 'request-reasoning-trace', event: 'token', text: '', reasoningDelta: 'private reasoning' }]);
315
+ });
316
+
317
+ it('fails loudly when a sampled token id is non-integer', async () => {
318
+ const runtime = new BrowserEngineRuntime();
319
+ const internals = runtime as unknown as RuntimeInternals;
320
+ internals.loaded = {
321
+ manifest: {},
322
+ backend: 'wasm',
323
+ model: {
324
+ id: '1_7b',
325
+ displayName: 'Fixture Bonsai',
326
+ cpuFallback: true,
327
+ runtimePolicy: {
328
+ flashAttention: false,
329
+ tokenEmbeddingOnWebGPU: true,
330
+ requireSingleWebGPUGraph: false,
331
+ },
332
+ },
333
+ };
334
+ internals.wllama = {
335
+ isModelLoaded: () => true,
336
+ createChatCompletion: async ({ onData }) => {
337
+ onData(tracedChunk('bad', 1.5));
338
+ },
339
+ };
340
+
341
+ await expect(runtime.generate(
342
+ 'request-invalid-token-id',
343
+ { messages: [{ role: 'user', content: 'Count.' }], returnTokenIds: true },
344
+ new AbortController().signal,
345
+ () => undefined,
346
+ )).rejects.toMatchObject({ code: 'INVALID_TOKEN_ID_TRACE' });
347
+ });
348
+
349
+ it('fails loudly when a sampled logprob entry omits its token id', async () => {
350
+ const runtime = new BrowserEngineRuntime();
351
+ const internals = runtime as unknown as RuntimeInternals;
352
+ internals.loaded = {
353
+ manifest: {},
354
+ backend: 'wasm',
355
+ model: {
356
+ id: '1_7b',
357
+ displayName: 'Fixture Bonsai',
358
+ cpuFallback: true,
359
+ runtimePolicy: {
360
+ flashAttention: false,
361
+ tokenEmbeddingOnWebGPU: true,
362
+ requireSingleWebGPUGraph: false,
363
+ },
364
+ },
365
+ };
366
+ internals.wllama = {
367
+ isModelLoaded: () => true,
368
+ createChatCompletion: async ({ onData }) => {
369
+ const malformed = tracedChunk('missing', 123);
370
+ const entry = malformed.choices[0]?.logprobs?.content?.[0];
371
+ if (!entry) throw new Error('Malformed trace fixture requires one logprob entry.');
372
+ Reflect.deleteProperty(entry, 'id');
373
+ onData(malformed);
374
+ },
375
+ };
376
+
377
+ await expect(runtime.generate(
378
+ 'request-missing-token-id-field',
379
+ { messages: [{ role: 'user', content: 'Count.' }], returnTokenIds: true },
380
+ new AbortController().signal,
381
+ () => undefined,
382
+ )).rejects.toMatchObject({
383
+ code: 'INVALID_TOKEN_ID_TRACE',
384
+ details: { index: 0, id: undefined },
385
+ });
386
+ });
387
+
388
+ it('fails loudly when completion usage outnumbers sampled token ids', async () => {
389
+ const runtime = new BrowserEngineRuntime();
390
+ const internals = runtime as unknown as RuntimeInternals;
391
+ internals.loaded = {
392
+ manifest: {},
393
+ backend: 'wasm',
394
+ model: {
395
+ id: '1_7b',
396
+ displayName: 'Fixture Bonsai',
397
+ cpuFallback: true,
398
+ runtimePolicy: {
399
+ flashAttention: false,
400
+ tokenEmbeddingOnWebGPU: true,
401
+ requireSingleWebGPUGraph: false,
402
+ },
403
+ },
404
+ };
405
+ internals.wllama = {
406
+ isModelLoaded: () => true,
407
+ createChatCompletion: async ({ onData }) => {
408
+ onData(chunk('missing', {
409
+ usage: { prompt_tokens: 4, completion_tokens: 1, total_tokens: 5 },
410
+ }));
411
+ },
412
+ };
413
+
414
+ await expect(runtime.generate(
415
+ 'request-missing-token-id',
416
+ { messages: [{ role: 'user', content: 'Count.' }], returnTokenIds: true },
417
+ new AbortController().signal,
418
+ () => undefined,
419
+ )).rejects.toMatchObject({
420
+ code: 'INCOMPLETE_TOKEN_ID_TRACE',
421
+ details: { expected: 1, observed: 0 },
422
+ });
423
+ });
424
+
425
+ it('fails loudly when a sampled token does not have exactly five top candidates', async () => {
426
+ const runtime = new BrowserEngineRuntime();
427
+ const internals = runtime as unknown as RuntimeInternals;
428
+ internals.loaded = {
429
+ manifest: {},
430
+ backend: 'wasm',
431
+ model: {
432
+ id: '1_7b',
433
+ displayName: 'Fixture Bonsai',
434
+ cpuFallback: true,
435
+ runtimePolicy: {
436
+ flashAttention: false,
437
+ tokenEmbeddingOnWebGPU: true,
438
+ requireSingleWebGPUGraph: false,
439
+ },
440
+ },
441
+ };
442
+ internals.wllama = {
443
+ isModelLoaded: () => true,
444
+ createChatCompletion: async ({ onData }) => {
445
+ const malformed = tracedChunk('short', 404);
446
+ malformed.choices[0]?.logprobs?.content?.[0]?.top_logprobs.pop();
447
+ onData(malformed);
448
+ },
449
+ };
450
+
451
+ await expect(runtime.generate(
452
+ 'request-short-top-logprobs',
453
+ { messages: [{ role: 'user', content: 'Count.' }], returnTokenIds: true },
454
+ new AbortController().signal,
455
+ () => undefined,
456
+ )).rejects.toMatchObject({
457
+ code: 'INVALID_TOKEN_LOGPROB_TRACE',
458
+ details: { index: 0, expected: 5, observed: 4 },
459
+ });
460
+ });
461
+
462
+ it('fails loudly when a top candidate has an invalid logprob', async () => {
463
+ const runtime = new BrowserEngineRuntime();
464
+ const internals = runtime as unknown as RuntimeInternals;
465
+ internals.loaded = {
466
+ manifest: {},
467
+ backend: 'wasm',
468
+ model: {
469
+ id: '1_7b',
470
+ displayName: 'Fixture Bonsai',
471
+ cpuFallback: true,
472
+ runtimePolicy: {
473
+ flashAttention: false,
474
+ tokenEmbeddingOnWebGPU: true,
475
+ requireSingleWebGPUGraph: false,
476
+ },
477
+ },
478
+ };
479
+ internals.wllama = {
480
+ isModelLoaded: () => true,
481
+ createChatCompletion: async ({ onData }) => {
482
+ const malformed = tracedChunk('invalid', 505);
483
+ const candidate = malformed.choices[0]?.logprobs?.content?.[0]?.top_logprobs[1];
484
+ if (!candidate) throw new Error('Malformed trace fixture requires a top candidate.');
485
+ candidate.logprob = Number.NaN;
486
+ onData(malformed);
487
+ },
488
+ };
489
+
490
+ await expect(runtime.generate(
491
+ 'request-invalid-top-logprob',
492
+ { messages: [{ role: 'user', content: 'Count.' }], returnTokenIds: true },
493
+ new AbortController().signal,
494
+ () => undefined,
495
+ )).rejects.toMatchObject({
496
+ code: 'INVALID_TOKEN_LOGPROB_TRACE',
497
+ details: { index: 0, field: 'topCandidates[1].logprob' },
498
+ });
499
  });
500
  });
src/engine/runtime-score-sequence.test.ts ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type { BackendReport } from './native-log';
3
+ import { BrowserEngineRuntime } from './runtime';
4
+
5
+ interface RawCompletionOptions {
6
+ prompt: number[];
7
+ max_tokens: number;
8
+ temperature: number;
9
+ top_k: number;
10
+ logprobs: number;
11
+ logit_bias: Record<string, number>;
12
+ cache_prompt: boolean;
13
+ post_sampling_probs: boolean;
14
+ abortSignal: AbortSignal;
15
+ }
16
+
17
+ interface RuntimeInternals {
18
+ wllama: {
19
+ isModelLoaded(): boolean;
20
+ createCompletion(options: RawCompletionOptions): Promise<unknown>;
21
+ } | null;
22
+ loaded: {
23
+ manifest: unknown;
24
+ backend: 'webgpu';
25
+ tuningScope: 'benchmark';
26
+ contextSize: number;
27
+ batchSize: number;
28
+ microBatchSize: number;
29
+ vocabularySize: number;
30
+ model: {
31
+ id: '27b';
32
+ displayName: string;
33
+ cpuFallback: false;
34
+ runtimePolicy: {
35
+ flashAttention: false;
36
+ tokenEmbeddingOnWebGPU: true;
37
+ requireSingleWebGPUGraph: true;
38
+ };
39
+ };
40
+ } | null;
41
+ nativeLog: {
42
+ report(): BackendReport;
43
+ };
44
+ }
45
+
46
+ const backendReport: BackendReport = {
47
+ backends: ['WebGPU'],
48
+ nGraphSplits: 1,
49
+ opsOnCpu: 0,
50
+ layersGpu: { offloaded: 65, total: 65 },
51
+ flashAttention: false,
52
+ cacheTypeK: 'f16',
53
+ cacheTypeV: 'f16',
54
+ webgpuKvBufferBytes: 128 * 1024 ** 2,
55
+ };
56
+
57
+ function configuredRuntime(createCompletion: (options: RawCompletionOptions) => Promise<unknown>) {
58
+ const runtime = new BrowserEngineRuntime();
59
+ const internals = runtime as unknown as RuntimeInternals;
60
+ internals.loaded = {
61
+ manifest: {},
62
+ backend: 'webgpu',
63
+ tuningScope: 'benchmark',
64
+ contextSize: 2_048,
65
+ batchSize: 32,
66
+ microBatchSize: 16,
67
+ vocabularySize: 248_320,
68
+ model: {
69
+ id: '27b',
70
+ displayName: 'Fixture Bonsai 27B',
71
+ cpuFallback: false,
72
+ runtimePolicy: {
73
+ flashAttention: false,
74
+ tokenEmbeddingOnWebGPU: true,
75
+ requireSingleWebGPUGraph: true,
76
+ },
77
+ },
78
+ };
79
+ internals.wllama = { isModelLoaded: () => true, createCompletion };
80
+ vi.spyOn(internals.nativeLog, 'report').mockReturnValue(backendReport);
81
+ return runtime;
82
+ }
83
+
84
+ describe('BrowserEngineRuntime teacher-forced scoring', () => {
85
+ it('scores the exact CPU sequence with raw token prefixes and keeps natural top-1 separate', async () => {
86
+ const calls: RawCompletionOptions[] = [];
87
+ const createCompletion = vi.fn(async (options: RawCompletionOptions) => {
88
+ calls.push(options);
89
+ const index = options.prompt.length - 38;
90
+ const referenceId = Number(Object.keys(options.logit_bias)[0]);
91
+ const naturalTop1Id = index === 29 ? referenceId + 10 : referenceId;
92
+ const candidates = index === 29
93
+ ? [
94
+ { id: naturalTop1Id, token: 'natural', logprob: -0.01, bytes: null },
95
+ { id: referenceId, token: 'reference', logprob: -0.02, bytes: null },
96
+ { id: referenceId + 20, token: 'third', logprob: -1, bytes: null },
97
+ { id: referenceId + 21, token: 'fourth', logprob: -2, bytes: null },
98
+ { id: referenceId + 22, token: 'fifth', logprob: -3, bytes: null },
99
+ ]
100
+ : [
101
+ { id: referenceId, token: 'reference', logprob: -0.01, bytes: null },
102
+ { id: referenceId + 10, token: 'second', logprob: -0.02, bytes: null },
103
+ { id: referenceId + 20, token: 'third', logprob: -1, bytes: null },
104
+ { id: referenceId + 21, token: 'fourth', logprob: -2, bytes: null },
105
+ { id: referenceId + 22, token: 'fifth', logprob: -3, bytes: null },
106
+ ];
107
+ return {
108
+ choices: [{
109
+ text: 'forced',
110
+ finish_reason: 'length',
111
+ logprobs: {
112
+ content: [{
113
+ id: referenceId,
114
+ token: 'reference',
115
+ logprob: index === 29 ? -0.02 : -0.01,
116
+ bytes: null,
117
+ top_logprobs: candidates,
118
+ }],
119
+ },
120
+ }],
121
+ };
122
+ });
123
+ const runtime = configuredRuntime(createCompletion);
124
+ const promptTokenIds = Array.from({ length: 38 }, (_, index) => index + 1);
125
+ const referenceTokenIds = Array.from({ length: 1_024 }, (_, index) => index + 1_000);
126
+
127
+ const result = await runtime.scoreSequence({
128
+ promptTokenIds,
129
+ referenceTokenIds,
130
+ topK: 5,
131
+ }, new AbortController().signal);
132
+
133
+ expect(createCompletion).toHaveBeenCalledTimes(1_024);
134
+ expect(calls[0]).toMatchObject({
135
+ prompt: promptTokenIds,
136
+ max_tokens: 1,
137
+ temperature: 0,
138
+ top_k: 1,
139
+ logprobs: 5,
140
+ logit_bias: { '1000': 1_000 },
141
+ cache_prompt: false,
142
+ post_sampling_probs: false,
143
+ });
144
+ expect(calls[1]).toMatchObject({
145
+ prompt: [...promptTokenIds, 1_000],
146
+ cache_prompt: true,
147
+ });
148
+ expect(calls.at(-1)?.prompt).toEqual([
149
+ ...promptTokenIds,
150
+ ...referenceTokenIds.slice(0, -1),
151
+ ]);
152
+ expect(result.entries[29]).toMatchObject({
153
+ index: 29,
154
+ selectedReference: { id: 1_029, logprob: -0.02 },
155
+ naturalTop1: { id: 1_039, logprob: -0.01 },
156
+ referenceRankInTopCandidatesZeroBased: 1,
157
+ top1Top2Margin: 0.01,
158
+ });
159
+ expect(result.summary.tokenCount).toBe(1_024);
160
+ expect(result.summary.meanNll).toBeCloseTo((1_023 * 0.01 + 0.02) / 1_024, 12);
161
+ expect(result.summary.perplexity).toBeCloseTo(Math.exp(result.summary.meanNll), 12);
162
+ });
163
+
164
+ it('honors an already-aborted diagnostic request before the first raw completion', async () => {
165
+ const createCompletion = vi.fn(async () => ({}));
166
+ const runtime = configuredRuntime(createCompletion);
167
+ const controller = new AbortController();
168
+ controller.abort();
169
+
170
+ await expect(runtime.scoreSequence({
171
+ promptTokenIds: Array.from({ length: 38 }, (_, index) => index + 1),
172
+ referenceTokenIds: Array.from({ length: 1_024 }, (_, index) => index + 1_000),
173
+ topK: 5,
174
+ }, controller.signal)).rejects.toMatchObject({ name: 'AbortError' });
175
+ expect(createCompletion).not.toHaveBeenCalled();
176
+ });
177
+
178
+ it('fails loudly when logit bias does not return the fixed reference token', async () => {
179
+ const runtime = configuredRuntime(async (options) => {
180
+ const referenceId = Number(Object.keys(options.logit_bias)[0]);
181
+ const selectedId = referenceId + 1;
182
+ return {
183
+ choices: [{
184
+ logprobs: {
185
+ content: [{
186
+ id: selectedId,
187
+ logprob: -0.01,
188
+ top_logprobs: [
189
+ { id: selectedId, logprob: -0.01 },
190
+ { id: referenceId, logprob: -0.02 },
191
+ { id: referenceId + 2, logprob: -1 },
192
+ { id: referenceId + 3, logprob: -2 },
193
+ { id: referenceId + 4, logprob: -3 },
194
+ ],
195
+ }],
196
+ },
197
+ }],
198
+ };
199
+ });
200
+
201
+ await expect(runtime.scoreSequence({
202
+ promptTokenIds: Array.from({ length: 38 }, (_, index) => index + 1),
203
+ referenceTokenIds: Array.from({ length: 1_024 }, (_, index) => index + 1_000),
204
+ topK: 5,
205
+ }, new AbortController().signal)).rejects.toMatchObject({
206
+ code: 'INVALID_SCORE_SEQUENCE_RESPONSE',
207
+ details: { index: 0, referenceTokenId: 1_000 },
208
+ });
209
+ });
210
+
211
+ it('rejects scoring outside the loaded 27B WebGPU benchmark path', async () => {
212
+ const runtime = configuredRuntime(async () => ({}));
213
+ const internals = runtime as unknown as RuntimeInternals;
214
+ if (!internals.loaded) throw new Error('Expected loaded fixture state.');
215
+ (internals.loaded as { tuningScope: string }).tuningScope = 'release-defaults';
216
+
217
+ await expect(runtime.scoreSequence({
218
+ promptTokenIds: Array.from({ length: 38 }, (_, index) => index + 1),
219
+ referenceTokenIds: Array.from({ length: 1_024 }, (_, index) => index + 1_000),
220
+ topK: 5,
221
+ }, new AbortController().signal)).rejects.toMatchObject({
222
+ code: 'SCORE_SEQUENCE_UNAVAILABLE',
223
+ });
224
+ });
225
+ });
src/engine/runtime.ts CHANGED
@@ -10,6 +10,7 @@ import runtimeSource from '../../vendor/wllama-bonsai/SOURCE.json';
10
  import {
11
  assertBackendPolicy,
12
  estimateStorage,
 
13
  evaluateModelGate,
14
  inspectWebGpuAdapter,
15
  persistStorage,
@@ -36,11 +37,14 @@ import type {
36
  BenchmarkWasmFlavor,
37
  EngineCapabilities,
38
  EngineEvent,
 
39
  GenerateParams,
40
  GenerateResult,
41
  LoadModelParams,
42
  LoadModelResult,
43
  RuntimeBackend,
 
 
44
  ShardDownloadFailureDetails,
45
  StorageEstimate,
46
  } from './protocol';
@@ -52,6 +56,10 @@ const WLLAMA_COMPAT_WASM_PATH = '/wasm/wllama-compat.wasm';
52
  const WLLAMA_COMPAT_WORKER_PATH = '/wasm/wllama-compat.js';
53
  const MAX_GLUE_INT = 2_147_483_647;
54
  const DEVICE_LOST_EXIT_GRACE_MS = 100;
 
 
 
 
55
 
56
  export interface ResolvedLoadTuning {
57
  scope: 'release-defaults' | 'benchmark';
@@ -183,6 +191,11 @@ interface LoadedModelState {
183
  manifest: ModelManifestV2;
184
  model: ManifestModelV2;
185
  backend: RuntimeBackend;
 
 
 
 
 
186
  }
187
 
188
  interface CachedShardState {
@@ -312,6 +325,244 @@ function mapTimings(timings: ResultTimings | undefined): GenerateResult['timings
312
  };
313
  }
314
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  function asReasoningDelta(chunk: ChatCompletionChunk): string | undefined {
316
  const choice = chunk.choices[0] as unknown as Record<string, unknown> | undefined;
317
  const delta = choice?.delta;
@@ -685,6 +936,19 @@ export class BrowserEngineRuntime {
685
  shardPath: null,
686
  });
687
  const model = findManifestModel(manifest, params.modelId);
 
 
 
 
 
 
 
 
 
 
 
 
 
688
  const urls = orderedShardUrls(manifest, model);
689
  const wllama = await this.ensureWllama(wasmFlavor);
690
  const cached = await this.inspectCachedShards(wllama, model, urls, signal);
@@ -713,13 +977,6 @@ export class BrowserEngineRuntime {
713
  shardCount: model.files.length,
714
  shardPath: null,
715
  });
716
- const contextSize = params.contextSize ?? model.defaultContext;
717
- if (!Number.isSafeInteger(contextSize) || contextSize <= 0 || contextSize > model.contextLength) {
718
- throw new EngineRuntimeError(
719
- 'INVALID_CONTEXT_SIZE',
720
- `Context size must be between 1 and ${model.contextLength}.`,
721
- );
722
- }
723
  const defaultThreads = Math.max(1, Math.floor((navigator.hardwareConcurrency || 1) / 2));
724
  const threads = params.threads ?? (gate.selectedBackend === 'wasm' ? defaultThreads : 1);
725
  if (!Number.isSafeInteger(threads) || threads <= 0) {
@@ -801,7 +1058,16 @@ export class BrowserEngineRuntime {
801
  tuningResult,
802
  );
803
  }
804
- this.loaded = { manifest, model, backend: gate.selectedBackend };
 
 
 
 
 
 
 
 
 
805
  emitProgress(sink, requestId, {
806
  phase: 'load',
807
  loadedBytes: model.downloadBytes,
@@ -860,9 +1126,11 @@ export class BrowserEngineRuntime {
860
  }
861
  throwIfAborted(signal);
862
  let text = '';
 
863
  let finishReason: GenerateResult['finishReason'] = null;
864
  let usage: GenerateResult['usage'] = null;
865
  let timings: GenerateResult['timings'] = null;
 
866
  const streamedToolCalls = new ToolCallAccumulator();
867
  try {
868
  await this.raceWebGpuDeviceLoss('generate', loaded.backend, loaded.model, wllama.createChatCompletion({
@@ -877,15 +1145,20 @@ export class BrowserEngineRuntime {
877
  tools: params.tools as ChatCompletionTool[] | undefined,
878
  tool_choice: params.toolChoice,
879
  cache_prompt: params.cachePrompt ?? true,
 
 
 
880
  abortSignal: signal,
881
  timings_per_token: true,
882
  onData: (chunk: ChatCompletionChunk) => {
 
883
  streamedToolCalls.append(chunk.choices[0]?.delta.tool_calls);
884
  const delta = chunk.choices[0]?.delta.content;
885
  const textDelta = typeof delta === 'string' ? delta : '';
886
  const reasoningDelta = asReasoningDelta(chunk);
887
  if (textDelta || reasoningDelta) {
888
  text += textDelta;
 
889
  sink({
890
  type: 'event',
891
  requestId,
@@ -906,6 +1179,8 @@ export class BrowserEngineRuntime {
906
  }
907
  await this.assertWebGpuAlive('generate', loaded.backend, loaded.model);
908
  throwIfAborted(signal);
 
 
909
  const report = this.nativeLog.report();
910
  try {
911
  assertBackendPolicy(loaded.model, loaded.backend, report);
@@ -926,7 +1201,172 @@ export class BrowserEngineRuntime {
926
  error instanceof Error ? error.message : String(error),
927
  );
928
  }
929
- return { text, finishReason, toolCalls, usage, timings };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
930
  }
931
 
932
  async backendReport(): Promise<BackendReport> {
 
10
  import {
11
  assertBackendPolicy,
12
  estimateStorage,
13
+ evaluateModelContextPolicy,
14
  evaluateModelGate,
15
  inspectWebGpuAdapter,
16
  persistStorage,
 
37
  BenchmarkWasmFlavor,
38
  EngineCapabilities,
39
  EngineEvent,
40
+ EngineSampledTokenTraceEntry,
41
  GenerateParams,
42
  GenerateResult,
43
  LoadModelParams,
44
  LoadModelResult,
45
  RuntimeBackend,
46
+ ScoreSequenceParams,
47
+ ScoreSequenceResult,
48
  ShardDownloadFailureDetails,
49
  StorageEstimate,
50
  } from './protocol';
 
56
  const WLLAMA_COMPAT_WORKER_PATH = '/wasm/wllama-compat.js';
57
  const MAX_GLUE_INT = 2_147_483_647;
58
  const DEVICE_LOST_EXIT_GRACE_MS = 100;
59
+ const TOKEN_TRACE_TOP_LOGPROBS = 5;
60
+ const STATE_DRIFT_REFERENCE_TOKENS = 1_024;
61
+ const STATE_DRIFT_PROMPT_TOKENS = 38;
62
+ const TEACHER_FORCE_LOGIT_BIAS = 1_000;
63
 
64
  export interface ResolvedLoadTuning {
65
  scope: 'release-defaults' | 'benchmark';
 
191
  manifest: ModelManifestV2;
192
  model: ManifestModelV2;
193
  backend: RuntimeBackend;
194
+ tuningScope: ResolvedLoadTuning['scope'];
195
+ contextSize: number;
196
+ batchSize: number;
197
+ microBatchSize: number;
198
+ vocabularySize: number;
199
  }
200
 
201
  interface CachedShardState {
 
325
  };
326
  }
327
 
328
+ function traceTokenId(value: unknown, index: number, field: string): number {
329
+ if (!Number.isSafeInteger(value) || (value as number) < 0 || (value as number) > 0xffff_ffff) {
330
+ throw new EngineRuntimeError(
331
+ 'INVALID_TOKEN_ID_TRACE',
332
+ 'The native runtime returned a missing or invalid token id in the sampled-token trace.',
333
+ { index, field, id: value },
334
+ );
335
+ }
336
+ return value as number;
337
+ }
338
+
339
+ function traceLogprob(value: unknown, index: number, field: string): number {
340
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
341
+ throw new EngineRuntimeError(
342
+ 'INVALID_TOKEN_LOGPROB_TRACE',
343
+ 'The native runtime returned a missing or invalid logprob in the sampled-token trace.',
344
+ { index, field, logprob: value },
345
+ );
346
+ }
347
+ return value;
348
+ }
349
+
350
+ function appendSampledTokenTrace(
351
+ target: EngineSampledTokenTraceEntry[],
352
+ chunk: ChatCompletionChunk,
353
+ ): void {
354
+ const entries = chunk.choices[0]?.logprobs?.content;
355
+ if (!entries) return;
356
+ for (const entry of entries) {
357
+ const index = target.length;
358
+ const selected = {
359
+ id: traceTokenId(entry.id, index, 'selected.id'),
360
+ logprob: traceLogprob(entry.logprob, index, 'selected.logprob'),
361
+ };
362
+ if (!Array.isArray(entry.top_logprobs) || entry.top_logprobs.length !== TOKEN_TRACE_TOP_LOGPROBS) {
363
+ throw new EngineRuntimeError(
364
+ 'INVALID_TOKEN_LOGPROB_TRACE',
365
+ `The native runtime must return exactly ${TOKEN_TRACE_TOP_LOGPROBS} top logprob candidates per sampled token.`,
366
+ {
367
+ index,
368
+ expected: TOKEN_TRACE_TOP_LOGPROBS,
369
+ observed: Array.isArray(entry.top_logprobs) ? entry.top_logprobs.length : null,
370
+ },
371
+ );
372
+ }
373
+ const seenIds = new Set<number>();
374
+ const topCandidates = entry.top_logprobs.map((candidate, candidateIndex) => {
375
+ const id = traceTokenId(candidate.id, index, `topCandidates[${candidateIndex}].id`);
376
+ if (seenIds.has(id)) {
377
+ throw new EngineRuntimeError(
378
+ 'INVALID_TOKEN_LOGPROB_TRACE',
379
+ 'The native runtime returned duplicate ids in a top-logprob candidate list.',
380
+ { index, candidateIndex, id },
381
+ );
382
+ }
383
+ seenIds.add(id);
384
+ return {
385
+ id,
386
+ logprob: traceLogprob(
387
+ candidate.logprob,
388
+ index,
389
+ `topCandidates[${candidateIndex}].logprob`,
390
+ ),
391
+ };
392
+ }).sort((left, right) => right.logprob - left.logprob || left.id - right.id);
393
+ const selectedCandidate = topCandidates.find((candidate) => candidate.id === selected.id);
394
+ if (!selectedCandidate || selectedCandidate.logprob !== selected.logprob) {
395
+ throw new EngineRuntimeError(
396
+ 'INVALID_TOKEN_LOGPROB_TRACE',
397
+ 'The sampled token must appear exactly once in its top-logprob candidates with the same logprob.',
398
+ { index, selected, selectedCandidate: selectedCandidate ?? null },
399
+ );
400
+ }
401
+ target.push({ selected, topCandidates });
402
+ }
403
+ }
404
+
405
+ function assertCompleteTokenTrace(
406
+ tokenTrace: EngineSampledTokenTraceEntry[] | null,
407
+ usage: GenerateResult['usage'],
408
+ ): void {
409
+ if (tokenTrace === null) return;
410
+ if (usage === null || tokenTrace.length !== usage.completionTokens) {
411
+ throw new EngineRuntimeError(
412
+ 'INCOMPLETE_TOKEN_ID_TRACE',
413
+ 'The native runtime did not return exactly one complete sampled-token trace per completion token.',
414
+ {
415
+ expected: usage?.completionTokens ?? null,
416
+ observed: tokenTrace.length,
417
+ },
418
+ );
419
+ }
420
+ }
421
+
422
+ function scoreRecord(value: unknown, index: number, field: string): Record<string, unknown> {
423
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
424
+ throw new EngineRuntimeError(
425
+ 'INVALID_SCORE_SEQUENCE_RESPONSE',
426
+ `Teacher-forced response ${index} has an invalid ${field}.`,
427
+ { index, field },
428
+ );
429
+ }
430
+ return value as Record<string, unknown>;
431
+ }
432
+
433
+ function scoreTokenId(
434
+ value: unknown,
435
+ index: number,
436
+ field: string,
437
+ vocabularySize: number,
438
+ ): number {
439
+ if (!Number.isSafeInteger(value) || (value as number) < 0 || (value as number) >= vocabularySize) {
440
+ throw new EngineRuntimeError(
441
+ 'INVALID_SCORE_SEQUENCE_RESPONSE',
442
+ `Teacher-forced response ${index} has an invalid ${field}.`,
443
+ { index, field, id: value, vocabularySize },
444
+ );
445
+ }
446
+ return value as number;
447
+ }
448
+
449
+ function scoreLogprob(value: unknown, index: number, field: string): number {
450
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
451
+ throw new EngineRuntimeError(
452
+ 'INVALID_SCORE_SEQUENCE_RESPONSE',
453
+ `Teacher-forced response ${index} has an invalid ${field}.`,
454
+ { index, field, logprob: value },
455
+ );
456
+ }
457
+ return value;
458
+ }
459
+
460
+ function parseTeacherForcedResponse(
461
+ response: unknown,
462
+ index: number,
463
+ referenceTokenId: number,
464
+ vocabularySize: number,
465
+ ): ScoreSequenceResult['entries'][number] {
466
+ const root = scoreRecord(response, index, 'root');
467
+ if (!Array.isArray(root.choices) || root.choices.length !== 1) {
468
+ throw new EngineRuntimeError(
469
+ 'INVALID_SCORE_SEQUENCE_RESPONSE',
470
+ `Teacher-forced response ${index} must contain exactly one completion choice.`,
471
+ { index, observed: Array.isArray(root.choices) ? root.choices.length : null },
472
+ );
473
+ }
474
+ const choice = scoreRecord(root.choices[0], index, 'choices[0]');
475
+ const logprobs = scoreRecord(choice.logprobs, index, 'choices[0].logprobs');
476
+ if (!Array.isArray(logprobs.content) || logprobs.content.length !== 1) {
477
+ throw new EngineRuntimeError(
478
+ 'INVALID_SCORE_SEQUENCE_RESPONSE',
479
+ `Teacher-forced response ${index} must contain one selected-token logprob entry.`,
480
+ { index, observed: Array.isArray(logprobs.content) ? logprobs.content.length : null },
481
+ );
482
+ }
483
+ const selectedEntry = scoreRecord(logprobs.content[0], index, 'logprobs.content[0]');
484
+ const selectedReference = {
485
+ id: scoreTokenId(selectedEntry.id, index, 'selectedReference.id', vocabularySize),
486
+ logprob: scoreLogprob(selectedEntry.logprob, index, 'selectedReference.logprob'),
487
+ };
488
+ if (selectedReference.id !== referenceTokenId) {
489
+ throw new EngineRuntimeError(
490
+ 'INVALID_SCORE_SEQUENCE_RESPONSE',
491
+ `Teacher forcing selected token ${selectedReference.id} instead of reference token ${referenceTokenId} at position ${index + 1}.`,
492
+ { index, selectedTokenId: selectedReference.id, referenceTokenId },
493
+ );
494
+ }
495
+ const rawTopLogprobs = selectedEntry.top_logprobs;
496
+ if (!Array.isArray(rawTopLogprobs)
497
+ || rawTopLogprobs.length !== TOKEN_TRACE_TOP_LOGPROBS) {
498
+ throw new EngineRuntimeError(
499
+ 'INVALID_SCORE_SEQUENCE_RESPONSE',
500
+ `Teacher-forced response ${index} must contain exactly ${TOKEN_TRACE_TOP_LOGPROBS} natural top candidates.`,
501
+ {
502
+ index,
503
+ observed: Array.isArray(rawTopLogprobs)
504
+ ? rawTopLogprobs.length
505
+ : null,
506
+ },
507
+ );
508
+ }
509
+ const seenIds = new Set<number>();
510
+ const topCandidates = rawTopLogprobs.map((rawCandidate, candidateIndex) => {
511
+ const candidate = scoreRecord(
512
+ rawCandidate,
513
+ index,
514
+ `topCandidates[${candidateIndex}]`,
515
+ );
516
+ const parsed = {
517
+ id: scoreTokenId(
518
+ candidate.id,
519
+ index,
520
+ `topCandidates[${candidateIndex}].id`,
521
+ vocabularySize,
522
+ ),
523
+ logprob: scoreLogprob(
524
+ candidate.logprob,
525
+ index,
526
+ `topCandidates[${candidateIndex}].logprob`,
527
+ ),
528
+ };
529
+ if (seenIds.has(parsed.id)) {
530
+ throw new EngineRuntimeError(
531
+ 'INVALID_SCORE_SEQUENCE_RESPONSE',
532
+ `Teacher-forced response ${index} contains duplicate natural candidate id ${parsed.id}.`,
533
+ { index, candidateIndex, id: parsed.id },
534
+ );
535
+ }
536
+ seenIds.add(parsed.id);
537
+ return parsed;
538
+ }).sort((left, right) => right.logprob - left.logprob || left.id - right.id);
539
+ const referenceRankInTopCandidatesZeroBased = topCandidates.findIndex(
540
+ (candidate) => candidate.id === referenceTokenId,
541
+ );
542
+ if (
543
+ referenceRankInTopCandidatesZeroBased !== -1
544
+ && topCandidates[referenceRankInTopCandidatesZeroBased]?.logprob !== selectedReference.logprob
545
+ ) {
546
+ throw new EngineRuntimeError(
547
+ 'INVALID_SCORE_SEQUENCE_RESPONSE',
548
+ `Teacher-forced response ${index} reports inconsistent reference logprobs.`,
549
+ { index, selectedReference, referenceRankInTopCandidatesZeroBased },
550
+ );
551
+ }
552
+ const naturalTop1 = topCandidates[0]!;
553
+ const runnerUp = topCandidates[1]!;
554
+ return {
555
+ index,
556
+ selectedReference,
557
+ naturalTop1: { ...naturalTop1 },
558
+ topCandidates,
559
+ referenceRankInTopCandidatesZeroBased: referenceRankInTopCandidatesZeroBased === -1
560
+ ? null
561
+ : referenceRankInTopCandidatesZeroBased,
562
+ top1Top2Margin: naturalTop1.logprob - runnerUp.logprob,
563
+ };
564
+ }
565
+
566
  function asReasoningDelta(chunk: ChatCompletionChunk): string | undefined {
567
  const choice = chunk.choices[0] as unknown as Record<string, unknown> | undefined;
568
  const delta = choice?.delta;
 
936
  shardPath: null,
937
  });
938
  const model = findManifestModel(manifest, params.modelId);
939
+ const contextSize = params.contextSize ?? model.defaultContext;
940
+ const contextPolicy = evaluateModelContextPolicy(model, contextSize, {
941
+ tuningScope: tuning.scope,
942
+ requestedBackend: params.backend,
943
+ flashMode: tuning.flashMode,
944
+ kvCacheType: tuning.kvCacheType,
945
+ });
946
+ if (!contextPolicy.allowed) {
947
+ throw new EngineRuntimeError(
948
+ 'INVALID_CONTEXT_SIZE',
949
+ `Context size must be between 1 and ${contextPolicy.limit} for this model and runtime policy.`,
950
+ );
951
+ }
952
  const urls = orderedShardUrls(manifest, model);
953
  const wllama = await this.ensureWllama(wasmFlavor);
954
  const cached = await this.inspectCachedShards(wllama, model, urls, signal);
 
977
  shardCount: model.files.length,
978
  shardPath: null,
979
  });
 
 
 
 
 
 
 
980
  const defaultThreads = Math.max(1, Math.floor((navigator.hardwareConcurrency || 1) / 2));
981
  const threads = params.threads ?? (gate.selectedBackend === 'wasm' ? defaultThreads : 1);
982
  if (!Number.isSafeInteger(threads) || threads <= 0) {
 
1058
  tuningResult,
1059
  );
1060
  }
1061
+ this.loaded = {
1062
+ manifest,
1063
+ model,
1064
+ backend: gate.selectedBackend,
1065
+ tuningScope: tuning.scope,
1066
+ contextSize: context.n_ctx,
1067
+ batchSize: context.n_batch,
1068
+ microBatchSize: context.n_ubatch,
1069
+ vocabularySize: context.n_vocab,
1070
+ };
1071
  emitProgress(sink, requestId, {
1072
  phase: 'load',
1073
  loadedBytes: model.downloadBytes,
 
1126
  }
1127
  throwIfAborted(signal);
1128
  let text = '';
1129
+ let reasoningText = '';
1130
  let finishReason: GenerateResult['finishReason'] = null;
1131
  let usage: GenerateResult['usage'] = null;
1132
  let timings: GenerateResult['timings'] = null;
1133
+ const tokenTrace: EngineSampledTokenTraceEntry[] | null = params.returnTokenIds === true ? [] : null;
1134
  const streamedToolCalls = new ToolCallAccumulator();
1135
  try {
1136
  await this.raceWebGpuDeviceLoss('generate', loaded.backend, loaded.model, wllama.createChatCompletion({
 
1145
  tools: params.tools as ChatCompletionTool[] | undefined,
1146
  tool_choice: params.toolChoice,
1147
  cache_prompt: params.cachePrompt ?? true,
1148
+ ...(params.returnTokenIds === true
1149
+ ? { logprobs: true, top_logprobs: TOKEN_TRACE_TOP_LOGPROBS }
1150
+ : {}),
1151
  abortSignal: signal,
1152
  timings_per_token: true,
1153
  onData: (chunk: ChatCompletionChunk) => {
1154
+ if (tokenTrace !== null) appendSampledTokenTrace(tokenTrace, chunk);
1155
  streamedToolCalls.append(chunk.choices[0]?.delta.tool_calls);
1156
  const delta = chunk.choices[0]?.delta.content;
1157
  const textDelta = typeof delta === 'string' ? delta : '';
1158
  const reasoningDelta = asReasoningDelta(chunk);
1159
  if (textDelta || reasoningDelta) {
1160
  text += textDelta;
1161
+ reasoningText += reasoningDelta ?? '';
1162
  sink({
1163
  type: 'event',
1164
  requestId,
 
1179
  }
1180
  await this.assertWebGpuAlive('generate', loaded.backend, loaded.model);
1181
  throwIfAborted(signal);
1182
+ assertCompleteTokenTrace(tokenTrace, usage);
1183
+ const tokenIds = tokenTrace?.map((entry) => entry.selected.id) ?? null;
1184
  const report = this.nativeLog.report();
1185
  try {
1186
  assertBackendPolicy(loaded.model, loaded.backend, report);
 
1201
  error instanceof Error ? error.message : String(error),
1202
  );
1203
  }
1204
+ return { text, reasoningText, tokenIds, tokenTrace, finishReason, toolCalls, usage, timings };
1205
+ }
1206
+
1207
+ async scoreSequence(
1208
+ params: ScoreSequenceParams,
1209
+ signal: AbortSignal,
1210
+ ): Promise<ScoreSequenceResult> {
1211
+ const loaded = this.loaded;
1212
+ const wllama = this.wllama;
1213
+ if (!loaded || !wllama?.isModelLoaded()) {
1214
+ throw new EngineRuntimeError('MODEL_NOT_LOADED', 'Load a model before scoring a sequence.');
1215
+ }
1216
+ if (
1217
+ loaded.model.id !== '27b'
1218
+ || loaded.backend !== 'webgpu'
1219
+ || loaded.tuningScope !== 'benchmark'
1220
+ ) {
1221
+ throw new EngineRuntimeError(
1222
+ 'SCORE_SEQUENCE_UNAVAILABLE',
1223
+ 'Teacher-forced scoring is diagnostic-only and requires the loaded 27B WebGPU benchmark path.',
1224
+ {
1225
+ modelId: loaded.model.id,
1226
+ backend: loaded.backend,
1227
+ tuningScope: loaded.tuningScope,
1228
+ },
1229
+ );
1230
+ }
1231
+ if (params.topK !== TOKEN_TRACE_TOP_LOGPROBS) {
1232
+ throw new EngineRuntimeError(
1233
+ 'INVALID_SCORE_SEQUENCE',
1234
+ `Teacher-forced scoring requires topK=${TOKEN_TRACE_TOP_LOGPROBS}.`,
1235
+ );
1236
+ }
1237
+ if (!Array.isArray(params.promptTokenIds)
1238
+ || params.promptTokenIds.length !== STATE_DRIFT_PROMPT_TOKENS) {
1239
+ throw new EngineRuntimeError(
1240
+ 'INVALID_SCORE_SEQUENCE',
1241
+ `Teacher-forced scoring requires exactly ${STATE_DRIFT_PROMPT_TOKENS} rendered prompt token ids.`,
1242
+ );
1243
+ }
1244
+ if (!Array.isArray(params.referenceTokenIds)
1245
+ || params.referenceTokenIds.length !== STATE_DRIFT_REFERENCE_TOKENS) {
1246
+ throw new EngineRuntimeError(
1247
+ 'INVALID_SCORE_SEQUENCE',
1248
+ `Teacher-forced scoring requires exactly ${STATE_DRIFT_REFERENCE_TOKENS} reference token ids.`,
1249
+ );
1250
+ }
1251
+ if (params.promptTokenIds.length + params.referenceTokenIds.length > loaded.contextSize) {
1252
+ throw new EngineRuntimeError(
1253
+ 'INVALID_SCORE_SEQUENCE',
1254
+ 'The rendered prompt and fixed reference sequence exceed the loaded context.',
1255
+ {
1256
+ promptTokens: params.promptTokenIds.length,
1257
+ referenceTokens: params.referenceTokenIds.length,
1258
+ contextSize: loaded.contextSize,
1259
+ },
1260
+ );
1261
+ }
1262
+ const validateInputTokenIds = (values: readonly number[], field: string): void => {
1263
+ for (const [index, tokenId] of values.entries()) {
1264
+ if (!Number.isSafeInteger(tokenId) || tokenId < 0 || tokenId >= loaded.vocabularySize) {
1265
+ throw new EngineRuntimeError(
1266
+ 'INVALID_SCORE_SEQUENCE',
1267
+ `${field}[${index}] is outside the loaded vocabulary.`,
1268
+ { field, index, tokenId, vocabularySize: loaded.vocabularySize },
1269
+ );
1270
+ }
1271
+ }
1272
+ };
1273
+ validateInputTokenIds(params.promptTokenIds, 'promptTokenIds');
1274
+ validateInputTokenIds(params.referenceTokenIds, 'referenceTokenIds');
1275
+ await this.assertWebGpuAlive('score-sequence', loaded.backend, loaded.model);
1276
+ throwIfAborted(signal);
1277
+
1278
+ const score = async (): Promise<ScoreSequenceResult> => {
1279
+ const entries: ScoreSequenceResult['entries'] = [];
1280
+ const createRawCompletion = wllama.createCompletion.bind(wllama) as unknown as (
1281
+ options: Record<string, unknown>,
1282
+ ) => Promise<unknown>;
1283
+ for (let index = 0; index < params.referenceTokenIds.length; index += 1) {
1284
+ throwIfAborted(signal);
1285
+ const referenceTokenId = params.referenceTokenIds[index]!;
1286
+ const prompt = [
1287
+ ...params.promptTokenIds,
1288
+ ...params.referenceTokenIds.slice(0, index),
1289
+ ];
1290
+ const response = await createRawCompletion({
1291
+ // The pinned llama.cpp endpoint accepts a raw token-id prompt even though
1292
+ // upstream wllama's OAI declaration still narrows this field to strings.
1293
+ prompt,
1294
+ stream: false,
1295
+ max_tokens: 1,
1296
+ temperature: 0,
1297
+ top_k: 1,
1298
+ logprobs: TOKEN_TRACE_TOP_LOGPROBS,
1299
+ logit_bias: { [String(referenceTokenId)]: TEACHER_FORCE_LOGIT_BIAS },
1300
+ cache_prompt: index !== 0,
1301
+ post_sampling_probs: false,
1302
+ abortSignal: signal,
1303
+ });
1304
+ throwIfAborted(signal);
1305
+ entries.push(parseTeacherForcedResponse(
1306
+ response,
1307
+ index,
1308
+ referenceTokenId,
1309
+ loaded.vocabularySize,
1310
+ ));
1311
+ }
1312
+ const meanNll = -entries.reduce(
1313
+ (sum, entry) => sum + entry.selectedReference.logprob,
1314
+ 0,
1315
+ ) / entries.length;
1316
+ const perplexity = Math.exp(meanNll);
1317
+ if (!Number.isFinite(meanNll) || !Number.isFinite(perplexity)) {
1318
+ throw new EngineRuntimeError(
1319
+ 'INVALID_SCORE_SEQUENCE_RESPONSE',
1320
+ 'Teacher-forced scoring produced a non-finite mean NLL or perplexity.',
1321
+ { meanNll, perplexity },
1322
+ );
1323
+ }
1324
+ return {
1325
+ method: {
1326
+ promptMode: 'raw-token-id-prefix',
1327
+ maxTokensPerStep: 1,
1328
+ temperature: 0,
1329
+ topK: 1,
1330
+ reportedTopLogprobs: TOKEN_TRACE_TOP_LOGPROBS,
1331
+ logitBias: TEACHER_FORCE_LOGIT_BIAS,
1332
+ cachePromptFirst: false,
1333
+ cachePromptSubsequent: true,
1334
+ },
1335
+ entries,
1336
+ summary: {
1337
+ tokenCount: STATE_DRIFT_REFERENCE_TOKENS,
1338
+ meanNll,
1339
+ perplexity,
1340
+ },
1341
+ };
1342
+ };
1343
+
1344
+ try {
1345
+ const result = await this.raceWebGpuDeviceLoss(
1346
+ 'score-sequence',
1347
+ loaded.backend,
1348
+ loaded.model,
1349
+ score(),
1350
+ );
1351
+ await this.assertWebGpuAlive('score-sequence', loaded.backend, loaded.model);
1352
+ throwIfAborted(signal);
1353
+ const report = this.nativeLog.report();
1354
+ try {
1355
+ assertBackendPolicy(loaded.model, loaded.backend, report);
1356
+ } catch (error) {
1357
+ await this.unload();
1358
+ throw new EngineRuntimeError(
1359
+ 'BACKEND_TRIPWIRE',
1360
+ error instanceof Error ? error.message : String(error),
1361
+ report,
1362
+ );
1363
+ }
1364
+ return result;
1365
+ } catch (error) {
1366
+ if (error instanceof EngineRuntimeError && error.code === 'WEBGPU_DEVICE_LOST') throw error;
1367
+ await this.assertWebGpuAlive('score-sequence', loaded.backend, loaded.model);
1368
+ throw error;
1369
+ }
1370
  }
1371
 
1372
  async backendReport(): Promise<BackendReport> {
src/engine/worker.ts CHANGED
@@ -137,6 +137,14 @@ async function handleRequest(request: EngineRequest): Promise<void> {
137
  )),
138
  );
139
  return;
 
 
 
 
 
 
 
 
140
  case 'abort': {
141
  const operation = active.get(request.params.targetRequestId);
142
  const aborted = operation !== undefined && !operation.controller.signal.aborted;
 
137
  )),
138
  );
139
  return;
140
+ case 'scoreSequence':
141
+ postSuccess(
142
+ request,
143
+ await runExclusive(request.requestId, (signal) => (
144
+ runtime.scoreSequence(request.params, signal)
145
+ )),
146
+ );
147
+ return;
148
  case 'abort': {
149
  const operation = active.get(request.params.targetRequestId);
150
  const aborted = operation !== undefined && !operation.controller.signal.aborted;
vendor/wllama-bonsai/PATCH.diff CHANGED
@@ -23,7 +23,7 @@ index f1496a4..8883324 100644
23
  GLUE_FIELD(int, n_threads)
24
  GLUE_FIELD_NULLABLE(str, model_alias)
25
  diff --git a/cpp/wllama-context.h b/cpp/wllama-context.h
26
- index 0dafe6d..c4b46f9 100644
27
  --- a/cpp/wllama-context.h
28
  +++ b/cpp/wllama-context.h
29
  @@ -19,6 +19,7 @@
@@ -34,7 +34,7 @@ index 0dafe6d..c4b46f9 100644
34
 
35
  #include "ggml-cpu.h"
36
  #include "ggml-backend.h"
37
- @@ -300,7 +301,7 @@ struct wllama_context
38
  server_task task = server_task(SERVER_TASK_TYPE_COMPLETION);
39
  task.id = rd->get_new_id();
40
  task.index = 0;
@@ -43,7 +43,31 @@ index 0dafe6d..c4b46f9 100644
43
  vocab,
44
  params,
45
  meta->slot_n_ctx,
46
- @@ -415,6 +416,8 @@ struct wllama_context
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  params.embedding = req.embeddings.value;
48
  if (req.n_batch.not_null())
49
  params.n_batch = req.n_batch.value;
@@ -52,7 +76,7 @@ index 0dafe6d..c4b46f9 100644
52
  if (req.n_parallel.not_null())
53
  params.n_parallel = req.n_parallel.value;
54
  if (req.pooling_type.not_null())
55
- @@ -568,6 +571,24 @@ struct wllama_context
56
  // load model
57
  llama_backend_init();
58
  llama_numa_init(params.numa);
@@ -101,6 +125,18 @@ index 08f0d87..c1001da 100644
101
  n_ctx: number;
102
  n_threads: number;
103
  model_alias?: string | undefined;
 
 
 
 
 
 
 
 
 
 
 
 
104
  diff --git a/src/types/types.ts b/src/types/types.ts
105
  index 2b3cca7..e91b395 100644
106
  --- a/src/types/types.ts
@@ -235,3 +271,17 @@ index ce74b2b..1796ca1 100644
235
  if (!this.compat) {
236
  this.logger().warn(
237
  'Not using compat mode' +
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  GLUE_FIELD(int, n_threads)
24
  GLUE_FIELD_NULLABLE(str, model_alias)
25
  diff --git a/cpp/wllama-context.h b/cpp/wllama-context.h
26
+ index 0dafe6d..d261dfe 100644
27
  --- a/cpp/wllama-context.h
28
  +++ b/cpp/wllama-context.h
29
  @@ -19,6 +19,7 @@
 
34
 
35
  #include "ggml-cpu.h"
36
  #include "ggml-backend.h"
37
+ @@ -300,16 +301,28 @@ struct wllama_context
38
  server_task task = server_task(SERVER_TASK_TYPE_COMPLETION);
39
  task.id = rd->get_new_id();
40
  task.index = 0;
 
43
  vocab,
44
  params,
45
  meta->slot_n_ctx,
46
+ meta->logit_bias_eog,
47
+ body);
48
+ task.params.res_type = res_type;
49
+ - task.cli_prompt = prompt;
50
+ - task.cli_files = files;
51
+ - task.cli = true;
52
+ + if (prompt.is_array())
53
+ + {
54
+ + auto tokenized_prompts = tokenize_input_prompts(vocab, nullptr, prompt, true, true);
55
+ + if (tokenized_prompts.size() != 1)
56
+ + {
57
+ + throw app_exception("wllama completion accepts exactly one token-id prompt");
58
+ + }
59
+ + task.tokens = std::move(tokenized_prompts[0]);
60
+ + }
61
+ + else
62
+ + {
63
+ + task.cli_prompt = prompt.get<std::string>();
64
+ + task.cli_files = files;
65
+ + task.cli = true;
66
+ + }
67
+
68
+ rd->post_task({std::move(task)});
69
+ }
70
+ @@ -415,6 +428,8 @@ struct wllama_context
71
  params.embedding = req.embeddings.value;
72
  if (req.n_batch.not_null())
73
  params.n_batch = req.n_batch.value;
 
76
  if (req.n_parallel.not_null())
77
  params.n_parallel = req.n_parallel.value;
78
  if (req.pooling_type.not_null())
79
+ @@ -568,6 +583,24 @@ struct wllama_context
80
  // load model
81
  llama_backend_init();
82
  llama_numa_init(params.numa);
 
125
  n_ctx: number;
126
  n_threads: number;
127
  model_alias?: string | undefined;
128
+ diff --git a/src/types/oai-compat.ts b/src/types/oai-compat.ts
129
+ index dbeeaa2..fd41519 100644
130
+ --- a/src/types/oai-compat.ts
131
+ +++ b/src/types/oai-compat.ts
132
+ @@ -135,6 +135,7 @@ export type ChatCompletionParams = {
133
+ // Response types----------
134
+
135
+ export interface ChatCompletionLogprob {
136
+ + id: number;
137
+ token: string;
138
+ logprob: number;
139
+ bytes: number[] | null;
140
  diff --git a/src/types/types.ts b/src/types/types.ts
141
  index 2b3cca7..e91b395 100644
142
  --- a/src/types/types.ts
 
271
  if (!this.compat) {
272
  this.logger().warn(
273
  'Not using compat mode' +
274
+ diff --git a/src/workers-code/llama-cpp.js b/src/workers-code/llama-cpp.js
275
+ index be62030..9f5144a 100644
276
+ --- a/src/workers-code/llama-cpp.js
277
+ +++ b/src/workers-code/llama-cpp.js
278
+ @@ -468,6 +468,9 @@ onmessage = async (e) => {
279
+ );
280
+ inputBuffer.set(argEncodedMsg, 0);
281
+ const outputPtr = await wllamaAction(argAction, inputPtr);
282
+ + if (!outputPtr) {
283
+ + throw new Error(`wllama_action("${argAction}") returned a null pointer`);
284
+ + }
285
+ // length of output buffer is written at the first 4 bytes of input buffer
286
+ const outputLen = new Uint32Array(
287
+ getHeapU8().buffer,
vendor/wllama-bonsai/SOURCE.json CHANGED
@@ -4,19 +4,21 @@
4
  "wllamaRevision": "912c18b75d4358c1405a64646b8dbe43a205943b",
5
  "llamaCppRevision": "00fa7cb284cbf133fc426733bd64238a3588a33e",
6
  "license": "MIT",
7
- "patch": "Expose offload_token_embedding and n_ubatch; place token_embd.weight on WebGPU whenever GPU layers are requested; fail loudly when the requested WebGPU buffer is unavailable; add an opt-in forceCompat selector for controlled benchmark A/B runs.",
8
  "patchSet": {
9
  "format": "git-diff-binary",
10
  "path": "vendor/wllama-bonsai/PATCH.diff",
11
- "bytes": 9350,
12
- "sha256": "ad8786a295eaeb75ee03752d7f84d30f14ea373407844d5c589f7ab46800bb4b",
13
  "files": [
14
  "CMakeLists.txt",
15
  "cpp/glue.hpp",
16
  "cpp/wllama-context.h",
17
  "src/glue/messages.ts",
 
18
  "src/types/types.ts",
19
- "src/wllama.ts"
 
20
  ]
21
  },
22
  "build": {
@@ -28,18 +30,18 @@
28
  "files": [
29
  {
30
  "path": "vendor/wllama-bonsai/esm/index.js",
31
- "bytes": 365397,
32
- "sha256": "24bcef8aea8e27fb7b7e2d9e6ea94ba8ced7bfbffd09a0675821b9eb1b4a4c9f"
33
  },
34
  {
35
  "path": "public/wasm/wllama.wasm",
36
- "bytes": 8017169,
37
- "sha256": "dd71f58c75f32c677a64eeb2e56efab7dedbe7395f730bb08bc80021bd182caa"
38
  },
39
  {
40
  "path": "public/wasm/wllama-compat.wasm",
41
- "bytes": 14864188,
42
- "sha256": "eca9754d0d8a490b5c7b58cb384d67e31aa8ab26dd330e4ad1460982df6dd82e"
43
  },
44
  {
45
  "path": "public/wasm/wllama-compat.js",
 
4
  "wllamaRevision": "912c18b75d4358c1405a64646b8dbe43a205943b",
5
  "llamaCppRevision": "00fa7cb284cbf133fc426733bd64238a3588a33e",
6
  "license": "MIT",
7
+ "patch": "Expose offload_token_embedding and n_ubatch; place token_embd.weight on WebGPU whenever GPU layers are requested; fail loudly when the requested WebGPU buffer is unavailable; add an opt-in forceCompat selector for controlled benchmark A/B runs; type sampled token ids in OAI logprob payloads; accept one raw token-id completion prompt and fail loudly on null action responses.",
8
  "patchSet": {
9
  "format": "git-diff-binary",
10
  "path": "vendor/wllama-bonsai/PATCH.diff",
11
+ "bytes": 11052,
12
+ "sha256": "27b96fa48c6fd66b44fe70e83cc775902e29ec5877261c7eb04fecabc529d2f9",
13
  "files": [
14
  "CMakeLists.txt",
15
  "cpp/glue.hpp",
16
  "cpp/wllama-context.h",
17
  "src/glue/messages.ts",
18
+ "src/types/oai-compat.ts",
19
  "src/types/types.ts",
20
+ "src/wllama.ts",
21
+ "src/workers-code/llama-cpp.js"
22
  ]
23
  },
24
  "build": {
 
30
  "files": [
31
  {
32
  "path": "vendor/wllama-bonsai/esm/index.js",
33
+ "bytes": 365536,
34
+ "sha256": "10c3811776e34092a225632929b97046af4215a4c91156169105c886be5cba5d"
35
  },
36
  {
37
  "path": "public/wasm/wllama.wasm",
38
+ "bytes": 8017784,
39
+ "sha256": "b292bf670a25e1ecf02c71dd92b49196f3336842135f1f85efd5a3fd5f36ea8a"
40
  },
41
  {
42
  "path": "public/wasm/wllama-compat.wasm",
43
+ "bytes": 14865338,
44
+ "sha256": "ea0623d2ca1758287a569169b4ad5a69c22e41bce3e5d022c7d3e4fb5c37918c"
45
  },
46
  {
47
  "path": "public/wasm/wllama-compat.js",
vendor/wllama-bonsai/esm/index.cjs CHANGED
The diff for this file is too large to render. See raw diff
 
vendor/wllama-bonsai/esm/index.js CHANGED
The diff for this file is too large to render. See raw diff
 
vendor/wllama-bonsai/esm/index.min.js CHANGED
The diff for this file is too large to render. See raw diff
 
vendor/wllama-bonsai/esm/index.min.js.map CHANGED
The diff for this file is too large to render. See raw diff
 
vendor/wllama-bonsai/esm/types/oai-compat.d.ts CHANGED
@@ -96,6 +96,7 @@ export type ChatCompletionParams = {
96
  timings_per_token?: boolean;
97
  } & SamplingParams;
98
  export interface ChatCompletionLogprob {
 
99
  token: string;
100
  logprob: number;
101
  bytes: number[] | null;
 
96
  timings_per_token?: boolean;
97
  } & SamplingParams;
98
  export interface ChatCompletionLogprob {
99
+ id: number;
100
  token: string;
101
  logprob: number;
102
  bytes: number[] | null;
vendor/wllama-bonsai/esm/workers-code/generated.d.ts CHANGED
The diff for this file is too large to render. See raw diff