jev-web 0.4.0 → 0.5.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 +42 -3
- package/package.json +1 -1
- package/src/decision2.mjs +351 -0
- package/src/index.mjs +18 -0
- package/src/registry.mjs +18 -4
package/README.md
CHANGED
|
@@ -12,8 +12,8 @@ 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),
|
|
16
|
-
`createDecisionRuntime({ family, fallback })`.
|
|
15
|
+
head), Bekko System One v0 (Ettin cross-encoder), and Decision 2.0 (Qwen3 +
|
|
16
|
+
candidate head) — behind one registry: `createDecisionRuntime({ family, fallback })`.
|
|
17
17
|
|
|
18
18
|
This is a generic runtime, not a classifier for any particular domain. You bring
|
|
19
19
|
the state text and the questions; the model answers by choosing among the options
|
|
@@ -225,7 +225,7 @@ tries the requested family and then any fallbacks, returning the shared contract
|
|
|
225
225
|
```js
|
|
226
226
|
import { createDecisionRuntime, listDecisionFamilies } from "jev-web";
|
|
227
227
|
|
|
228
|
-
listDecisionFamilies(); // ["open-jev", "laya", "strands-decider", "bekko"]
|
|
228
|
+
listDecisionFamilies(); // ["open-jev", "laya", "strands-decider", "bekko", "decision2"]
|
|
229
229
|
const runtime = await createDecisionRuntime({
|
|
230
230
|
family: "strands-decider",
|
|
231
231
|
fallback: ["laya", "open-jev"],
|
|
@@ -366,6 +366,45 @@ Lower-level helpers are exported for custom runtimes:
|
|
|
366
366
|
`renderBekkoContent`, `renderBekkoRequest`, `tokenizeBekkoRequest`,
|
|
367
367
|
`collateBekkoDocs`, `bekkoSoftmax`, `bekkoAnswerFromLogits`.
|
|
368
368
|
|
|
369
|
+
## Decision 2.0 family (Qwen3 + candidate head)
|
|
370
|
+
|
|
371
|
+
`createDecision2Decider(options)` runs
|
|
372
|
+
[vLLM Semantic Router Decision 2.0](https://huggingface.co/collections/vllm-sr/decision-20),
|
|
373
|
+
a Qwen3 fine-tune with a candidate head: one prompt per question, each option
|
|
374
|
+
rendered as a `{"description": …, "key": …}` JSON segment. One ONNX graph
|
|
375
|
+
(`input_ids, attention_mask, answer_pos, option_pos → logits [B, K]`), one
|
|
376
|
+
forward pass for the whole batch:
|
|
377
|
+
|
|
378
|
+
```js
|
|
379
|
+
import { createDecision2Decider } from "jev-web";
|
|
380
|
+
|
|
381
|
+
const decider = await createDecision2Decider({
|
|
382
|
+
// defaults to onnx-community/Decision-2.0-Kai-0.6B-ONNX @ pinned revision,
|
|
383
|
+
// q8 (onnx/model_quantized.onnx, 0.61 GB)
|
|
384
|
+
onProgress: (p) => console.log(p.phase, p.loaded, p.total),
|
|
385
|
+
});
|
|
386
|
+
const { answers } = await decider.decide(stateText, questions);
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Protocol implemented here (mirrors the reference `decision2` runtime):
|
|
390
|
+
|
|
391
|
+
- segments `Context:` / `Task type:` / `Question:` / `Options:` / one
|
|
392
|
+
`<option>` JSON segment per option / the `Decision:` suffix, each tokenized
|
|
393
|
+
separately so `option_pos` (last token of each `</option>`) and `answer_pos`
|
|
394
|
+
(last token) are exact — validated 11/11 against the reference fixtures;
|
|
395
|
+
- choice questions accept `criteria` as `{label: description}` (empty
|
|
396
|
+
descriptions encode as `null`); noul slots are fixed `false`/`true`;
|
|
397
|
+
- score questions add the per-level `score_bias` from `config.json` before the
|
|
398
|
+
softmax (temperature 1).
|
|
399
|
+
|
|
400
|
+
Larger siblings are available via the exported model/revision constants:
|
|
401
|
+
`DECISION2_EOS_MODEL` / `DECISION2_EOS_REVISION` (0.8B) and
|
|
402
|
+
`DECISION2_SOL_MODEL` / `DECISION2_SOL_REVISION` (2B).
|
|
403
|
+
|
|
404
|
+
Lower-level helpers are exported for custom runtimes:
|
|
405
|
+
`renderDecision2Content`, `renderDecision2Options`, `buildDecision2Segments`,
|
|
406
|
+
`tokenizeDecision2Segments`, `decision2Softmax`, `decision2AnswerFromLogits`.
|
|
407
|
+
|
|
369
408
|
## Worker / thread
|
|
370
409
|
|
|
371
410
|
`jev-web` does not bundle a worker, so hosts can create one the way their
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jev-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* decision2.mjs — vLLM Semantic Router Decision 2.0 family.
|
|
3
|
+
*
|
|
4
|
+
* Decision 2.0 is a Qwen3 fine-tune with a candidate head: one prompt per
|
|
5
|
+
* question, built from segments (each tokenized separately so the readout
|
|
6
|
+
* positions are exact):
|
|
7
|
+
*
|
|
8
|
+
* Context:\n{state}\n\n
|
|
9
|
+
* Task type: {noul|choice|score}\n
|
|
10
|
+
* Question:\n{instructions}\n
|
|
11
|
+
* Options:\n
|
|
12
|
+
* <option>\n{"description":…,"key":"…"}\n</option>\n (per option)
|
|
13
|
+
* \nSelect the single option best supported by the context and instructions.\n
|
|
14
|
+
* Decision:
|
|
15
|
+
*
|
|
16
|
+
* Graph: input_ids [B,L], attention_mask [B,L], answer_pos [B], option_pos
|
|
17
|
+
* [B,K] -> logits [B,K]. option_pos is the last token of each </option>;
|
|
18
|
+
* answer_pos is the last token of the prompt. Score questions add the
|
|
19
|
+
* per-level score_bias from config.json, then softmax (temperature 1).
|
|
20
|
+
*
|
|
21
|
+
* Reference: onnx-community/Decision-2.0-Kai-0.6B-ONNX (conversion/README).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { normalizeQuestions } from "./questions.mjs";
|
|
25
|
+
import { fetchCachedBytes } from "./laya.mjs";
|
|
26
|
+
|
|
27
|
+
export const DECISION2_DEFAULT_MODEL = "onnx-community/Decision-2.0-Kai-0.6B-ONNX";
|
|
28
|
+
export const DECISION2_DEFAULT_REVISION = "13ab55013ab1";
|
|
29
|
+
export const DECISION2_DEFAULT_FILE = "onnx/model_quantized.onnx";
|
|
30
|
+
export const DECISION2_MAX_INPUT_TOKENS = 8192;
|
|
31
|
+
|
|
32
|
+
// Larger siblings (same protocol, same onnx/ layout).
|
|
33
|
+
export const DECISION2_EOS_MODEL = "onnx-community/Decision-2.0-Eos-0.8B-ONNX";
|
|
34
|
+
export const DECISION2_EOS_REVISION = "6369be38417e";
|
|
35
|
+
export const DECISION2_SOL_MODEL = "onnx-community/Decision-2.0-Sol-2B-ONNX";
|
|
36
|
+
export const DECISION2_SOL_REVISION = "5b3478e05ce3";
|
|
37
|
+
|
|
38
|
+
export const DECISION2_NOUL_DESCRIPTIONS = { false: "No", true: "Yes" };
|
|
39
|
+
const DECISION_SUFFIX = "Select the single option best supported by the context and instructions.";
|
|
40
|
+
|
|
41
|
+
/** Flatten a state or instruction into text, stably (object keys sorted). */
|
|
42
|
+
export function renderDecision2Content(content) {
|
|
43
|
+
if (typeof content === "string") return content;
|
|
44
|
+
return JSON.stringify(sortKeys(content));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Recursively sort object keys for deterministic serialization. */
|
|
48
|
+
function sortKeys(value) {
|
|
49
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
50
|
+
if (value !== null && typeof value === "object") {
|
|
51
|
+
return Object.fromEntries(
|
|
52
|
+
Object.keys(value).sort().map((k) => [k, sortKeys(value[k])]));
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build the option list for one question: [{key, description}]. Descriptions
|
|
59
|
+
* are null when empty (matching the reference encoding).
|
|
60
|
+
*/
|
|
61
|
+
export function renderDecision2Options(question) {
|
|
62
|
+
if (question.type === "choice") {
|
|
63
|
+
const criteria = question.criteria ?? {};
|
|
64
|
+
return question.options.map((label) => {
|
|
65
|
+
const d = criteria[label];
|
|
66
|
+
return { key: label, description: d == null || String(d).trim() === "" ? null : String(d) };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (question.type === "noul") {
|
|
70
|
+
const criteria = question.criteria ?? {};
|
|
71
|
+
const falseDesc = criteria.false ?? criteria.no ?? DECISION2_NOUL_DESCRIPTIONS.false;
|
|
72
|
+
const trueDesc = criteria.true ?? criteria.yes ?? DECISION2_NOUL_DESCRIPTIONS.true;
|
|
73
|
+
return [
|
|
74
|
+
{ key: "false", description: String(falseDesc) },
|
|
75
|
+
{ key: "true", description: String(trueDesc) },
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
if (question.type === "score") {
|
|
79
|
+
return question.options.map((rubric, i) => ({ key: String(i), description: String(rubric) }));
|
|
80
|
+
}
|
|
81
|
+
throw new TypeError(`decision2: unsupported question type ${question.type}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build the prompt segments for one question. Each segment is tokenized
|
|
86
|
+
* separately; the returned `optionEnds` marks which segment index ends each
|
|
87
|
+
* option (for option_pos), and the prompt ends with the Decision: segment.
|
|
88
|
+
*/
|
|
89
|
+
export function buildDecision2Segments(question, stateText) {
|
|
90
|
+
const options = renderDecision2Options(question);
|
|
91
|
+
const segments = [
|
|
92
|
+
`Context:\n${stateText}\n\n`,
|
|
93
|
+
`Task type: ${question.type}\n`,
|
|
94
|
+
`Question:\n${renderDecision2Content(question.instructions)}\n`,
|
|
95
|
+
`Options:`,
|
|
96
|
+
];
|
|
97
|
+
const optionEnds = [];
|
|
98
|
+
for (const opt of options) {
|
|
99
|
+
// Sorted keys, compact JSON, matching the reference.
|
|
100
|
+
const payload = JSON.stringify({ description: opt.description, key: opt.key });
|
|
101
|
+
segments.push(`\n<option>\n${payload}\n</option>`);
|
|
102
|
+
optionEnds.push(segments.length - 1);
|
|
103
|
+
}
|
|
104
|
+
segments.push(`\n\n${DECISION_SUFFIX}\n`);
|
|
105
|
+
segments.push(`Decision:`);
|
|
106
|
+
return { segments, optionEnds, keys: options.map((o) => o.key) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Tokenize segments separately and concatenate, recording the absolute
|
|
111
|
+
* position of each option's last token (option_pos) and the prompt's last
|
|
112
|
+
* token (answer_pos).
|
|
113
|
+
*/
|
|
114
|
+
export function tokenizeDecision2Segments(segments, optionEnds, encode, maxLength) {
|
|
115
|
+
const ids = [];
|
|
116
|
+
const optionPos = [];
|
|
117
|
+
for (let s = 0; s < segments.length; s++) {
|
|
118
|
+
const segIds = encode(segments[s]);
|
|
119
|
+
const start = ids.length;
|
|
120
|
+
ids.push(...segIds);
|
|
121
|
+
if (optionEnds.includes(s)) {
|
|
122
|
+
optionPos.push(start + segIds.length - 1);
|
|
123
|
+
}
|
|
124
|
+
if (ids.length > maxLength) {
|
|
125
|
+
throw new RangeError(
|
|
126
|
+
`decision2: prompt too long (${ids.length} > ${maxLength} tokens)`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { ids, optionPos, answerPos: ids.length - 1 };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Softmax with temperature (temperature 1 for Decision 2.0). */
|
|
133
|
+
export function decision2Softmax(logits) {
|
|
134
|
+
const max = Math.max(...logits);
|
|
135
|
+
const exps = logits.map((x) => Math.exp(Number(x) - max));
|
|
136
|
+
const sum = exps.reduce((a, b) => a + b, 0);
|
|
137
|
+
return exps.map((e) => e / sum);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Decode one question's logits into a jev-web answer. Score questions add
|
|
142
|
+
* the per-level bias before the softmax.
|
|
143
|
+
*/
|
|
144
|
+
export function decision2AnswerFromLogits({ logits, question, keys, scoreBias }) {
|
|
145
|
+
const adjusted = logits.map(Number);
|
|
146
|
+
if (question.type === "score" && scoreBias) {
|
|
147
|
+
const bias = scoreBias[String(keys.length)];
|
|
148
|
+
if (bias) {
|
|
149
|
+
if (bias.length !== adjusted.length) {
|
|
150
|
+
throw new TypeError(
|
|
151
|
+
`decision2: score_bias length ${bias.length} != ${adjusted.length} options`);
|
|
152
|
+
}
|
|
153
|
+
for (let i = 0; i < adjusted.length; i++) adjusted[i] += bias[i];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const probs = decision2Softmax(adjusted);
|
|
157
|
+
const best = probs.indexOf(Math.max(...probs));
|
|
158
|
+
|
|
159
|
+
if (question.type === "choice") {
|
|
160
|
+
const probabilities = Object.fromEntries(keys.map((k, i) => [k, probs[i]]));
|
|
161
|
+
return {
|
|
162
|
+
type: "choice", choice: keys[best], index: best,
|
|
163
|
+
probabilities, confidence: Math.max(...probs),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (question.type === "noul") {
|
|
167
|
+
const yesIdx = keys.indexOf("true");
|
|
168
|
+
const yes = probs[yesIdx];
|
|
169
|
+
const [noLabel, yesLabel] = question.options;
|
|
170
|
+
return {
|
|
171
|
+
type: "noul", noul: yes,
|
|
172
|
+
probabilities: { [noLabel]: 1 - yes, [yesLabel]: yes },
|
|
173
|
+
confidence: Math.max(yes, 1 - yes),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// score
|
|
177
|
+
const values = keys.map((k) => Number(k));
|
|
178
|
+
const score = probs.reduce((acc, p, i) => acc + p * values[i], 0);
|
|
179
|
+
const probabilities = Object.fromEntries(keys.map((k, i) => [k, probs[i]]));
|
|
180
|
+
return {
|
|
181
|
+
type: "score", score, level: best,
|
|
182
|
+
legend: Object.fromEntries(question.options.map((r, i) => [String(i), r])),
|
|
183
|
+
probabilities, confidence: Math.max(...probs),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
188
|
+
const hfUrl = (model, revision, ...parts) =>
|
|
189
|
+
`https://huggingface.co/${model}/resolve/${revision ? `${revision}/` : ""}${parts.join("/")}`;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Create a Decision 2.0 decider.
|
|
193
|
+
*
|
|
194
|
+
* @param {object} [opts]
|
|
195
|
+
* model/revision/file — pinned ONNX export (or your own mirror)
|
|
196
|
+
* configUrl — override for config.json (carries decision2.score_bias)
|
|
197
|
+
* ort, transformers, fetchImpl, wasmPaths, device, sessionFactory — as in
|
|
198
|
+
* the other families
|
|
199
|
+
* maxLength — token budget (default 8192)
|
|
200
|
+
*/
|
|
201
|
+
export async function createDecision2Decider({
|
|
202
|
+
model = DECISION2_DEFAULT_MODEL,
|
|
203
|
+
revision = DECISION2_DEFAULT_REVISION,
|
|
204
|
+
file = DECISION2_DEFAULT_FILE,
|
|
205
|
+
configUrl = null,
|
|
206
|
+
ort = null,
|
|
207
|
+
transformers = null,
|
|
208
|
+
fetchImpl = fetch,
|
|
209
|
+
wasmPaths = null,
|
|
210
|
+
device = "auto",
|
|
211
|
+
sessionFactory = null,
|
|
212
|
+
onProgress = null,
|
|
213
|
+
maxLength = DECISION2_MAX_INPUT_TOKENS,
|
|
214
|
+
scope = globalThis,
|
|
215
|
+
} = {}) {
|
|
216
|
+
const ortMod = ort ?? (await import("onnxruntime-web"));
|
|
217
|
+
const tf = transformers ?? (await import("@huggingface/transformers"));
|
|
218
|
+
if (wasmPaths) {
|
|
219
|
+
try { ortMod.env.wasm.wasmPaths = wasmPaths; } catch { /* older runtime */ }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const resolvedDevice = device !== "auto"
|
|
223
|
+
? device
|
|
224
|
+
: (scope?.navigator?.gpu ? "webgpu" : "wasm");
|
|
225
|
+
const executionProviders = resolvedDevice === "webgpu" ? ["webgpu", "wasm"] : ["wasm"];
|
|
226
|
+
|
|
227
|
+
const configRes = await fetchImpl(configUrl ?? hfUrl(model, revision, "config.json"));
|
|
228
|
+
if (!configRes.ok) throw new Error(`decision2: config fetch failed (${configRes.status})`);
|
|
229
|
+
const config = await configRes.json();
|
|
230
|
+
const scoreBias = config?.decision2?.score_bias ?? {};
|
|
231
|
+
|
|
232
|
+
const tok = await tf.AutoTokenizer.from_pretrained(model, {
|
|
233
|
+
revision,
|
|
234
|
+
progress_callback: (p) => onProgress?.({ phase: "tokenizer", ...p }),
|
|
235
|
+
});
|
|
236
|
+
const encode = (text) =>
|
|
237
|
+
Array.from(tok(text, { add_special_tokens: false }).input_ids.data, Number);
|
|
238
|
+
|
|
239
|
+
const url = hfUrl(model, revision, file);
|
|
240
|
+
const createSession = sessionFactory
|
|
241
|
+
?? (async (u, opts) => {
|
|
242
|
+
const modelBytes = await fetchCachedBytes(u, {
|
|
243
|
+
fetchImpl, onProgress: (p) => onProgress?.(p), persist: true,
|
|
244
|
+
});
|
|
245
|
+
return ortMod.InferenceSession.create(modelBytes, opts);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
let session;
|
|
249
|
+
let activeDevice = resolvedDevice;
|
|
250
|
+
try {
|
|
251
|
+
session = await createSession(url, { executionProviders, graphOptimizationLevel: "all" });
|
|
252
|
+
} catch (e) {
|
|
253
|
+
if (resolvedDevice === "webgpu") {
|
|
254
|
+
onProgress?.({ phase: `webgpu unavailable (${e?.message ?? e}) — retrying on wasm` });
|
|
255
|
+
session = await createSession(url, { executionProviders: ["wasm"], graphOptimizationLevel: "all" });
|
|
256
|
+
activeDevice = "wasm";
|
|
257
|
+
} else {
|
|
258
|
+
throw e;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Probe external data (the q8 file ships a .onnx_data sidecar).
|
|
263
|
+
try {
|
|
264
|
+
const probe = await fetchImpl(url + "_data", { method: "HEAD" });
|
|
265
|
+
if (!probe.ok) {
|
|
266
|
+
const probe2 = await fetchImpl(url.replace(/\.onnx$/, ".onnx_data"), { method: "HEAD" });
|
|
267
|
+
void probe2;
|
|
268
|
+
}
|
|
269
|
+
} catch { /* optional */ }
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
info: {
|
|
273
|
+
family: "decision2",
|
|
274
|
+
model, revision, device: activeDevice,
|
|
275
|
+
maxLength,
|
|
276
|
+
},
|
|
277
|
+
async decide(state, questions) {
|
|
278
|
+
const t0 = now();
|
|
279
|
+
const normalized = normalizeQuestions(questions);
|
|
280
|
+
const stateText = renderDecision2Content(state);
|
|
281
|
+
const B = normalized.length;
|
|
282
|
+
const K = Math.max(...normalized.map((q) => renderDecision2Options(q).length));
|
|
283
|
+
|
|
284
|
+
const allIds = [];
|
|
285
|
+
const answerPos = [];
|
|
286
|
+
const optionPos = [];
|
|
287
|
+
const metas = [];
|
|
288
|
+
let truncated = false;
|
|
289
|
+
let length = 0;
|
|
290
|
+
|
|
291
|
+
for (const q of normalized) {
|
|
292
|
+
const { segments, optionEnds, keys } = buildDecision2Segments(q, stateText);
|
|
293
|
+
const { ids, optionPos: oPos, answerPos: aPos } =
|
|
294
|
+
tokenizeDecision2Segments(segments, optionEnds, encode, maxLength);
|
|
295
|
+
allIds.push(ids);
|
|
296
|
+
answerPos.push(aPos);
|
|
297
|
+
// Pad option positions with 0 (matches the strands convention).
|
|
298
|
+
const padded = [...oPos];
|
|
299
|
+
while (padded.length < K) padded.push(0);
|
|
300
|
+
optionPos.push(padded);
|
|
301
|
+
metas.push({ question: q, keys });
|
|
302
|
+
length += ids.length;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Pad to a rectangular batch.
|
|
306
|
+
const L = Math.max(...allIds.map((r) => r.length));
|
|
307
|
+
const inputIds = new BigInt64Array(B * L);
|
|
308
|
+
const attentionMask = new BigInt64Array(B * L);
|
|
309
|
+
for (let b = 0; b < B; b++) {
|
|
310
|
+
allIds[b].forEach((id, i) => {
|
|
311
|
+
inputIds[b * L + i] = BigInt(id);
|
|
312
|
+
attentionMask[b * L + i] = 1n;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
const feeds = {
|
|
316
|
+
input_ids: new ortMod.Tensor("int64", inputIds, [B, L]),
|
|
317
|
+
attention_mask: new ortMod.Tensor("int64", attentionMask, [B, L]),
|
|
318
|
+
answer_pos: new ortMod.Tensor("int64", BigInt64Array.from(answerPos.map(BigInt)), [B]),
|
|
319
|
+
option_pos: new ortMod.Tensor(
|
|
320
|
+
"int64",
|
|
321
|
+
BigInt64Array.from(optionPos.flat().map(BigInt)),
|
|
322
|
+
[B, K]),
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const tRun = now();
|
|
326
|
+
const out = await session.run(feeds);
|
|
327
|
+
const runMs = now() - tRun;
|
|
328
|
+
const logits = out.logits ?? out[Object.keys(out)[0]];
|
|
329
|
+
if (logits.dims.length !== 2 || logits.dims[0] !== B || logits.dims[1] !== K) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`decision2: invalid logits shape [${logits.dims}] for batch ${B}x${K}`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const answers = metas.map(({ question, keys }, b) => {
|
|
335
|
+
const row = Array.from(
|
|
336
|
+
{ length: keys.length },
|
|
337
|
+
(_, k) => Number(logits.data[b * K + k]));
|
|
338
|
+
return decision2AnswerFromLogits({ logits: row, question, keys, scoreBias });
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
answers, truncated, length,
|
|
343
|
+
prompts: metas.map(({ question, keys }) => ({ question: question.instructions, keys })),
|
|
344
|
+
timings: { totalMs: Math.round(now() - t0), runMs: Math.round(runMs) },
|
|
345
|
+
};
|
|
346
|
+
},
|
|
347
|
+
async dispose() {
|
|
348
|
+
try { await session.release?.(); } catch { /* optional */ }
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -69,6 +69,24 @@ export {
|
|
|
69
69
|
BEKKO_DEFAULT_NO,
|
|
70
70
|
BEKKO_NOUL_DESCRIPTIONS,
|
|
71
71
|
} from "./bekko.mjs";
|
|
72
|
+
export {
|
|
73
|
+
createDecision2Decider,
|
|
74
|
+
renderDecision2Content,
|
|
75
|
+
renderDecision2Options,
|
|
76
|
+
buildDecision2Segments,
|
|
77
|
+
tokenizeDecision2Segments,
|
|
78
|
+
decision2Softmax,
|
|
79
|
+
decision2AnswerFromLogits,
|
|
80
|
+
DECISION2_DEFAULT_MODEL,
|
|
81
|
+
DECISION2_DEFAULT_REVISION,
|
|
82
|
+
DECISION2_DEFAULT_FILE,
|
|
83
|
+
DECISION2_EOS_MODEL,
|
|
84
|
+
DECISION2_EOS_REVISION,
|
|
85
|
+
DECISION2_SOL_MODEL,
|
|
86
|
+
DECISION2_SOL_REVISION,
|
|
87
|
+
DECISION2_MAX_INPUT_TOKENS,
|
|
88
|
+
DECISION2_NOUL_DESCRIPTIONS,
|
|
89
|
+
} from "./decision2.mjs";
|
|
72
90
|
export {
|
|
73
91
|
DEFAULT_MODEL,
|
|
74
92
|
DEFAULT_REVISION,
|
package/src/registry.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* registry.mjs — multi-family runtime for JEV-style typed-decision models.
|
|
3
3
|
*
|
|
4
|
-
* jev-web ships three families today
|
|
5
|
-
* ModernBERT encoder + typed head, Strands Decider 2B
|
|
6
|
-
* head,
|
|
7
|
-
* its own without touching
|
|
4
|
+
* jev-web ships three families today — open-jev (fused DeBERTa graph), Laya
|
|
5
|
+
* (ModernBERT encoder + typed head), Strands Decider 2B (Qwen torso + pointer
|
|
6
|
+
* head), Bekko System One v0 (cross-encoder), and Decision 2.0 (Qwen3 +
|
|
7
|
+
* candidate head) — and any third party can add its own without touching
|
|
8
|
+
* core:
|
|
8
9
|
*
|
|
9
10
|
* registerDecisionFamily("my-model", {
|
|
10
11
|
* defaults: { model: "org/my-typed-decision-ONNX", revision: "main" },
|
|
@@ -38,6 +39,11 @@ import {
|
|
|
38
39
|
BEKKO_DEFAULT_MODEL,
|
|
39
40
|
BEKKO_DEFAULT_REVISION,
|
|
40
41
|
} from "./bekko.mjs";
|
|
42
|
+
import {
|
|
43
|
+
createDecision2Decider,
|
|
44
|
+
DECISION2_DEFAULT_MODEL,
|
|
45
|
+
DECISION2_DEFAULT_REVISION,
|
|
46
|
+
} from "./decision2.mjs";
|
|
41
47
|
|
|
42
48
|
const families = new Map();
|
|
43
49
|
|
|
@@ -94,6 +100,14 @@ registerDecisionFamily("bekko", {
|
|
|
94
100
|
create: createBekkoDecider,
|
|
95
101
|
});
|
|
96
102
|
|
|
103
|
+
registerDecisionFamily("decision2", {
|
|
104
|
+
defaults: {
|
|
105
|
+
model: DECISION2_DEFAULT_MODEL,
|
|
106
|
+
revision: DECISION2_DEFAULT_REVISION,
|
|
107
|
+
},
|
|
108
|
+
create: createDecision2Decider,
|
|
109
|
+
});
|
|
110
|
+
|
|
97
111
|
/**
|
|
98
112
|
* Create a decision runtime, preferring `family` and falling back in order.
|
|
99
113
|
* @param {{family?:string, fallback?:string[], onFallback?:Function}} [opts]
|