jev-web 0.7.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +773 -502
- package/docs/demo-model-picker.png +0 -0
- package/package.json +64 -60
- package/src/answers.mjs +36 -36
- package/src/bekko.mjs +439 -439
- package/src/clef.mjs +498 -0
- package/src/config.mjs +65 -65
- package/src/decision2.mjs +382 -382
- package/src/decision3.mjs +571 -0
- package/src/encode.mjs +83 -83
- package/src/index.mjs +196 -112
- package/src/kev.mjs +296 -296
- package/src/laya.mjs +630 -630
- package/src/questions.mjs +46 -39
- package/src/registry.mjs +199 -155
- package/src/session.mjs +151 -151
- package/src/strands.mjs +718 -535
- package/src/vega.mjs +1125 -0
package/README.md
CHANGED
|
@@ -1,503 +1,774 @@
|
|
|
1
|
-
# jev-web
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/jev-web)
|
|
4
|
-
[](./LICENSE)
|
|
5
|
-
[](https://github.com/warsang/jev-web/actions/workflows/test.yml)
|
|
6
|
-
|
|
7
|
-
Run **typed-decision models** in the browser. One *state* text
|
|
8
|
-
plus any number of typed *questions* (`choice` / `score` / `noul`) go in; a
|
|
9
|
-
calibrated probability distribution per question comes back from a **single
|
|
10
|
-
forward pass**. No server, no API keys, no network calls after the weights are
|
|
11
|
-
cached.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
(ModernBERT encoder + typed head), Strands Decider 2B (Qwen torso + pointer
|
|
15
|
-
head), Bekko System One v0 (Ettin cross-encoder), Decision 2.0 (Qwen3 +
|
|
16
|
-
candidate head),
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
`
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
`
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
`
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
`
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
(`
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
`
|
|
357
|
-
|
|
358
|
-
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
(
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
`
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
`
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
`
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
`
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
1
|
+
# jev-web
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/jev-web)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://github.com/warsang/jev-web/actions/workflows/test.yml)
|
|
6
|
+
|
|
7
|
+
Run **typed-decision models** in the browser. One *state* text
|
|
8
|
+
plus any number of typed *questions* (`choice` / `score` / `noul`) go in; a
|
|
9
|
+
calibrated probability distribution per question comes back from a **single
|
|
10
|
+
forward pass**. No server, no API keys, no network calls after the weights are
|
|
11
|
+
cached.
|
|
12
|
+
|
|
13
|
+
Nine model families ship today — open-jev (fused DeBERTa graph), Laya
|
|
14
|
+
(ModernBERT encoder + typed head), Strands Decider 2B (Qwen torso + pointer
|
|
15
|
+
head), Bekko System One v0 (Ettin cross-encoder), Decision 2.0 (Qwen3 +
|
|
16
|
+
candidate head), Kev (Qwen3 + pointer head), Vega 0.8B (frozen Qwen3.5
|
|
17
|
+
torso + latent decision-physics engine), clef (Qwen3 torso + joint schema
|
|
18
|
+
head), and Decision 3.0 (d3 code readout) — behind one registry:
|
|
19
|
+
`createDecisionRuntime({ family, fallback })`.
|
|
20
|
+
|
|
21
|
+
This is a generic runtime, not a classifier for any particular domain. You bring
|
|
22
|
+
the state text and the questions; the model answers by choosing among the options
|
|
23
|
+
you give it.
|
|
24
|
+
|
|
25
|
+
## ▶ Live demo
|
|
26
|
+
|
|
27
|
+
**https://warsang.github.io/jev-web/**
|
|
28
|
+
|
|
29
|
+
An interactive playground that runs this exact package: type a state, add typed
|
|
30
|
+
questions, and watch the model and a hand-written keyword baseline answer
|
|
31
|
+
side by side. It also counts your actual outbound requests, so the
|
|
32
|
+
"offline / private / no server" claim is checkable rather than asserted. The
|
|
33
|
+
model selector loads **any of the nine registered families** — open-jev by
|
|
34
|
+
default, plus Bekko, Kev, Laya, Decision 2.0/3.0, Vega 0.8B (fused or the
|
|
35
|
+
8-bit split export), clef and Strands Decider — and `?model=<family>`
|
|
36
|
+
deep-links one.
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
The picker is part of the demo: every family answers through the same
|
|
41
|
+
`decide()` contract, and the pass counter reports each family's real cost
|
|
42
|
+
(split Vega reads the state once per question, the others batch).
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
<sub>The frames are captured from the live demo at
|
|
47
|
+
[warsang.github.io/jev-web](https://warsang.github.io/jev-web/) — site → typed
|
|
48
|
+
decisions with latency → the negation case where the rules and the model
|
|
49
|
+
disagree → the comparison matrix.</sub>
|
|
50
|
+
|
|
51
|
+
Three questions, one forward pass, and a hard label from the rule parser that
|
|
52
|
+
could not answer the third question at all:
|
|
53
|
+
|
|
54
|
+

|
|
55
|
+
|
|
56
|
+
## Quickstart
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import { createDecider } from "jev-web";
|
|
60
|
+
|
|
61
|
+
const decider = await createDecider(); // ~340 MB of weights, once
|
|
62
|
+
const { answers } = await decider.decide(
|
|
63
|
+
"I was charged twice for the same order.",
|
|
64
|
+
[{ type: "noul", instructions: "The customer is asking for a refund." }],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
answers[0].noul; // 0.92 — p(yes)
|
|
68
|
+
answers[0].probabilities; // { no: 0.08, yes: 0.92 }
|
|
69
|
+
answers[0].confidence; // 0.92
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
That is the whole integration. The first call downloads the weights and the
|
|
73
|
+
browser caches them; every later call — and every later visit, including
|
|
74
|
+
offline — is local. **Zero bundled dependencies**: `onnxruntime-web` is a peer
|
|
75
|
+
the package manager installs, `@huggingface/transformers` is an optional peer
|
|
76
|
+
imported lazily, and hosts can inject either one.
|
|
77
|
+
|
|
78
|
+
## What comes back
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
const { answers } = await decider.decide(
|
|
82
|
+
"I was charged twice for the same order and nobody answers my emails. I want my money back now.",
|
|
83
|
+
[
|
|
84
|
+
{ type: "choice", instructions: "Which product area is the message about?",
|
|
85
|
+
options: ["fees & charges", "refund & dispute", "card", "other"] },
|
|
86
|
+
{ type: "noul", instructions: "The customer is asking for a refund." },
|
|
87
|
+
{ type: "score", instructions: "How negative is the message?",
|
|
88
|
+
options: ["very negative", "negative", "neutral", "positive", "very positive"] },
|
|
89
|
+
],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// [
|
|
93
|
+
// { type: "choice", choice: "fees & charges", index: 0,
|
|
94
|
+
// probabilities: { "fees & charges": 0.796, "refund & dispute": 0.169,
|
|
95
|
+
// card: 0.019, other: 0.016 }, confidence: 0.796 },
|
|
96
|
+
// { type: "noul", noul: 0.927,
|
|
97
|
+
// probabilities: { no: 0.073, yes: 0.927 }, confidence: 0.927 },
|
|
98
|
+
// { type: "score", score: 0.866, level: 1,
|
|
99
|
+
// probabilities: { "very negative": 0.336, negative: 0.478, neutral: 0.175,
|
|
100
|
+
// positive: 0.008, "very positive": 0.003 }, confidence: 0.478 },
|
|
101
|
+
// ]
|
|
102
|
+
// timings: { totalMs, inferMs } · length, truncated, prompts
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Three questions, **one** forward pass, and every question carries the full
|
|
106
|
+
distribution — not just a label. That last part is the point: a hard label
|
|
107
|
+
cannot be thresholded, routed by confidence, or second-guessed, and the
|
|
108
|
+
`choice` answer above is a genuine 79.6 / 16.9 split rather than a coin flip you
|
|
109
|
+
find out about later.
|
|
110
|
+
|
|
111
|
+
## jev-web vs. calling a hosted LLM API with `fetch()`
|
|
112
|
+
|
|
113
|
+
| | jev-web | Hosted LLM API via `fetch()` |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| **Cost per decision** | **$0.00** — the weights are static files | per-token, forever; a busy form multiplies it by every visitor |
|
|
116
|
+
| **Latency** | one local forward pass, measured live on the demo page | typically 0.4–3 s, plus a network round trip |
|
|
117
|
+
| **Network at answer time** | **0 requests** — works in airplane mode | one round trip per call, minimum |
|
|
118
|
+
| **Output shape** | **calibrated distribution per question, every question in one pass** | prose you then have to regex back into a distribution |
|
|
119
|
+
| **Determinism** | same input → same probabilities, always | temperature 0 + structured outputs helps; still not guaranteed |
|
|
120
|
+
| **Privacy** | your text never leaves the device | your users' text is sent to a third party on every call |
|
|
121
|
+
| **Ops burden** | static files on a CDN, no key, no rate limit | key management, quotas, rate limits, uptime, spend alerts |
|
|
122
|
+
| **Ceiling** | only *picks among options you supply* — never writes, never explains | general purpose; will happily invent an option you never offered |
|
|
123
|
+
|
|
124
|
+
**The honest trade.** jev-web is a classifier, not a chatbot. If you need
|
|
125
|
+
free-form answers you need a generator. If you need a routing decision, a score,
|
|
126
|
+
or a yes/no with a confidence you can threshold on, this is the cheaper, faster
|
|
127
|
+
and private one — and it is also *smaller*: a 0.5 B model plus your options
|
|
128
|
+
beats a frontier model plus a parsing layer, on every axis except generality.
|
|
129
|
+
|
|
130
|
+
## Try it before you install it
|
|
131
|
+
|
|
132
|
+
The live demo covers the interesting cases, and the presets are chosen to break
|
|
133
|
+
keyword parsing rather than to flatter the model:
|
|
134
|
+
|
|
135
|
+
| preset | what it shows |
|
|
136
|
+
|---|---|
|
|
137
|
+
| **Banking triage** | easy text — a fair rule can match, the model adds a confidence |
|
|
138
|
+
| **Negation trap** | *"This is not about a refund. My card was stolen…"* — the keyword rule routes to `refund & dispute`; the model routes to `card` (53.3%) and answers the refund question **no** (0.101) |
|
|
139
|
+
| **Contradictory review** | praise and complaint in one sentence; keyword polarity reads the praise |
|
|
140
|
+
| **Mixed intent** | two intents in one state — a hard label must pick one, a distribution does not have to |
|
|
141
|
+
| **Out of domain** | no in-domain signal at all; the rule parser has nothing to say |
|
|
142
|
+
|
|
143
|
+
The negation case is the one worth looking at. The state is *"This is not about
|
|
144
|
+
a refund. My card was stolen in Madrid and the bank refuses to help. I never
|
|
145
|
+
received the money back I asked about."*
|
|
146
|
+
|
|
147
|
+

|
|
148
|
+
|
|
149
|
+
The rule parser finds the word `refund` and routes to **refund & dispute** — the
|
|
150
|
+
exact opposite of what the customer meant. The model routes to **card** (53.3%)
|
|
151
|
+
and answers *"the customer is asking for a refund"* with **no** (0.101). Both
|
|
152
|
+
the bad routing and the inversion come from the same clause, and neither is
|
|
153
|
+
visible in a hard label.
|
|
154
|
+
|
|
155
|
+
The demo also tracks outbound requests through the Performance API: you will see
|
|
156
|
+
~14 requests while the weights load, then **0** for every decision after that.
|
|
157
|
+
|
|
158
|
+
## Install
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npm i jev-web @huggingface/transformers
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`@huggingface/transformers` is an optional peer (imported lazily); hosts that
|
|
165
|
+
already ship their own copy can inject it via `createDecider({ transformers })`.
|
|
166
|
+
`onnxruntime-web` is a regular peer, so `npm i jev-web` installs it; hosts with
|
|
167
|
+
a custom runtime (local files, an older ORT, a worker) inject it via
|
|
168
|
+
`createDecider({ ort })`.
|
|
169
|
+
|
|
170
|
+
## Use
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
import { createDecider, DEFAULT_MODEL, DEFAULT_REVISION } from "jev-web";
|
|
174
|
+
|
|
175
|
+
const decider = await createDecider({
|
|
176
|
+
// defaults to the reference open-jev DeBERTa-v3-large ONNX export,
|
|
177
|
+
// pinned by revision; any repo with the same typed-decision graph works
|
|
178
|
+
onProgress: (p) => console.log(p.phase, p.file, p.progress),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const { answers } = await decider.decide(
|
|
182
|
+
"I was charged twice for the same order and nobody answers my emails.",
|
|
183
|
+
[
|
|
184
|
+
{
|
|
185
|
+
type: "choice",
|
|
186
|
+
instructions: "Which product area is the message about?",
|
|
187
|
+
options: ["fees & charges", "refund & dispute", "card", "other"],
|
|
188
|
+
},
|
|
189
|
+
{ type: "noul", instructions: "The customer is asking for a refund." },
|
|
190
|
+
{
|
|
191
|
+
type: "score",
|
|
192
|
+
instructions: "How negative is the message?",
|
|
193
|
+
options: ["very negative", "negative", "neutral", "positive", "very positive"],
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Question types
|
|
200
|
+
|
|
201
|
+
| type | options | answer |
|
|
202
|
+
|---|---|---|
|
|
203
|
+
| `choice` | 2–255 labels | `choice` (argmax), `probabilities`, `confidence` |
|
|
204
|
+
| `score` | 2–10 ordered levels | `score` (expected level index, may fall between levels) |
|
|
205
|
+
| `noul` | exactly 2 (defaults `["no","yes"]`) | `noul` (p of the second option = p(yes)), `confidence` |
|
|
206
|
+
|
|
207
|
+
Every answer carries the full distribution. `decide()` also returns
|
|
208
|
+
`{ truncated, length }` — the state is capped (256 tokens by default) and the
|
|
209
|
+
flag tells you when it was cut.
|
|
210
|
+
|
|
211
|
+
## Backends and quantization
|
|
212
|
+
|
|
213
|
+
`device` / `dtype` default to `auto`: WebGPU → `q4f16`, otherwise WASM → `q4`.
|
|
214
|
+
Override explicitly (`fp16`, `fp32`, `q8`, …) when accuracy matters more than
|
|
215
|
+
download size. First load downloads the weights once; transformers.js caches
|
|
216
|
+
them (Cache API / IndexedDB) so later visits are offline-fast.
|
|
217
|
+
|
|
218
|
+
`decider.info` reports the resolved `{ model, revision, device, dtype,
|
|
219
|
+
temperature, maxStateTokens, maxLen, pool, configSource }`.
|
|
220
|
+
|
|
221
|
+
## Model config
|
|
222
|
+
|
|
223
|
+
Defaults come from the model repo's `open_jev_config.json` when present
|
|
224
|
+
(temperature, marker tokens, state cap), else the published reference values.
|
|
225
|
+
To use a different typed-decision export:
|
|
226
|
+
|
|
227
|
+
```js
|
|
228
|
+
createDecider({ model: "your-org/your-typed-decision-ONNX", revision: "main" });
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The export must add the `[STATE]`, `[Q]`, `[OPT]` markers to its tokenizer,
|
|
232
|
+
take `input_ids, attention_mask, seg, pair_q, pair_opt`, and return per-pair
|
|
233
|
+
`logits`. `typedMarkerIds()` fails loudly when a repo is missing them.
|
|
234
|
+
|
|
235
|
+
## Multiple families (registry)
|
|
236
|
+
|
|
237
|
+
`jev-web` is a multi-family runtime: `createDecisionRuntime({ family, fallback })`
|
|
238
|
+
tries the requested family and then any fallbacks, returning the shared contract
|
|
239
|
+
`{ info, decide(state, questions), dispose? }`:
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
import { createDecisionRuntime, listDecisionFamilies } from "jev-web";
|
|
243
|
+
|
|
244
|
+
listDecisionFamilies(); // ["open-jev", "laya", "strands-decider", "bekko", "decision2", "kev", "vega", "clef", "decision3"]
|
|
245
|
+
const runtime = await createDecisionRuntime({
|
|
246
|
+
family: "strands-decider",
|
|
247
|
+
fallback: ["laya", "open-jev"],
|
|
248
|
+
onFallback: ({ family, error }) => console.warn(family, error),
|
|
249
|
+
});
|
|
250
|
+
const { answers, truncated, length, prompts, timings } = await runtime.decide(state, questions);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Third-party families plug in without touching core:
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
registerDecisionFamily("my-model", {
|
|
257
|
+
defaults: { model: "org/my-typed-decision-ONNX", revision: "main" },
|
|
258
|
+
create: async (opts) => ({ info, decide, dispose }),
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`decide()` returns `prompts` (decoded input sequence(s), for audit/debug) and
|
|
263
|
+
`timings` (per-stage ms) alongside the answers.
|
|
264
|
+
|
|
265
|
+
## Laya family (ModernBERT encoder + typed head)
|
|
266
|
+
|
|
267
|
+
`createLayaDecider(options)` runs the [Laya](https://huggingface.co/convaiinnovations/laya)
|
|
268
|
+
System-1 decision models, whose browser export is a **two-file q8 ONNX split**
|
|
269
|
+
(encoder + head) rather than one fused graph:
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
import { createLayaDecider } from "jev-web";
|
|
273
|
+
|
|
274
|
+
const decider = await createLayaDecider({
|
|
275
|
+
// defaults to alfred361/laya-typed-decisions-web-q8 @ pinned revision
|
|
276
|
+
wasmPaths: "/assets/", // self-hosted ORT wasm (optional)
|
|
277
|
+
onProgress: (p) => console.log(p.phase, p.loaded, p.total),
|
|
278
|
+
});
|
|
279
|
+
const { answers } = await decider.decide(stateText, questions);
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Protocol implemented here (mirrors the reference `laya` Python runtime):
|
|
283
|
+
|
|
284
|
+
- sequence `[CLS] <type> question: <instructions> [SEP] [MASK]opt0 … [SEP] <state> [SEP]`,
|
|
285
|
+
`marker_pos` at each option's `[MASK]`, `qtype` 0 choice / 1 score / 2 noul;
|
|
286
|
+
- encoder `input_ids, attention_mask → hidden`; head
|
|
287
|
+
`hidden, attention_mask, marker_pos, marker_mask, qtype → logits`;
|
|
288
|
+
- per-question calibration from `rl_agent_config.json`
|
|
289
|
+
(`temperature` + `temperature_by_options` buckets, clamped to [0.5, 5]).
|
|
290
|
+
|
|
291
|
+
Notes:
|
|
292
|
+
- the q8 web head is **batch-1 only**, so `decide()` batches the encoder and
|
|
293
|
+
runs the head once per question;
|
|
294
|
+
- `choice` supports at most `max_prefixes` options (6 for the typed-decisions
|
|
295
|
+
export);
|
|
296
|
+
- weights download once (~520 MB q8) and are browser-cached.
|
|
297
|
+
|
|
298
|
+
Lower-level helpers are exported for custom runtimes:
|
|
299
|
+
`buildLayaSequence`, `collateLayaItems`, `layaAnswersFromLogits`,
|
|
300
|
+
`layaTokenizerAdapter`, `renderLayaOptions`, `layaTempBucket`, `clampTemperature`.
|
|
301
|
+
|
|
302
|
+
## Strands Decider family (pointer head over a Qwen3.5 or Gemma 4 torso)
|
|
303
|
+
|
|
304
|
+
`createStrandsDecider(options)` runs
|
|
305
|
+
[Strands Decider 2B](https://strandsagents.com/blog/introducing-strands-decider/),
|
|
306
|
+
a decision model: a Qwen3.5 or Gemma 4 torso (LoRA merged) with its LM head
|
|
307
|
+
replaced by a pointer head that scores each option from its own hidden state
|
|
308
|
+
against the `<answer>` position. One fused ONNX graph, one forward pass for the
|
|
309
|
+
whole batch:
|
|
310
|
+
|
|
311
|
+
```js
|
|
312
|
+
import { createStrandsDecider } from "jev-web";
|
|
313
|
+
|
|
314
|
+
const decider = await createStrandsDecider({
|
|
315
|
+
// defaults to onnx-community/strands-decider-2B-hobson-v19-ONNX @ pinned
|
|
316
|
+
// revision, q8 (onnx/model_quantized.onnx)
|
|
317
|
+
onProgress: (p) => console.log(p.phase, p.loaded, p.total),
|
|
318
|
+
});
|
|
319
|
+
const { answers } = await decider.decide(stateText, questions);
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Protocol implemented here (mirrors the reference Python runtime):
|
|
323
|
+
|
|
324
|
+
- prompt `<state>…</state><question type="{noul|choice|score}">…<options>\n1. …\n</options></question><answer>`;
|
|
325
|
+
- graph `input_ids, attention_mask, answer_pos, option_pos → logits`, where
|
|
326
|
+
`answer_pos` is each row's last real token (the `<answer>` pooling position)
|
|
327
|
+
and `option_pos` carries each option line's last-token position (padded with
|
|
328
|
+
-1); the prompt is tokenised in chunks at pre-tokeniser piece boundaries so
|
|
329
|
+
those positions are exact with no offset mapping;
|
|
330
|
+
- choice questions accept `criteria` as `{label: description}` (mirroring the
|
|
331
|
+
reference schema); noul slots are fixed `false`/`true` with overridable
|
|
332
|
+
rubric descriptions;
|
|
333
|
+
- the question gets first claim on the 4096-token window (up to 75%), the state
|
|
334
|
+
is truncated from the right into what remains, and an over-long question is
|
|
335
|
+
truncated from the front, keeping the options and `<answer>`;
|
|
336
|
+
- **calibration comes from the checkpoint**, not from this package: the loader
|
|
337
|
+
reads `strands_decider_config.json` (`hobson_config.json` on v19) from the
|
|
338
|
+
model repo at start-up and applies its `temperature` / `temperature_by_kind`
|
|
339
|
+
/ `ordinal_smoothing` / `max_length`. A repo that ships no config falls back
|
|
340
|
+
to the pinned reference values (the current pin's temperatures: `noul` 0.911
|
|
341
|
+
/ `choice` 0.734 / `score` 1.328). `decider.info.configSource` names the file
|
|
342
|
+
that was used, or `null` for the pinned defaults;
|
|
343
|
+
- explicit `temperature` / `temperaturesByKind` / `ordinalSmoothing` / `maxLen`
|
|
344
|
+
options still override the checkpoint, which overrides the pinned values;
|
|
345
|
+
`calibrationModel` points the config load at another repo (e.g. the source
|
|
346
|
+
checkpoint when your own export predates config shipping);
|
|
347
|
+
- `score` confidence is ordinal (spread-based, with the training smoothing
|
|
348
|
+
floor), not max-probability.
|
|
349
|
+
|
|
350
|
+
Checkpoint compatibility (verified against the 2026-10-09 drop):
|
|
351
|
+
|
|
352
|
+
| checkpoint | params / q8 ONNX | browser? |
|
|
353
|
+
|---|---|---|
|
|
354
|
+
| `2B-hobson-v19` (current pin) | ~2.0B / 1.8 GB | ✅ shipped |
|
|
355
|
+
| `2B-qwen3.5-v1-2610`, `E2B-gemma4-v1-2610` | ~2.1B / ~2.1 GB | ✅ export-ready (E2B: see caveat below) |
|
|
356
|
+
| `E4B-gemma4-v1-2610` | ~5.7B / ~5.7 GB | ⚠️ opt-in heavy only |
|
|
357
|
+
| `12B-gemma4-v1-2610` | ~13.3B / ~13 GB | ❌ too heavy |
|
|
358
|
+
| `26B-A4B-gemma4-v1-2610` (MoE, 128 experts) | ~26B / ~26 GB | ❌ too heavy |
|
|
359
|
+
|
|
360
|
+
A new browser-eligible checkpoint needs an ONNX export plus a model/revision
|
|
361
|
+
repin — no runtime code change (`export/strands-decider/` documents the
|
|
362
|
+
pipeline and its browser-feasibility gate). Named checkpoints are addressed
|
|
363
|
+
through `STRANDS_VARIANTS` — temperatures are never baked into a variant,
|
|
364
|
+
they ship with the checkpoint:
|
|
365
|
+
|
|
366
|
+
```js
|
|
367
|
+
const decider = await createStrandsDecider({ variant: "qwen3.5-v1" }); // the newer Qwen torso
|
|
368
|
+
const decider2 = await createStrandsDecider(); // hobson-v19, the default
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
`qwen3.5-v1` currently ships the exact fp16 graph (~3.7 GB) because dynamic
|
|
372
|
+
q8 fails shape-inference on this graph's export metadata and the weight-only
|
|
373
|
+
path drifts on the GatedDeltaNet delta rule (see
|
|
374
|
+
`export/strands-decider/RESUME.md`), so it stays an opt-in until a quantized
|
|
375
|
+
build lands. Caveat: the E2B model card reports
|
|
376
|
+
a faulty GPU kernel path for *multi-question* yes/no answers before an upstream
|
|
377
|
+
attention fix; this runtime batches every question into one forward pass, so
|
|
378
|
+
validate multi-question parity before pinning E2B as a default.
|
|
379
|
+
|
|
380
|
+
Lower-level helpers are exported for custom runtimes:
|
|
381
|
+
`renderStrandsQuestion`, `renderStrandsState`, `renderStrandsContent`,
|
|
382
|
+
`buildStrandsBatch`, `collateStrandsItems`, `strandsOptionTokenIndices`,
|
|
383
|
+
`strandsAnswersFromLogits`, `strandsChoiceConfidence`, `strandsScoreConfidence`,
|
|
384
|
+
`parseStrandsCalibration`, `loadStrandsCalibration`.
|
|
385
|
+
|
|
386
|
+
## Bekko family (Ettin cross-encoder)
|
|
387
|
+
|
|
388
|
+
`createBekkoDecider(options)` runs
|
|
389
|
+
[Bekko System One v0](https://github.com/hotchpotch/bekko-system-one), a
|
|
390
|
+
cross-encoder that scores each (query, candidate) pair: the query renders as
|
|
391
|
+
`Instruction: {instruction}\nState: {state}` and every option becomes a
|
|
392
|
+
`Candidate: {id}: {description}` document. One ONNX graph
|
|
393
|
+
(`prefix_ids, prefix_mask, doc_ids, doc_mask, owners → logits [N, tasks]`),
|
|
394
|
+
one forward per question, with candidates batched by the reference
|
|
395
|
+
attention budget:
|
|
396
|
+
|
|
397
|
+
```js
|
|
398
|
+
import { createBekkoDecider } from "jev-web";
|
|
399
|
+
|
|
400
|
+
const decider = await createBekkoDecider({
|
|
401
|
+
// defaults to hotchpotch/bekko-system-one-v0-17m @ pinned revision,
|
|
402
|
+
// onnx_browser/ (29 MB)
|
|
403
|
+
onProgress: (p) => console.log(p.phase, p.loaded, p.total),
|
|
404
|
+
});
|
|
405
|
+
// or pick a larger sibling:
|
|
406
|
+
// await createBekkoDecider({ variant: "68m" }); // 196 MB
|
|
407
|
+
// await createBekkoDecider({ variant: "400m" }); // 1.43 GB, #2 on the S1MB leaderboard
|
|
408
|
+
const { answers } = await decider.decide(stateText, questions);
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Protocol implemented here (mirrors the reference browser runtime,
|
|
412
|
+
`browser/src/core.js` + `decision.js`, MIT):
|
|
413
|
+
|
|
414
|
+
- query `[cls] Instruction: …\nState: … [sep]`; instruction and state split the
|
|
415
|
+
4096-token budget 50/50 with spillover; candidates truncate to 2048 tokens;
|
|
416
|
+
- choice questions accept `criteria` as `{label: description}`; noul slots are
|
|
417
|
+
fixed `true`/`false` with authored meanings (overridable via criteria);
|
|
418
|
+
- score levels map to numeric candidate values `0..n-1`; the answer is the
|
|
419
|
+
expected value plus the argmax level and a normalised score.
|
|
420
|
+
|
|
421
|
+
Larger siblings are available via the `variant` option (`"17m"`, `"68m"`,
|
|
422
|
+
`"400m"`) or the exported model/revision constants: `BEKKO_68M_MODEL` /
|
|
423
|
+
`BEKKO_68M_REVISION` and `BEKKO_400M_MODEL` / `BEKKO_400M_REVISION` (same
|
|
424
|
+
`onnx_browser/` layout). The `BEKKO_VARIANTS` map carries each size's pinned
|
|
425
|
+
revision and download size.
|
|
426
|
+
|
|
427
|
+
Lower-level helpers are exported for custom runtimes:
|
|
428
|
+
`renderBekkoContent`, `renderBekkoRequest`, `tokenizeBekkoRequest`,
|
|
429
|
+
`collateBekkoDocs`, `bekkoSoftmax`, `bekkoAnswerFromLogits`.
|
|
430
|
+
|
|
431
|
+
## Decision 2.0 family (Qwen3 + candidate head)
|
|
432
|
+
|
|
433
|
+
`createDecision2Decider(options)` runs
|
|
434
|
+
[vLLM Semantic Router Decision 2.0](https://huggingface.co/collections/vllm-sr/decision-20),
|
|
435
|
+
a Qwen3 fine-tune with a candidate head: one prompt per question, each option
|
|
436
|
+
rendered as a `{"description": …, "key": …}` JSON segment. One ONNX graph
|
|
437
|
+
(`input_ids, attention_mask, answer_pos, option_pos → logits [B, K]`), one
|
|
438
|
+
forward pass for the whole batch:
|
|
439
|
+
|
|
440
|
+
```js
|
|
441
|
+
import { createDecision2Decider } from "jev-web";
|
|
442
|
+
|
|
443
|
+
const decider = await createDecision2Decider({
|
|
444
|
+
// defaults to onnx-community/Decision-2.0-Kai-0.6B-ONNX @ pinned revision,
|
|
445
|
+
// q8 (onnx/model_quantized.onnx, 0.61 GB)
|
|
446
|
+
onProgress: (p) => console.log(p.phase, p.loaded, p.total),
|
|
447
|
+
});
|
|
448
|
+
// or pick a larger sibling:
|
|
449
|
+
// await createDecision2Decider({ variant: "eos" }); // 0.8B, 16k context
|
|
450
|
+
// await createDecision2Decider({ variant: "sol" }); // 2B, 16k context
|
|
451
|
+
const { answers } = await decider.decide(stateText, questions);
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Protocol implemented here (mirrors the reference `decision2` runtime):
|
|
455
|
+
|
|
456
|
+
- segments `Context:` / `Task type:` / `Question:` / `Options:` / one
|
|
457
|
+
`<option>` JSON segment per option / the `Decision:` suffix, each tokenized
|
|
458
|
+
separately so `option_pos` (last token of each `</option>`) and `answer_pos`
|
|
459
|
+
(last token) are exact — validated 11/11 against the reference fixtures;
|
|
460
|
+
- choice questions accept `criteria` as `{label: description}` (empty
|
|
461
|
+
descriptions encode as `null`); noul slots are fixed `false`/`true`;
|
|
462
|
+
- score questions add the per-level `score_bias` from `config.json` before the
|
|
463
|
+
softmax (temperature 1).
|
|
464
|
+
|
|
465
|
+
Larger siblings are available via the `variant` option (`"kai"`, `"eos"`,
|
|
466
|
+
`"sol"`) or the exported model/revision constants: `DECISION2_EOS_MODEL` /
|
|
467
|
+
`DECISION2_EOS_REVISION` (0.8B) and `DECISION2_SOL_MODEL` /
|
|
468
|
+
`DECISION2_SOL_REVISION` (2B). The `DECISION2_VARIANTS` map carries each
|
|
469
|
+
variant's pinned revision and context size.
|
|
470
|
+
|
|
471
|
+
Lower-level helpers are exported for custom runtimes:
|
|
472
|
+
`renderDecision2Content`, `renderDecision2Options`, `buildDecision2Segments`,
|
|
473
|
+
`tokenizeDecision2Segments`, `decision2Softmax`, `decision2AnswerFromLogits`.
|
|
474
|
+
|
|
475
|
+
## Kev family (Qwen3 + pointer head)
|
|
476
|
+
|
|
477
|
+
`createKevDecider(options)` runs
|
|
478
|
+
[Kev](https://huggingface.co/jaredpalmer/kev-0.6b) (Apache-2.0), a typed
|
|
479
|
+
decision model: one state and any number of typed questions are packed into a
|
|
480
|
+
single sequence with five delimiter tokens; a block-causal mask derived
|
|
481
|
+
in-graph lets every question see the state and itself only, and a pointer head
|
|
482
|
+
scores each option's `</opt>` token against its question's `<decide>` token.
|
|
483
|
+
One ONNX graph (`input_ids, attention_mask → logits [B, seq]`), one forward
|
|
484
|
+
pass for the whole batch — read the logit at each option's `</opt>` position
|
|
485
|
+
and softmax within the question:
|
|
486
|
+
|
|
487
|
+
```js
|
|
488
|
+
import { createKevDecider } from "jev-web";
|
|
489
|
+
|
|
490
|
+
const decider = await createKevDecider({
|
|
491
|
+
// defaults to onnx-community/kev-0.6b-ONNX @ pinned revision,
|
|
492
|
+
// onnx/model_q4f16.onnx (335 MB)
|
|
493
|
+
onProgress: (p) => console.log(p.phase, p.loaded, p.total),
|
|
494
|
+
});
|
|
495
|
+
// or pick the larger sibling:
|
|
496
|
+
// await createKevDecider({ variant: "4b" }); // 1.07 GB, #29 on the Decision Index
|
|
497
|
+
const { answers } = await decider.decide(stateText, questions);
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
Protocol implemented here (mirrors the reference JS in the model README):
|
|
501
|
+
|
|
502
|
+
- delimiters `<|fim_prefix|>` (state), `<|fim_middle|>` (question),
|
|
503
|
+
`<|box_start|>` / `<|box_end|>` (option), `<|fim_suffix|>` (decide); ids
|
|
504
|
+
derived through the tokenizer, limits from `config.json`'s `kev` block
|
|
505
|
+
(8192 state / 8192 branch tokens, 255 options);
|
|
506
|
+
- caller text is escaped (`<|name|>` → `<¦name¦>`) so it can never forge a
|
|
507
|
+
delimiter;
|
|
508
|
+
- choice questions accept `criteria` as `{label: description}`, rendered as
|
|
509
|
+
`label: description`; noul defaults to `no` / `yes`.
|
|
510
|
+
|
|
511
|
+
The `KEV_VARIANTS` map (`"0.6b"`, `"4b"`) carries each size's pinned revision
|
|
512
|
+
and download size. Lower-level helpers are exported for custom runtimes:
|
|
513
|
+
`escapeKevText`, `packKevSequence`.
|
|
514
|
+
|
|
515
|
+
## Vega family (frozen Qwen3.5 backbone + latent decision-physics engine)
|
|
516
|
+
|
|
517
|
+
`createVegaDecider(options)` runs [Vega 0.8B](https://huggingface.co/nandakishorm/vega-08b-public-intents)
|
|
518
|
+
(by the Laya author): the language model is frozen and never trained — a 0.8B
|
|
519
|
+
Qwen3.5 torso is read once (layers 13 and 19) and a 14M-parameter physics
|
|
520
|
+
engine on top does the deciding. Each question drops a particle into a latent
|
|
521
|
+
space whose landscape holds one Gaussian well per option; a damped symplectic
|
|
522
|
+
integrator with a selective-SSM thermostat runs up to 12 steps (settled
|
|
523
|
+
particles freeze), and the Boltzmann occupancy of the wells is the answer
|
|
524
|
+
distribution:
|
|
525
|
+
|
|
526
|
+
```js
|
|
527
|
+
import { createVegaDecider } from "jev-web";
|
|
528
|
+
|
|
529
|
+
const decider = await createVegaDecider({
|
|
530
|
+
// variant "fp16" (default): warsang/vega-08b-web, fused torso + engine, 1.7 GB
|
|
531
|
+
onProgress: (p) => console.log(p.phase, p.loaded, p.total),
|
|
532
|
+
});
|
|
533
|
+
const q8 = await createVegaDecider({ variant: "q8" });
|
|
534
|
+
// nvkudva/vega-onnx-q8: 8-bit embed + cut decoder + fp32 engine, 840 MB,
|
|
535
|
+
// WebAssembly only (its MatMulNBits graphs have no WebGPU kernel)
|
|
536
|
+
const { answers } = await decider.decide(stateText, questions);
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Protocol implemented here (mirrors the reference `vega_common.py` / `vega_api.py`):
|
|
540
|
+
|
|
541
|
+
- prompt `[BOS] Observation:\n<state>\n\nMeasurement (choice|score|boolean): <instructions>\nPossible outcomes:\n* <option> …\nOutcome:` — the state comes first so its span never sees the question
|
|
542
|
+
(causal attention), and short states get a query-first prefix so the frozen
|
|
543
|
+
model reads knowing what is asked;
|
|
544
|
+
- perception: the torso emits per-segment span means for the
|
|
545
|
+
state/question/final-token slots, every option, and 16 equal state chunks
|
|
546
|
+
(the engine's memory reader) from the one causal read — pooled in-graph in
|
|
547
|
+
the fused variant, in JS over the decoder's layer-13/19 hidden states in
|
|
548
|
+
the split one; the engine graph (unrolled integrator; the settle-freeze
|
|
549
|
+
math is identical to the reference's early exit) emits raw logits plus the
|
|
550
|
+
particle physics;
|
|
551
|
+
- calibration is **thermal and checkpoint-driven**: `vega_config.json`'s
|
|
552
|
+
`calibration.theta` maps the particle's physical state (residual kinetic
|
|
553
|
+
energy, distance to the nearest well, steps used, option count) to a
|
|
554
|
+
temperature per question type; `decider.info.calibrationSource` names the
|
|
555
|
+
file used. The split export's `vega_web.json` also carries the two gated
|
|
556
|
+
task adapters (`public_intents`, `typed_decisions`): the engine's per-
|
|
557
|
+
question gates pick the adapter whose `calibration` block (theta, conformal,
|
|
558
|
+
abstain, unbound) calibrates that answer, and the answers gain `adapter`
|
|
559
|
+
and `gate`;
|
|
560
|
+
- answers carry the full distribution plus Vega's reliability machinery:
|
|
561
|
+
a split-conformal `predictionSet`, an `abstain` flag at the chosen risk
|
|
562
|
+
(`alpha` conformal level, `risk` abstention level, both per question type),
|
|
563
|
+
an `unbound` out-of-distribution flag (particle ended far from every
|
|
564
|
+
well), `settleSteps`, and the final `energy`;
|
|
565
|
+
- jev-web's `noul` maps to Vega's `boolean` (fixed `no` / `yes` slots);
|
|
566
|
+
`choice` accepts `criteria` as `{label: description}`, and `score` uses the
|
|
567
|
+
option texts as levels.
|
|
568
|
+
|
|
569
|
+
Variants (`variant`, or explicit `model`/`revision`/`format`):
|
|
570
|
+
|
|
571
|
+
| variant | export | graphs | size |
|
|
572
|
+
|---|---|---|---|
|
|
573
|
+
| `fp16` (default) | `warsang/vega-08b-web` | fused fp16 torso + fp32 engine, span means pooled in-graph | ~1.7 GB |
|
|
574
|
+
| `q8` | `nvkudva/vega-onnx-q8` (`v2/`) | 8-bit `embed_q8` + cut `decoder_q8` (layers 0–19, text M-RoPE positions, zeroed `past_*` state) + fp32 `engine` with both adapters; JS pools | ~840 MB |
|
|
575
|
+
|
|
576
|
+
Caveats: the fused export's gated task adapters are not shipped in its v1 — it
|
|
577
|
+
uses the base engine, the source repo's default; and that torso ships as the
|
|
578
|
+
exact fp16 graph because q8 on the fused graph drifts on the GatedDeltaNet
|
|
579
|
+
delta rule. The split export is text-only here (its vision encoder is not
|
|
580
|
+
wired in), runs one decoder read per question, is refused — `RangeError` —
|
|
581
|
+
rather than truncated when a state does not fit `maxLen`, and its checkpoints
|
|
582
|
+
were verified end-to-end against the reference (`export/vega/06_verify_q8_export.py`:
|
|
583
|
+
JS-vs-reference pooling exact, engine graph vs PyTorch 1.4e-6, argmax 3/3
|
|
584
|
+
against the fp16 backbone). Both keep the trained 73,728-token window, so pass
|
|
585
|
+
`maxLen` to cap a browser's read. The fused export repo (`warsang/vega-08b-web`,
|
|
586
|
+
~1.7 GB) is uploaded by `export/vega/05_upload.py`; the revision lands in
|
|
587
|
+
`VEGA_DEFAULT_REVISION`.
|
|
588
|
+
|
|
589
|
+
Lower-level helpers are exported for custom runtimes:
|
|
590
|
+
`buildVegaPrompt`, `packVegaBatch`, `packVegaChunks`, `vegaQuestionDef`,
|
|
591
|
+
`vegaOptionTexts`, `vegaThermalFeatures`, `vegaThermalProbabilities`,
|
|
592
|
+
`vegaAnswers`, `vegaOptionLabels`, `vegaJsonDumps`, `vegaRoute`,
|
|
593
|
+
`vegaTextPositions`, `emptyVegaPast`, `poolVegaHidden`.
|
|
594
|
+
|
|
595
|
+
## clef family (Qwen3 torso + joint schema head)
|
|
596
|
+
|
|
597
|
+
`createClefDecider(options)` runs [clef-text-0.6b](https://huggingface.co/FluidInference/clef-text-0.6b)
|
|
598
|
+
(Apache-2.0): a Qwen3-0.6B torso (LoRA merged) with Cloudflare's **joint schema
|
|
599
|
+
head** — the whole record (state plus every question's schema) becomes one
|
|
600
|
+
prompt, and the head decides every field in one joint pass. It is distilled
|
|
601
|
+
from Cloudflare's clef-flash 9B (79.1 vs 88.2 mean across 12 held-out
|
|
602
|
+
benchmarks) and ships Cloudflare's unchanged reference module
|
|
603
|
+
(`joint_schema_model.py`).
|
|
604
|
+
|
|
605
|
+
```js
|
|
606
|
+
import { createClefDecider } from "jev-web";
|
|
607
|
+
|
|
608
|
+
const decider = await createClefDecider({
|
|
609
|
+
// defaults to warsang/clef-text-0.6b-web @ pinned revision
|
|
610
|
+
onProgress: (p) => console.log(p.phase, p.loaded, p.total),
|
|
611
|
+
});
|
|
612
|
+
const { answers } = await decider.decide(stateText, questions);
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
Protocol implemented here (mirrors `encode_record` / `ClefModel`):
|
|
616
|
+
|
|
617
|
+
- record `<|im_start|>system\n…<|im_end|>\n<|im_start|>user\nSTATE:\n<state>`
|
|
618
|
+
+ `\n\nSCHEMA FIELDS:\n` + one block per question (`\nFIELD n\nID: …\nTYPE:
|
|
619
|
+
…\nINSTRUCTION: …\nALLOWED OPTIONS:\nOPTION n:
|
|
620
|
+
{"description":…,"option_id":…}\n…END FIELD\n`) + `<|im_end|>\n<|im_start|>
|
|
621
|
+
assistant\n<think>\n\n</think>\n\nJOINT SCHEMA DECISIONS:` — every piece
|
|
622
|
+
tokenized separately, the state truncated head-first to the window;
|
|
623
|
+
- `choice` option keys are **sorted** like the reference's
|
|
624
|
+
`sorted(criteria.items())` (probabilities still come back keyed by your
|
|
625
|
+
labels); `noul` maps to the fixed `true`/`false` slots (default descriptions,
|
|
626
|
+
overridable under either `true/false` or `yes/no`); `score` levels map to
|
|
627
|
+
numeric keys `0..n-1` with a `legend`;
|
|
628
|
+
- a question may carry an `id` — it is rendered as the field ID (default
|
|
629
|
+
`"0"`, `"1"`, …);
|
|
630
|
+
- the head's evidence and field passes attend to the full hidden sequence, so
|
|
631
|
+
torso and head ship as **one fused ONNX graph**; span means (question spans,
|
|
632
|
+
option spans, the final token, and the tied-embedding lexical mean per
|
|
633
|
+
option) travel as pooling matrices — the Vega trick;
|
|
634
|
+
- choice/score confidence is the top probability (the reference SystemOne
|
|
635
|
+
measure), not an entropy score.
|
|
636
|
+
|
|
637
|
+
Lower-level helpers are exported for custom runtimes:
|
|
638
|
+
`encodeClefRecord`, `packClefBatch`, `clefQuestionOptions`, `clefAnswers`,
|
|
639
|
+
`renderClefContent`.
|
|
640
|
+
|
|
641
|
+
Caveat: this family ships the **text-only** clef — images and videos (the
|
|
642
|
+
multimodal clef checkpoints' processor path) are not part of the browser
|
|
643
|
+
export. The fused fp16 graph needs an onnxruntime in the 1.31 generation
|
|
644
|
+
(transformers.js 4.3 ships ORT-web 1.31 in the browser); hosts pinned to an
|
|
645
|
+
older runtime can re-export the exact fp32 graph (2.7 GB) with
|
|
646
|
+
`export/clef/01_export.py` and pass it as `file`.
|
|
647
|
+
|
|
648
|
+
## Decision 3.0 family (d3 code readout)
|
|
649
|
+
|
|
650
|
+
`createDecision3Decider(options)` runs the
|
|
651
|
+
[vLLM Semantic Router Decision 3.0](https://huggingface.co/vllm-sr/d3-edge)
|
|
652
|
+
collection (Apache-2.0): Decision 2.0's candidate head is replaced by a **code
|
|
653
|
+
readout** — every option is printed under a single-token answer code (A…Z,
|
|
654
|
+
AA…AZ, up to 255), one question is decided per forward pass, and a 255-way
|
|
655
|
+
readout scores those codes at the last prompt position. d3-edge (0.59B,
|
|
656
|
+
multimodal) scores **28.82** on the Jev Decision Index 0.3 public suite (+12
|
|
657
|
+
over Decision 2.0's 0.6B).
|
|
658
|
+
|
|
659
|
+
```js
|
|
660
|
+
import { createDecision3Decider } from "jev-web";
|
|
661
|
+
|
|
662
|
+
const decider = await createDecision3Decider({
|
|
663
|
+
// defaults to warsang/d3-edge-web @ pinned revision
|
|
664
|
+
onProgress: (p) => console.log(p.phase, p.loaded, p.total),
|
|
665
|
+
});
|
|
666
|
+
// or pick a larger sibling:
|
|
667
|
+
// await createDecision3Decider({ variant: "lite" }); // 0.85B
|
|
668
|
+
// await createDecision3Decider({ variant: "nano" }); // 2.21B
|
|
669
|
+
const { answers } = await decider.decide(stateText, questions);
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
Protocol implemented here (mirrors `d3_format.py` / `d3_runtime.py`):
|
|
673
|
+
|
|
674
|
+
- user prompt `State:\n<state>\n\nQuestion:\n<instructions>\n\nOptions:\nA:
|
|
675
|
+
<option>\nB: …\n\nReply with only the code of the best option.` wrapped in
|
|
676
|
+
the checkpoint's Qwen3.5 chat template with `add_generation_prompt` and
|
|
677
|
+
`enable_thinking=false` (the built-in rendering is byte-identical to the
|
|
678
|
+
template and kicks in when the runtime has no `apply_chat_template`);
|
|
679
|
+
- the answer-code table comes from the checkpoint's `decision_config.json`
|
|
680
|
+
(falling back to the shipped A…Z / AA…ZZ list); questions run `batchSize`
|
|
681
|
+
(8) per forward pass, left-padded like the reference;
|
|
682
|
+
- `choice` keys are the option labels with optional descriptions; `noul` maps
|
|
683
|
+
to the fixed `false`/`true` slots with overridable descriptions; `score`
|
|
684
|
+
takes 2–10 distinct levels, renders as bare option texts, and answers with
|
|
685
|
+
the expected level index, the reference entropy `confidence` and a `legend`;
|
|
686
|
+
- two ONNX graphs ship: the torso (Qwen3.5 backbone with the reference
|
|
687
|
+
**non-causal full attention** — softmax layers see the whole prompt while the
|
|
688
|
+
GatedDeltaNet layers stay causal and padding-safe) emits the last prompt
|
|
689
|
+
token's hidden state, and a 255×d readout graph emits masked code logits
|
|
690
|
+
(fp32); the JS softmaxes them with the checkpoint temperature;
|
|
691
|
+
- a prompt over `maxLen` raises a `RangeError` — like the reference, nothing
|
|
692
|
+
is truncated (`truncated` is always `false`).
|
|
693
|
+
|
|
694
|
+
Caveats: the browser export is **text-only in v1** — the checkpoints'
|
|
695
|
+
image/video inputs run through a separate vision tower that the two shipped
|
|
696
|
+
graphs do not include; the exported window is capped by the browser budget
|
|
697
|
+
(`maxLen`, 8192 default) rather than the checkpoint's uncapped `max_length`;
|
|
698
|
+
and the fp16 torso needs an onnxruntime in the 1.31 generation (the same
|
|
699
|
+
requirement as clef — transformers.js 4.3 ships ORT-web 1.31 in the browser).
|
|
700
|
+
|
|
701
|
+
Lower-level helpers are exported for custom runtimes:
|
|
702
|
+
`buildDecision3Prompt`, `decision3Messages`, `renderDecision3Chat`,
|
|
703
|
+
`normalizeDecision3Question`, `decision3OptionTexts`,
|
|
704
|
+
`decision3ProbsFromLogits`, `decision3Confidence`, `decision3Answers`,
|
|
705
|
+
`packDecision3Batch`.
|
|
706
|
+
|
|
707
|
+
## Worker / thread
|
|
708
|
+
|
|
709
|
+
`jev-web` does not bundle a worker, so hosts can create one the way their
|
|
710
|
+
bundler prefers:
|
|
711
|
+
|
|
712
|
+
```js
|
|
713
|
+
// host code
|
|
714
|
+
const worker = new Worker(new URL("./my-worker.mjs", import.meta.url), { type: "module" });
|
|
715
|
+
// my-worker.mjs
|
|
716
|
+
import { createDecider } from "jev-web";
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
## API
|
|
720
|
+
|
|
721
|
+
- `createDecider(options)` → open-jev family: `{ info, decide, decideMany, dispose }`
|
|
722
|
+
- `createLayaDecider(options)` → Laya family: `{ info, decide, dispose }` (same `decide` contract)
|
|
723
|
+
- `createStrandsDecider(options)` → Strands Decider family: `{ info, decide, dispose }` (same `decide` contract)
|
|
724
|
+
- `createBekkoDecider(options)`, `createDecision2Decider(options)`, `createKevDecider(options)`,
|
|
725
|
+
`createVegaDecider(options)`, `createClefDecider(options)`, `createDecision3Decider(options)` → the same `decide` contract
|
|
726
|
+
- `normalizeQuestions`, `buildDecisionInput`, `typedMarkerIds`, `answersFromScores`,
|
|
727
|
+
`softmaxWithTemperature`, `resolveDevice`, `resolveDtype`, `detectWebGPU`,
|
|
728
|
+
`loadModelConfig`, defaults (model/revision/temperature/caps).
|
|
729
|
+
|
|
730
|
+
## Demos
|
|
731
|
+
|
|
732
|
+
- **Live demo** — <https://warsang.github.io/jev-web/> (built from `demo/`, deployed on every push)
|
|
733
|
+
- **Local playground** — `npm run dev` → <http://localhost:5188>. Same ideas, plus
|
|
734
|
+
COOP/COEP headers that unlock `SharedArrayBuffer` for the threaded
|
|
735
|
+
onnxruntime-web build (GitHub Pages cannot set those headers, so the deployed
|
|
736
|
+
demo runs single-threaded).
|
|
737
|
+
- **Verify a change visually** — `node tools/shot.mjs jobs.json` drives headless
|
|
738
|
+
Chrome over CDP, waits on a real DOM condition, and reports the page's own
|
|
739
|
+
console errors. The GIF in this README was built with it. It exists because a
|
|
740
|
+
screenshot alone cannot tell you a promise is silently rejecting.
|
|
741
|
+
|
|
742
|
+
## Limits
|
|
743
|
+
|
|
744
|
+
- Typed-decision models answer **only** by picking among your options; they do
|
|
745
|
+
not explain or generate text.
|
|
746
|
+
- Accuracy is domain-bound: questions and states unlike the model's training
|
|
747
|
+
domain are out of distribution. Measure before relying on any answer.
|
|
748
|
+
- English, 512-token sequence (state capped), one state per forward pass.
|
|
749
|
+
- **WebGPU support is uneven.** A `navigator.gpu` adapter existing is not
|
|
750
|
+
evidence that inference will run: onnxruntime-web compiles WGSL per subgraph,
|
|
751
|
+
and Firefox currently rejects the reference DeBERTa-v3 export's `Clip`
|
|
752
|
+
subgraph (`Failed to create a WebGPU compute pipeline: ShaderModule with
|
|
753
|
+
'Clip' label is invalid`, surfaced as `failed to call OrtRun()`). The live
|
|
754
|
+
demo detects that signature and rebuilds on WASM, which works but is much
|
|
755
|
+
slower. If you hit it in your own app, fall back explicitly rather than
|
|
756
|
+
trusting adapter detection:
|
|
757
|
+
|
|
758
|
+
```js
|
|
759
|
+
let decider;
|
|
760
|
+
try {
|
|
761
|
+
decider = await createDecider({ device: "webgpu" });
|
|
762
|
+
await decider.decide("warmup", [{ type: "noul", instructions: "Ready?" }]);
|
|
763
|
+
} catch {
|
|
764
|
+
decider = await createDecider({ device: "wasm", dtype: "q4" });
|
|
765
|
+
}
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
That warm-up call is the point: probe with **inference**, not with
|
|
769
|
+
`requestAdapter()`.
|
|
770
|
+
|
|
771
|
+
## License
|
|
772
|
+
|
|
773
|
+
MIT (runtime code). Model weights keep their own license (the reference export
|
|
503
774
|
is Apache-2.0).
|