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 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), and Decision 2.0 (Qwen3 +
16
- candidate head) — behind one registry: `createDecisionRuntime({ family, fallback })`.
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 exported model/revision constants:
362
- `BEKKO_68M_MODEL` / `BEKKO_68M_REVISION` and `BEKKO_400M_MODEL` /
363
- `BEKKO_400M_REVISION` (68M and 400M, same `onnx_browser/` layout).
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.6.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
- const tok = await tf.AutoTokenizer.from_pretrained(tokenizerModel ?? model, {
293
- revision: tokenizerRevision ?? (tokenizerModel ? undefined : revision),
294
- progress_callback: report("tokenizer"),
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
+ }