jev-web 0.6.0 → 0.7.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/README.md +53 -7
- package/package.json +1 -1
- package/src/bekko.mjs +54 -5
- package/src/index.mjs +12 -1
- package/src/kev.mjs +297 -0
- package/src/registry.mjs +14 -1
package/README.md
CHANGED
|
@@ -12,8 +12,9 @@ cached.
|
|
|
12
12
|
|
|
13
13
|
Three model families ship today — open-jev (fused DeBERTa graph), Laya
|
|
14
14
|
(ModernBERT encoder + typed head), Strands Decider 2B (Qwen torso + pointer
|
|
15
|
-
head), Bekko System One v0 (Ettin cross-encoder),
|
|
16
|
-
candidate head) — behind one registry:
|
|
15
|
+
head), Bekko System One v0 (Ettin cross-encoder), Decision 2.0 (Qwen3 +
|
|
16
|
+
candidate head), and Kev (Qwen3 + pointer head) — behind one registry:
|
|
17
|
+
`createDecisionRuntime({ family, fallback })`.
|
|
17
18
|
|
|
18
19
|
This is a generic runtime, not a classifier for any particular domain. You bring
|
|
19
20
|
the state text and the questions; the model answers by choosing among the options
|
|
@@ -225,7 +226,7 @@ tries the requested family and then any fallbacks, returning the shared contract
|
|
|
225
226
|
```js
|
|
226
227
|
import { createDecisionRuntime, listDecisionFamilies } from "jev-web";
|
|
227
228
|
|
|
228
|
-
listDecisionFamilies(); // ["open-jev", "laya", "strands-decider", "bekko", "decision2"]
|
|
229
|
+
listDecisionFamilies(); // ["open-jev", "laya", "strands-decider", "bekko", "decision2", "kev"]
|
|
229
230
|
const runtime = await createDecisionRuntime({
|
|
230
231
|
family: "strands-decider",
|
|
231
232
|
fallback: ["laya", "open-jev"],
|
|
@@ -345,6 +346,9 @@ const decider = await createBekkoDecider({
|
|
|
345
346
|
// onnx_browser/ (29 MB)
|
|
346
347
|
onProgress: (p) => console.log(p.phase, p.loaded, p.total),
|
|
347
348
|
});
|
|
349
|
+
// or pick a larger sibling:
|
|
350
|
+
// await createBekkoDecider({ variant: "68m" }); // 196 MB
|
|
351
|
+
// await createBekkoDecider({ variant: "400m" }); // 1.43 GB, #2 on the S1MB leaderboard
|
|
348
352
|
const { answers } = await decider.decide(stateText, questions);
|
|
349
353
|
```
|
|
350
354
|
|
|
@@ -358,9 +362,11 @@ Protocol implemented here (mirrors the reference browser runtime,
|
|
|
358
362
|
- score levels map to numeric candidate values `0..n-1`; the answer is the
|
|
359
363
|
expected value plus the argmax level and a normalised score.
|
|
360
364
|
|
|
361
|
-
Larger siblings are available via the
|
|
362
|
-
`
|
|
363
|
-
`
|
|
365
|
+
Larger siblings are available via the `variant` option (`"17m"`, `"68m"`,
|
|
366
|
+
`"400m"`) or the exported model/revision constants: `BEKKO_68M_MODEL` /
|
|
367
|
+
`BEKKO_68M_REVISION` and `BEKKO_400M_MODEL` / `BEKKO_400M_REVISION` (same
|
|
368
|
+
`onnx_browser/` layout). The `BEKKO_VARIANTS` map carries each size's pinned
|
|
369
|
+
revision and download size.
|
|
364
370
|
|
|
365
371
|
Lower-level helpers are exported for custom runtimes:
|
|
366
372
|
`renderBekkoContent`, `renderBekkoRequest`, `tokenizeBekkoRequest`,
|
|
@@ -410,6 +416,46 @@ Lower-level helpers are exported for custom runtimes:
|
|
|
410
416
|
`renderDecision2Content`, `renderDecision2Options`, `buildDecision2Segments`,
|
|
411
417
|
`tokenizeDecision2Segments`, `decision2Softmax`, `decision2AnswerFromLogits`.
|
|
412
418
|
|
|
419
|
+
## Kev family (Qwen3 + pointer head)
|
|
420
|
+
|
|
421
|
+
`createKevDecider(options)` runs
|
|
422
|
+
[Kev](https://huggingface.co/jaredpalmer/kev-0.6b) (Apache-2.0), a typed
|
|
423
|
+
decision model: one state and any number of typed questions are packed into a
|
|
424
|
+
single sequence with five delimiter tokens; a block-causal mask derived
|
|
425
|
+
in-graph lets every question see the state and itself only, and a pointer head
|
|
426
|
+
scores each option's `</opt>` token against its question's `<decide>` token.
|
|
427
|
+
One ONNX graph (`input_ids, attention_mask → logits [B, seq]`), one forward
|
|
428
|
+
pass for the whole batch — read the logit at each option's `</opt>` position
|
|
429
|
+
and softmax within the question:
|
|
430
|
+
|
|
431
|
+
```js
|
|
432
|
+
import { createKevDecider } from "jev-web";
|
|
433
|
+
|
|
434
|
+
const decider = await createKevDecider({
|
|
435
|
+
// defaults to onnx-community/kev-0.6b-ONNX @ pinned revision,
|
|
436
|
+
// onnx/model_q4f16.onnx (335 MB)
|
|
437
|
+
onProgress: (p) => console.log(p.phase, p.loaded, p.total),
|
|
438
|
+
});
|
|
439
|
+
// or pick the larger sibling:
|
|
440
|
+
// await createKevDecider({ variant: "4b" }); // 1.07 GB, #29 on the Decision Index
|
|
441
|
+
const { answers } = await decider.decide(stateText, questions);
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Protocol implemented here (mirrors the reference JS in the model README):
|
|
445
|
+
|
|
446
|
+
- delimiters `<|fim_prefix|>` (state), `<|fim_middle|>` (question),
|
|
447
|
+
`<|box_start|>` / `<|box_end|>` (option), `<|fim_suffix|>` (decide); ids
|
|
448
|
+
derived through the tokenizer, limits from `config.json`'s `kev` block
|
|
449
|
+
(8192 state / 8192 branch tokens, 255 options);
|
|
450
|
+
- caller text is escaped (`<|name|>` → `<¦name¦>`) so it can never forge a
|
|
451
|
+
delimiter;
|
|
452
|
+
- choice questions accept `criteria` as `{label: description}`, rendered as
|
|
453
|
+
`label: description`; noul defaults to `no` / `yes`.
|
|
454
|
+
|
|
455
|
+
The `KEV_VARIANTS` map (`"0.6b"`, `"4b"`) carries each size's pinned revision
|
|
456
|
+
and download size. Lower-level helpers are exported for custom runtimes:
|
|
457
|
+
`escapeKevText`, `packKevSequence`.
|
|
458
|
+
|
|
413
459
|
## Worker / thread
|
|
414
460
|
|
|
415
461
|
`jev-web` does not bundle a worker, so hosts can create one the way their
|
|
@@ -454,4 +500,4 @@ import { createDecider } from "jev-web";
|
|
|
454
500
|
## License
|
|
455
501
|
|
|
456
502
|
MIT (runtime code). Model weights keep their own license (the reference export
|
|
457
|
-
is Apache-2.0).
|
|
503
|
+
is Apache-2.0).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jev-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Run open-jev-, Laya- and Strands-Decider-shaped typed-decision models in the browser: one state plus typed questions in, calibrated probabilities out.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/bekko.mjs
CHANGED
|
@@ -28,6 +28,28 @@ export const BEKKO_68M_REVISION = "ab7685f23e5edbc1acb12ced4f2c4e12591efa69";
|
|
|
28
28
|
export const BEKKO_400M_MODEL = "hotchpotch/bekko-system-one-v0-400m";
|
|
29
29
|
export const BEKKO_400M_REVISION = "1960df5602bd93cc8d336fdebd9fb68a30926e13";
|
|
30
30
|
|
|
31
|
+
/** Named size variants with their pinned revisions and download sizes. */
|
|
32
|
+
export const BEKKO_VARIANTS = {
|
|
33
|
+
"17m": {
|
|
34
|
+
model: BEKKO_DEFAULT_MODEL,
|
|
35
|
+
revision: BEKKO_DEFAULT_REVISION,
|
|
36
|
+
bytes: 29_000_000,
|
|
37
|
+
description: "17m, 29 MB browser ONNX",
|
|
38
|
+
},
|
|
39
|
+
"68m": {
|
|
40
|
+
model: BEKKO_68M_MODEL,
|
|
41
|
+
revision: BEKKO_68M_REVISION,
|
|
42
|
+
bytes: 196_342_909,
|
|
43
|
+
description: "68m, 196 MB browser ONNX",
|
|
44
|
+
},
|
|
45
|
+
"400m": {
|
|
46
|
+
model: BEKKO_400M_MODEL,
|
|
47
|
+
revision: BEKKO_400M_REVISION,
|
|
48
|
+
bytes: 1_426_199_724,
|
|
49
|
+
description: "400m, 1.43 GB browser ONNX",
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
31
53
|
// Candidate batching heuristic from the reference runtime: bound the
|
|
32
54
|
// candidate-attention growth per forward; not a total RAM limit.
|
|
33
55
|
export const BEKKO_ATTENTION_BUDGET = 1_048_576;
|
|
@@ -252,6 +274,7 @@ const hfUrl = (model, revision, ...parts) =>
|
|
|
252
274
|
export async function createBekkoDecider({
|
|
253
275
|
model = BEKKO_DEFAULT_MODEL,
|
|
254
276
|
revision = BEKKO_DEFAULT_REVISION,
|
|
277
|
+
variant = null,
|
|
255
278
|
subdir = BEKKO_DEFAULT_SUBDIR,
|
|
256
279
|
manifestUrl = null,
|
|
257
280
|
modelUrl = null,
|
|
@@ -268,6 +291,13 @@ export async function createBekkoDecider({
|
|
|
268
291
|
attentionBudget = BEKKO_ATTENTION_BUDGET,
|
|
269
292
|
scope = globalThis,
|
|
270
293
|
} = {}) {
|
|
294
|
+
// Named size shortcut: {variant: "400m"} sets model/revision.
|
|
295
|
+
if (variant) {
|
|
296
|
+
const v = BEKKO_VARIANTS[variant];
|
|
297
|
+
if (!v) throw new TypeError(`bekko: unknown variant "${variant}" (17m, 68m, 400m)`);
|
|
298
|
+
model = v.model;
|
|
299
|
+
revision = v.revision;
|
|
300
|
+
}
|
|
271
301
|
const ortMod = ort ?? (await import("onnxruntime-web"));
|
|
272
302
|
const tf = transformers ?? (await import("@huggingface/transformers"));
|
|
273
303
|
if (wasmPaths) {
|
|
@@ -289,10 +319,29 @@ export async function createBekkoDecider({
|
|
|
289
319
|
const manifest = await manifestRes.json();
|
|
290
320
|
const taskColumns = manifest.tasks;
|
|
291
321
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
322
|
+
/**
|
|
323
|
+
* Load the tokenizer. The bekko repos keep tokenizer.json under
|
|
324
|
+
* onnx_browser/, not the repo root, so AutoTokenizer.from_pretrained(repo)
|
|
325
|
+
* finds nothing there. Load the subdir's tokenizer.json directly and
|
|
326
|
+
* construct the base tokenizer (special tokens come from the manifest).
|
|
327
|
+
* An explicit tokenizerModel still goes through from_pretrained.
|
|
328
|
+
*/
|
|
329
|
+
async function loadBekkoTokenizer() {
|
|
330
|
+
if (tokenizerModel) {
|
|
331
|
+
return tf.AutoTokenizer.from_pretrained(tokenizerModel, {
|
|
332
|
+
revision: tokenizerRevision,
|
|
333
|
+
progress_callback: report("tokenizer"),
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
const tokRes = await fetchImpl(hfUrl(model, revision, subdir, "tokenizer.json"));
|
|
337
|
+
if (!tokRes.ok) {
|
|
338
|
+
throw new Error(`bekko: tokenizer fetch failed (${tokRes.status})`);
|
|
339
|
+
}
|
|
340
|
+
report("tokenizer")({ file: "tokenizer.json" });
|
|
341
|
+
return new tf.PreTrainedTokenizer(await tokRes.json(), {});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const tok = await loadBekkoTokenizer();
|
|
296
345
|
const encode = (text) =>
|
|
297
346
|
Array.from(tok(text, { add_special_tokens: false }).input_ids.data, Number);
|
|
298
347
|
|
|
@@ -388,4 +437,4 @@ export async function createBekkoDecider({
|
|
|
388
437
|
try { await session.release?.(); } catch { /* optional */ }
|
|
389
438
|
},
|
|
390
439
|
};
|
|
391
|
-
}
|
|
440
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -64,6 +64,7 @@ export {
|
|
|
64
64
|
BEKKO_68M_REVISION,
|
|
65
65
|
BEKKO_400M_MODEL,
|
|
66
66
|
BEKKO_400M_REVISION,
|
|
67
|
+
BEKKO_VARIANTS,
|
|
67
68
|
BEKKO_ATTENTION_BUDGET,
|
|
68
69
|
BEKKO_DEFAULT_YES,
|
|
69
70
|
BEKKO_DEFAULT_NO,
|
|
@@ -88,6 +89,16 @@ export {
|
|
|
88
89
|
DECISION2_MAX_INPUT_TOKENS,
|
|
89
90
|
DECISION2_NOUL_DESCRIPTIONS,
|
|
90
91
|
} from "./decision2.mjs";
|
|
92
|
+
export {
|
|
93
|
+
createKevDecider,
|
|
94
|
+
escapeKevText,
|
|
95
|
+
packKevSequence,
|
|
96
|
+
KEV_DEFAULT_MODEL,
|
|
97
|
+
KEV_DEFAULT_REVISION,
|
|
98
|
+
KEV_4B_MODEL,
|
|
99
|
+
KEV_4B_REVISION,
|
|
100
|
+
KEV_VARIANTS,
|
|
101
|
+
} from "./kev.mjs";
|
|
91
102
|
export {
|
|
92
103
|
DEFAULT_MODEL,
|
|
93
104
|
DEFAULT_REVISION,
|
|
@@ -99,4 +110,4 @@ export {
|
|
|
99
110
|
resolveDtype,
|
|
100
111
|
detectWebGPU,
|
|
101
112
|
loadModelConfig,
|
|
102
|
-
} from "./config.mjs";
|
|
113
|
+
} from "./config.mjs";
|
package/src/kev.mjs
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kev.mjs — Kev decision-model family (jaredpalmer/kev, Apache-2.0).
|
|
3
|
+
*
|
|
4
|
+
* Kev is a typed decision model: one document (the *state*) and any number of
|
|
5
|
+
* typed questions are packed into a single sequence with five delimiter
|
|
6
|
+
* tokens. A block-causal mask (derived in-graph from the delimiter ids) lets
|
|
7
|
+
* every question see the state and itself only; a pointer head scores each
|
|
8
|
+
* option's `</opt>` token against its question's `<decide>` token.
|
|
9
|
+
*
|
|
10
|
+
* graph: input_ids, attention_mask (right padded) -> logits [B, seq]
|
|
11
|
+
* decode: read the logit at every option's `</opt>` position,
|
|
12
|
+
* softmax within each question.
|
|
13
|
+
*
|
|
14
|
+
* Reference: onnx-community/kev-0.6b-ONNX README (ships a JS implementation
|
|
15
|
+
* this mirrors) and config.json `kev` block (delimiters, ids, limits).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { normalizeQuestions } from "./questions.mjs";
|
|
19
|
+
import { answersFromScores } from "./answers.mjs";
|
|
20
|
+
import { fetchCachedBytes, fetchOnnxExternalData } from "./laya.mjs";
|
|
21
|
+
import { resolveDtype } from "./config.mjs";
|
|
22
|
+
|
|
23
|
+
const HF_BASE = "https://huggingface.co";
|
|
24
|
+
const hfUrl = (model, revision, ...parts) =>
|
|
25
|
+
[HF_BASE, model, "resolve", revision, ...parts.filter(Boolean)].join("/");
|
|
26
|
+
|
|
27
|
+
export const KEV_DEFAULT_MODEL = "onnx-community/kev-0.6b-ONNX";
|
|
28
|
+
export const KEV_DEFAULT_REVISION = "5a827091b3d58fe4fdfc02a5a5b2ce364047d678";
|
|
29
|
+
export const KEV_4B_MODEL = "onnx-community/kev-4b-ONNX";
|
|
30
|
+
export const KEV_4B_REVISION = "195bdf73096df2bca7150223c9279af7893163c";
|
|
31
|
+
|
|
32
|
+
/** Named size variants with their pinned revisions and download sizes. */
|
|
33
|
+
export const KEV_VARIANTS = {
|
|
34
|
+
"0.6b": {
|
|
35
|
+
model: KEV_DEFAULT_MODEL,
|
|
36
|
+
revision: KEV_DEFAULT_REVISION,
|
|
37
|
+
bytes: 335_000_000,
|
|
38
|
+
description: "kev-0.6b, 335 MB q4f16 browser ONNX",
|
|
39
|
+
},
|
|
40
|
+
"4b": {
|
|
41
|
+
model: KEV_4B_MODEL,
|
|
42
|
+
revision: KEV_4B_REVISION,
|
|
43
|
+
bytes: 1_070_000_000,
|
|
44
|
+
description: "kev-4b, 1.07 GB q4 browser ONNX",
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Fallback when config.json has no `kev` block (both shipped repos have one).
|
|
49
|
+
const KEV_FALLBACK = {
|
|
50
|
+
delimiters: {
|
|
51
|
+
state: "<|fim_prefix|>",
|
|
52
|
+
question: "<|fim_middle|>",
|
|
53
|
+
option_start: "<|box_start|>",
|
|
54
|
+
option_end: "<|box_end|>",
|
|
55
|
+
decide: "<|fim_suffix|>",
|
|
56
|
+
},
|
|
57
|
+
max_state_tokens: 8192,
|
|
58
|
+
max_branch_tokens: 8192,
|
|
59
|
+
max_options: 255,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Caller text can never produce a delimiter: <|name|> -> <¦name¦>. */
|
|
63
|
+
export function escapeKevText(text) {
|
|
64
|
+
return String(text).replace(/<\|([A-Za-z0-9_]+)\|>/g, "<¦$1¦>");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderKevContent(content) {
|
|
68
|
+
if (typeof content === "string") return content.trim();
|
|
69
|
+
return JSON.stringify(content);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Render one option; choice options pick up `criteria` descriptions. */
|
|
73
|
+
function renderKevOption(question, label) {
|
|
74
|
+
if (question.type === "choice") {
|
|
75
|
+
const desc = question.criteria?.[label];
|
|
76
|
+
if (desc != null && String(desc).trim() && String(desc).trim() !== label) {
|
|
77
|
+
return `${label}: ${String(desc).trim()}`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return label;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Pack the state and normalized questions into one kev token sequence.
|
|
85
|
+
* Returns { tokens, groups } where groups[i] holds the sequence indices of
|
|
86
|
+
* question i's option `</opt>` tokens.
|
|
87
|
+
*/
|
|
88
|
+
export function packKevSequence({ stateText, questions, encode, delims, limits }) {
|
|
89
|
+
const { state: STATE, question: Q, option_start: OPT, option_end: END, decide: DECIDE } = delims;
|
|
90
|
+
const stateIds = encode(stateText).slice(0, limits.max_state_tokens);
|
|
91
|
+
const tokens = [STATE, ...stateIds];
|
|
92
|
+
const groups = [];
|
|
93
|
+
let truncated = false;
|
|
94
|
+
|
|
95
|
+
for (const q of questions) {
|
|
96
|
+
const instrIds = encode(q.instructions);
|
|
97
|
+
const spans = q.options.map((label) => [OPT, ...encode(renderKevOption(q, label)), END]);
|
|
98
|
+
const branch = [Q, ...instrIds, ...spans.flat(), DECIDE];
|
|
99
|
+
if (branch.length > limits.max_branch_tokens) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`kev: question branch too long (${branch.length} > ${limits.max_branch_tokens} tokens)`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (tokens.length + branch.length > limits.max_state_tokens) {
|
|
105
|
+
truncated = true;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
const base = tokens.length;
|
|
109
|
+
let cursor = 1 + instrIds.length;
|
|
110
|
+
const ends = spans.map((s) => {
|
|
111
|
+
cursor += s.length;
|
|
112
|
+
return base + cursor - 1; // index of this option's </opt>
|
|
113
|
+
});
|
|
114
|
+
tokens.push(...branch);
|
|
115
|
+
groups.push(ends);
|
|
116
|
+
}
|
|
117
|
+
return { tokens, groups, truncated };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Create a Kev decider.
|
|
122
|
+
*
|
|
123
|
+
* @param {object} [opts]
|
|
124
|
+
* model/revision — pinned ONNX export (or your own mirror)
|
|
125
|
+
* variant — "0.6b" | "4b" shortcut for model/revision
|
|
126
|
+
* modelFile — override the resolved onnx file (default picks q4f16/q4 by device)
|
|
127
|
+
* configUrl/modelUrl — override the resolved file URLs
|
|
128
|
+
* ort — injected onnxruntime-web (lazy import otherwise)
|
|
129
|
+
* transformers — injected @huggingface/transformers (tokenizer only)
|
|
130
|
+
* fetchImpl — fetch implementation
|
|
131
|
+
* wasmPaths — ORT wasm/loader directory (self-hosted builds)
|
|
132
|
+
* device — "auto" | "wasm" | "webgpu"
|
|
133
|
+
* dtype — "auto" | "q4" | "q4f16"
|
|
134
|
+
* sessionFactory — (url, sessionOptions) => Promise<InferenceSession>
|
|
135
|
+
* onProgress — progress callback
|
|
136
|
+
*/
|
|
137
|
+
export async function createKevDecider({
|
|
138
|
+
model = KEV_DEFAULT_MODEL,
|
|
139
|
+
revision = KEV_DEFAULT_REVISION,
|
|
140
|
+
variant = null,
|
|
141
|
+
modelFile = null,
|
|
142
|
+
configUrl = null,
|
|
143
|
+
modelUrl = null,
|
|
144
|
+
ort = null,
|
|
145
|
+
transformers = null,
|
|
146
|
+
fetchImpl = fetch,
|
|
147
|
+
wasmPaths = null,
|
|
148
|
+
device = "auto",
|
|
149
|
+
dtype = "auto",
|
|
150
|
+
sessionFactory = null,
|
|
151
|
+
onProgress = null,
|
|
152
|
+
scope = globalThis,
|
|
153
|
+
} = {}) {
|
|
154
|
+
if (variant) {
|
|
155
|
+
const v = KEV_VARIANTS[variant];
|
|
156
|
+
if (!v) throw new TypeError(`kev: unknown variant "${variant}" (0.6b, 4b)`);
|
|
157
|
+
model = v.model;
|
|
158
|
+
revision = v.revision;
|
|
159
|
+
}
|
|
160
|
+
const ortMod = ort ?? (await import("onnxruntime-web"));
|
|
161
|
+
const tf = transformers ?? (await import("@huggingface/transformers"));
|
|
162
|
+
if (wasmPaths) {
|
|
163
|
+
try { ortMod.env.wasm.wasmPaths = wasmPaths; } catch { /* older runtime */ }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const resolvedDevice = device !== "auto"
|
|
167
|
+
? device
|
|
168
|
+
: (scope?.navigator?.gpu ? "webgpu" : "wasm");
|
|
169
|
+
const resolvedDtype = resolveDtype(resolvedDevice, dtype);
|
|
170
|
+
const executionProviders = resolvedDevice === "webgpu" ? ["webgpu", "wasm"] : ["wasm"];
|
|
171
|
+
|
|
172
|
+
const report = (phase) => (p) =>
|
|
173
|
+
onProgress?.({ phase, file: p?.file ?? null, loaded: p?.loaded ?? null, total: p?.total ?? null });
|
|
174
|
+
|
|
175
|
+
// Model config carries the kev protocol block (delimiters, ids, limits).
|
|
176
|
+
const cfgRes = await fetchImpl(configUrl ?? hfUrl(model, revision, "", "config.json"));
|
|
177
|
+
if (!cfgRes.ok) throw new Error(`kev: config fetch failed (${cfgRes.status})`);
|
|
178
|
+
const kevCfg = (await cfgRes.json()).kev ?? {};
|
|
179
|
+
const delimiters = { ...KEV_FALLBACK.delimiters, ...(kevCfg.delimiters ?? {}) };
|
|
180
|
+
const limits = {
|
|
181
|
+
max_state_tokens: kevCfg.max_state_tokens ?? KEV_FALLBACK.max_state_tokens,
|
|
182
|
+
max_branch_tokens: kevCfg.max_branch_tokens ?? KEV_FALLBACK.max_branch_tokens,
|
|
183
|
+
max_options: kevCfg.max_options ?? KEV_FALLBACK.max_options,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const tok = await tf.AutoTokenizer.from_pretrained(model, {
|
|
187
|
+
revision,
|
|
188
|
+
progress_callback: report("tokenizer"),
|
|
189
|
+
});
|
|
190
|
+
const rawEncode = (text) =>
|
|
191
|
+
Array.from(tok(text, { add_special_tokens: false }).input_ids.data, Number);
|
|
192
|
+
// Delimiter ids are derived from the raw strings; caller text is escaped so
|
|
193
|
+
// it can never produce a delimiter.
|
|
194
|
+
const encode = (text) => rawEncode(escapeKevText(text));
|
|
195
|
+
// Derive delimiter ids through the tokenizer (robust to id changes).
|
|
196
|
+
const delims = Object.fromEntries(
|
|
197
|
+
Object.entries(delimiters).map(([k, t]) => {
|
|
198
|
+
const ids = rawEncode(t);
|
|
199
|
+
if (ids.length !== 1) throw new Error(`kev: delimiter ${t} is not a single token`);
|
|
200
|
+
return [k, ids[0]];
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const qfile = modelFile ?? (resolvedDtype === "q4f16" ? "model_q4f16.onnx" : "model_q4.onnx");
|
|
205
|
+
const url = modelUrl ?? hfUrl(model, revision, "onnx", qfile);
|
|
206
|
+
const createSession = sessionFactory
|
|
207
|
+
?? (async (u, opts) => {
|
|
208
|
+
const modelBytes = await fetchCachedBytes(u, {
|
|
209
|
+
fetchImpl, onProgress: (p) => onProgress?.(p), persist: true,
|
|
210
|
+
});
|
|
211
|
+
const external = await fetchOnnxExternalData(u, {
|
|
212
|
+
fetchImpl, onProgress: (p) => onProgress?.(p),
|
|
213
|
+
});
|
|
214
|
+
const sessionOpts = external ? { ...opts, externalData: [external] } : opts;
|
|
215
|
+
return ortMod.InferenceSession.create(modelBytes, sessionOpts);
|
|
216
|
+
});
|
|
217
|
+
let session;
|
|
218
|
+
let activeDevice = resolvedDevice;
|
|
219
|
+
try {
|
|
220
|
+
session = await createSession(url, {
|
|
221
|
+
executionProviders,
|
|
222
|
+
graphOptimizationLevel: "all",
|
|
223
|
+
});
|
|
224
|
+
} catch (e) {
|
|
225
|
+
if (resolvedDevice === "webgpu") {
|
|
226
|
+
onProgress?.({ phase: `webgpu unavailable (${e?.message ?? e}) — retrying on wasm` });
|
|
227
|
+
session = await createSession(url, { executionProviders: ["wasm"], graphOptimizationLevel: "all" });
|
|
228
|
+
activeDevice = "wasm";
|
|
229
|
+
} else {
|
|
230
|
+
throw e;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
info: {
|
|
236
|
+
family: "kev",
|
|
237
|
+
model,
|
|
238
|
+
revision,
|
|
239
|
+
device: activeDevice,
|
|
240
|
+
dtype: resolvedDtype,
|
|
241
|
+
tasks: ["choice", "noul", "score"],
|
|
242
|
+
limits,
|
|
243
|
+
},
|
|
244
|
+
async decide(state, questions) {
|
|
245
|
+
const t0 = Date.now();
|
|
246
|
+
const normalized = normalizeQuestions(questions);
|
|
247
|
+
for (const [i, q] of normalized.entries()) {
|
|
248
|
+
if (q.options.length > limits.max_options) {
|
|
249
|
+
throw new TypeError(`questions[${i}]: kev supports at most ${limits.max_options} options`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const stateText = renderKevContent(state);
|
|
253
|
+
const { tokens, groups, truncated } = packKevSequence({
|
|
254
|
+
stateText, questions: normalized, encode, delims, limits,
|
|
255
|
+
});
|
|
256
|
+
if (groups.length !== normalized.length) {
|
|
257
|
+
throw new Error("kev: packed fewer questions than requested");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const n = tokens.length;
|
|
261
|
+
const inputIds = new ortMod.Tensor("int64", BigInt64Array.from(tokens, BigInt), [1, n]);
|
|
262
|
+
const attentionMask = new ortMod.Tensor("int64", new BigInt64Array(n).fill(1n), [1, n]);
|
|
263
|
+
const tRun = Date.now();
|
|
264
|
+
const out = await session.run({ input_ids: inputIds, attention_mask: attentionMask });
|
|
265
|
+
const runMs = Date.now() - tRun;
|
|
266
|
+
const outLogits = out.logits ?? out[Object.keys(out)[0]];
|
|
267
|
+
if (outLogits.dims.length !== 2 || outLogits.dims[0] !== 1 || outLogits.dims[1] !== n) {
|
|
268
|
+
throw new Error("kev: invalid model logits shape");
|
|
269
|
+
}
|
|
270
|
+
const scores = Array.from(outLogits.data, Number);
|
|
271
|
+
// One flat score list; groups map each question to its option positions.
|
|
272
|
+
const flat = [];
|
|
273
|
+
const flatGroups = groups.map((ends) => ends.map((pos) => {
|
|
274
|
+
flat.push(scores[pos]);
|
|
275
|
+
return flat.length - 1;
|
|
276
|
+
}));
|
|
277
|
+
const answers = answersFromScores(flat, {
|
|
278
|
+
questions: normalized,
|
|
279
|
+
groups: flatGroups,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
answers,
|
|
284
|
+
truncated,
|
|
285
|
+
length: n,
|
|
286
|
+
prompts: normalized.map((q) => ({
|
|
287
|
+
instructions: q.instructions,
|
|
288
|
+
options: q.options,
|
|
289
|
+
})),
|
|
290
|
+
timings: { totalMs: Date.now() - t0, runMs },
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
async dispose() {
|
|
294
|
+
try { await session.release?.(); } catch { /* optional */ }
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
package/src/registry.mjs
CHANGED
|
@@ -44,6 +44,11 @@ import {
|
|
|
44
44
|
DECISION2_DEFAULT_MODEL,
|
|
45
45
|
DECISION2_DEFAULT_REVISION,
|
|
46
46
|
} from "./decision2.mjs";
|
|
47
|
+
import {
|
|
48
|
+
createKevDecider,
|
|
49
|
+
KEV_DEFAULT_MODEL,
|
|
50
|
+
KEV_DEFAULT_REVISION,
|
|
51
|
+
} from "./kev.mjs";
|
|
47
52
|
|
|
48
53
|
const families = new Map();
|
|
49
54
|
|
|
@@ -108,6 +113,14 @@ registerDecisionFamily("decision2", {
|
|
|
108
113
|
create: createDecision2Decider,
|
|
109
114
|
});
|
|
110
115
|
|
|
116
|
+
registerDecisionFamily("kev", {
|
|
117
|
+
defaults: {
|
|
118
|
+
model: KEV_DEFAULT_MODEL,
|
|
119
|
+
revision: KEV_DEFAULT_REVISION,
|
|
120
|
+
},
|
|
121
|
+
create: createKevDecider,
|
|
122
|
+
});
|
|
123
|
+
|
|
111
124
|
/**
|
|
112
125
|
* Create a decision runtime, preferring `family` and falling back in order.
|
|
113
126
|
* @param {{family?:string, fallback?:string[], onFallback?:Function}} [opts]
|
|
@@ -140,4 +153,4 @@ export async function createDecisionRuntime({
|
|
|
140
153
|
}
|
|
141
154
|
}
|
|
142
155
|
throw lastError ?? new Error("createDecisionRuntime: no family could be created");
|
|
143
|
-
}
|
|
156
|
+
}
|