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/src/vega.mjs ADDED
@@ -0,0 +1,1125 @@
1
+ /**
2
+ * vega.mjs — Vega 0.8B family (frozen Qwen3.5 backbone + latent decision-physics engine).
3
+ *
4
+ * Vega is the "System One" decision model by the Laya author: the language model
5
+ * is frozen and never trained; everything learned lives in a small physics
6
+ * engine on top of it (github.com/nandakishorm/vega-08b-public-intents ships
7
+ * the reference `vega_common.py` / `vega_api.py`):
8
+ *
9
+ * prompt (template 0, built by buildVegaPrompt — mirrors build_prompt):
10
+ * [BOS] Observation:\n <state> \n\nMeasurement (choice|score|boolean): <instructions>
11
+ * \nPossible outcomes: \n* <opt> … \nOutcome:
12
+ * perception: one causal read up to the deepest captured layer (13 and 19 of
13
+ * 24), each span mean-pooled -> item feats [B, 2, 3, d] (state / question /
14
+ * last token) + option feats [B, K, 1, d] + state chunk means for the
15
+ * memory reader;
16
+ * engine: the state becomes a world latent, the question drops a particle
17
+ * into a 64-d space whose landscape holds one Gaussian well per option, a
18
+ * damped symplectic integrator with a selective-SSM thermostat runs up to
19
+ * n_steps steps (settled particles freeze), and the Boltzmann occupancy of
20
+ * the wells is the answer distribution;
21
+ * calibration: thermal — tau depends on the particle's physical state
22
+ * (residual kinetic energy, distance to the nearest well, steps used,
23
+ * number of options), fitted per question type and shipped in
24
+ * vega_config.json; split-conformal prediction sets and an abstain flag
25
+ * come from the same energies, plus an "unbound particle" OOD signal.
26
+ *
27
+ * Two ONNX graph layouts ship:
28
+ *
29
+ * fused (warsang/vega-08b-web, the default) — one fused torso that pools
30
+ * in-graph + the fp32 engine, 1.5 GB fp16 torso, base engine only;
31
+ * split (nvkudva/vega-onnx-q8) — an 8-bit embed graph, a cut decoder that
32
+ * emits the residual stream at layers 13/19 (the LM head and layers 20-23
33
+ * never run), the fp32 engine with both gated task adapters, and JS
34
+ * pooling; 840 MB, wasm-only (8-bit MatMulNBits has no WebGPU kernel).
35
+ *
36
+ * JS owns the protocol, the packing, the thermal calibration and the answer
37
+ * decoding in both.
38
+ */
39
+ import { normalizeQuestions } from "./questions.mjs";
40
+ import { softmaxWithTemperature } from "./answers.mjs";
41
+ import { fetchCachedBytes, fetchOnnxExternalData } from "./laya.mjs";
42
+
43
+ export const VEGA_DEFAULT_MODEL = "warsang/vega-08b-web";
44
+ export const VEGA_DEFAULT_REVISION = "d2bd4604ed770ca614745f07f623b66e89f406e4";
45
+ // The exact fp16 torso ships by default: q8 on this graph drifts on the
46
+ // GatedDeltaNet delta rule (export/vega/04_repo.py measures ~0.46 in the
47
+ // pooled features), so torso_quantized.onnx stays an opt-in experiment.
48
+ export const VEGA_TORSO_FILE = "onnx/torso_fp16.onnx";
49
+ export const VEGA_ENGINE_FILE = "onnx/engine.onnx";
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Split export: nvkudva/vega-onnx-q8 (8-bit embed + cut decoder + fp32 engine)
53
+ // ---------------------------------------------------------------------------
54
+ export const VEGA_Q8_DEFAULT_MODEL = "nvkudva/vega-onnx-q8";
55
+ export const VEGA_Q8_DEFAULT_REVISION = "017c096d369ac698087f8c35a6ad8c867a7fe0f4";
56
+ export const VEGA_Q8_DEFAULT_SUBFOLDER = "v2";
57
+ export const VEGA_Q8_EMBED_FILE = "embed_q8.onnx";
58
+ export const VEGA_Q8_DECODER_FILE = "decoder_q8.onnx";
59
+ export const VEGA_Q8_ENGINE_FILE = "engine.onnx";
60
+
61
+ /**
62
+ * Named exports. `fp16` is this repo's own fused export (default); `q8` is
63
+ * nvkudva's split 8-bit export of the same checkpoint, which adds the two
64
+ * gated task adapters (public_intents / typed_decisions) and is wasm-only.
65
+ */
66
+ export const VEGA_VARIANTS = {
67
+ fp16: {
68
+ model: VEGA_DEFAULT_MODEL,
69
+ revision: VEGA_DEFAULT_REVISION,
70
+ format: "fused",
71
+ description: "fused fp16 torso + fp32 engine, 1.7 GB (the default)",
72
+ },
73
+ q8: {
74
+ model: VEGA_Q8_DEFAULT_MODEL,
75
+ revision: VEGA_Q8_DEFAULT_REVISION,
76
+ subfolder: VEGA_Q8_DEFAULT_SUBFOLDER,
77
+ format: "split",
78
+ description: "8-bit split embed + cut decoder + fp32 engine with adapters, 840 MB, wasm-only",
79
+ },
80
+ };
81
+
82
+ /** Reference prompt template 0 (the inference surface form). */
83
+ const VEGA_HEAD = "Observation:\n";
84
+ const VEGA_QUESTION = "\n\nMeasurement ({t}): {ins}\nPossible outcomes:";
85
+ const VEGA_OPTION = "\n* {o}";
86
+ const VEGA_END = "\nOutcome:";
87
+ /** Prefixed to short states so the frozen model reads knowing what is asked. */
88
+ const VEGA_QUERY_FIRST = "Question to keep in mind while reading ({t}): {ins}\n\n";
89
+
90
+ /** jev-web question types -> Vega qtypes (choice=0, score=1, boolean=2). */
91
+ export const VEGA_QTYPE = { choice: 0, score: 1, boolean: 2 };
92
+ export const VEGA_QTYPE_NAMES = ["choice", "score", "boolean"];
93
+
94
+ export const VEGA_DEFAULTS = {
95
+ optMaxTokens: 32,
96
+ insMaxTokens: 256,
97
+ memoryChunks: 16,
98
+ memoryMinTokens: 128,
99
+ memoryChunkTokens: 128,
100
+ prefixMinTokens: 4096,
101
+ queryFirstMaxState: 4096,
102
+ maxLen: 73728,
103
+ nSteps: 12,
104
+ risk: "0.1",
105
+ alpha: "0.1",
106
+ };
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Protocol: question defs, option texts, prompt building
110
+ // ---------------------------------------------------------------------------
111
+
112
+ /**
113
+ * Map a normalized jev-web question to Vega's internal question def.
114
+ * Vega's `boolean` is the noul primitive; its slots are fixed [no, yes].
115
+ */
116
+ export function vegaQuestionDef(question) {
117
+ // the reference json.dumps non-string instructions with ensure_ascii=True
118
+ const ins = typeof question.instructions === "string"
119
+ ? question.instructions
120
+ : vegaJsonDumps(question.instructions, true);
121
+ if (question.type === "noul") {
122
+ return { t: "boolean", ins, crit: { ...(question.criteria ?? {}) } };
123
+ }
124
+ if (question.type === "score") {
125
+ return { t: "score", ins, crit: [...question.options] };
126
+ }
127
+ // choice: criteria {label: description} when provided, else bare options.
128
+ const crit = question.criteria && typeof question.criteria === "object"
129
+ && !Array.isArray(question.criteria)
130
+ ? question.criteria
131
+ : Object.fromEntries(question.options.map((o) => [String(o), null]));
132
+ return { t: "choice", ins, crit };
133
+ }
134
+
135
+ /**
136
+ * Python `json.dumps(x, ensure_ascii)` — ", " / ": " separators, control
137
+ * characters escaped, and optionally \uXXXX for everything above ASCII. The
138
+ * reference runtime serializes dict states, non-string instructions and
139
+ * non-string criteria values with it, so the prompt stays byte-identical to
140
+ * the Python original (JSON.stringify uses compact separators and never
141
+ * escapes non-ASCII).
142
+ */
143
+ export function vegaJsonDumps(value, ensureAscii = false) {
144
+ const esc = (s) => {
145
+ let out = '"';
146
+ for (const ch of s) {
147
+ const c = ch.codePointAt(0);
148
+ if (ch === '"') out += '\\"';
149
+ else if (ch === "\\") out += "\\\\";
150
+ else if (ch === "\n") out += "\\n";
151
+ else if (ch === "\r") out += "\\r";
152
+ else if (ch === "\t") out += "\\t";
153
+ else if (c < 0x20) out += "\\u" + c.toString(16).padStart(4, "0");
154
+ else if (ensureAscii && c > 0x7f) {
155
+ if (c > 0xffff) {
156
+ const u = c - 0x10000;
157
+ out += "\\u" + (0xd800 + (u >> 10)).toString(16).padStart(4, "0");
158
+ out += "\\u" + (0xdc00 + (u & 0x3ff)).toString(16).padStart(4, "0");
159
+ } else out += "\\u" + c.toString(16).padStart(4, "0");
160
+ } else out += ch;
161
+ }
162
+ return out + '"';
163
+ };
164
+ const go = (x) => {
165
+ if (x === null || x === undefined) return "null";
166
+ if (typeof x === "string") return esc(x);
167
+ if (typeof x === "boolean") return x ? "true" : "false";
168
+ if (typeof x === "number") return String(x);
169
+ if (Array.isArray(x)) return "[" + x.map(go).join(", ") + "]";
170
+ return "{" + Object.entries(x).map(([k, v]) => esc(k) + ": " + go(v)).join(", ") + "}";
171
+ };
172
+ return go(value);
173
+ }
174
+
175
+ /** json.dumps of a non-string criterion value (strings pass through). */
176
+ function vegaCritText(v) {
177
+ return typeof v === "string" ? v : vegaJsonDumps(v, false);
178
+ }
179
+
180
+ /** The reference render_text: strings verbatim, anything else json.dumps. */
181
+ export function renderVegaText(content) {
182
+ return typeof content === "string" ? content : vegaJsonDumps(content, false);
183
+ }
184
+
185
+ /** Option texts in label-index order, exactly as option_texts() renders them. */
186
+ export function vegaOptionTexts(qdef) {
187
+ if (qdef.t === "choice") {
188
+ return Object.entries(qdef.crit).map(([k, v]) => {
189
+ const d = v === null || v === undefined ? "" : vegaCritText(v);
190
+ return d ? `${k} - ${d}` : String(k);
191
+ });
192
+ }
193
+ if (qdef.t === "score") {
194
+ return qdef.crit.map((c, i) => `level ${i} - ${vegaCritText(c)}`);
195
+ }
196
+ const crit = qdef.crit ?? {};
197
+ const no = crit.false === null || crit.false === undefined ? "" : vegaCritText(crit.false);
198
+ const yes = crit.true === null || crit.true === undefined ? "" : vegaCritText(crit.true);
199
+ return [
200
+ `no - ${no || "the statement does not hold"}`,
201
+ `yes - ${yes || "the statement holds"}`,
202
+ ];
203
+ }
204
+
205
+ /**
206
+ * One prompt: BOS + [query-first prefix for short states] + head + state +
207
+ * question + options + end, with the token span of every segment.
208
+ *
209
+ * The state comes first so its span never sees the question (causal
210
+ * attention); the question, options and final token have read the whole
211
+ * state. Options are capped per option, and an over-long option list is
212
+ * shrunk evenly before the instructions are (mirrors build_prompt).
213
+ *
214
+ * `truncateState` defaults to the reference's silent truncation; the split
215
+ * export refuses the decision instead (a cut-off record is worse than none),
216
+ * so its runtime passes false and gets a RangeError.
217
+ *
218
+ * @returns {{ids:number[], spans:{state:[number,number], question:[number,number],
219
+ * options:[number,number][], last:number}}}
220
+ */
221
+ export function buildVegaPrompt({
222
+ encode,
223
+ stateText,
224
+ qdef,
225
+ maxLen,
226
+ bosId = null,
227
+ queryFirstMaxState = VEGA_DEFAULTS.queryFirstMaxState,
228
+ optMaxTokens = VEGA_DEFAULTS.optMaxTokens,
229
+ insMaxTokens = VEGA_DEFAULTS.insMaxTokens,
230
+ truncateState = true,
231
+ }) {
232
+ const options = vegaOptionTexts(qdef);
233
+ const enc = (s) => encode(s, false);
234
+ const bos = bosId === null ? [] : [bosId];
235
+ let head = enc(VEGA_HEAD);
236
+ let qids = enc(VEGA_QUESTION.replace("{t}", qdef.t).replace("{ins}", qdef.ins))
237
+ .slice(0, insMaxTokens);
238
+ let oids = options.map((o) => enc(VEGA_OPTION.replace("{o}", o)).slice(0, optMaxTokens));
239
+ const end = enc(VEGA_END);
240
+ let fixed = bos.length + head.length + qids.length
241
+ + oids.reduce((n, o) => n + o.length, 0) + end.length;
242
+ if (fixed > maxLen - 16) {
243
+ const per = Math.max(3, Math.floor(
244
+ (maxLen - 16 - bos.length - head.length - end.length - Math.min(qids.length, 64))
245
+ / Math.max(1, oids.length)));
246
+ oids = oids.map((o) => o.slice(0, per));
247
+ qids = qids.slice(0, Math.max(
248
+ 8, maxLen - 16 - bos.length - head.length - end.length
249
+ - oids.reduce((n, o) => n + o.length, 0)));
250
+ fixed = bos.length + head.length + qids.length
251
+ + oids.reduce((n, o) => n + o.length, 0) + end.length;
252
+ }
253
+ let st = enc(stateText);
254
+ let pre = [];
255
+ if (queryFirstMaxState && st.length < queryFirstMaxState) {
256
+ pre = enc(VEGA_QUERY_FIRST.replace("{t}", qdef.t).replace("{ins}", qdef.ins))
257
+ .slice(0, Math.min(128, insMaxTokens));
258
+ fixed += pre.length;
259
+ }
260
+ const room = Math.max(0, maxLen - fixed);
261
+ if (!truncateState && st.length > room) {
262
+ throw new RangeError(
263
+ `vega: state is ${st.length} tokens, only ${room} fit in maxLen=${maxLen}`
264
+ + " (this export refuses a truncated state; raise maxLen)");
265
+ }
266
+ st = st.slice(0, room); // truncate_left = false: the head of the state is kept
267
+ const ids = [...bos, ...pre, ...head];
268
+ const s0 = ids.length;
269
+ ids.push(...st);
270
+ const spans = { state: st.length ? [s0, ids.length] : [bos.length, s0] };
271
+ const q0 = ids.length;
272
+ ids.push(...qids);
273
+ spans.question = [q0, ids.length];
274
+ spans.options = [];
275
+ for (const o of oids) {
276
+ const a = ids.length;
277
+ ids.push(...o);
278
+ spans.options.push([a, ids.length]);
279
+ }
280
+ ids.push(...end);
281
+ spans.last = ids.length - 1;
282
+ return { ids, spans };
283
+ }
284
+
285
+ // ---------------------------------------------------------------------------
286
+ // Batch packing: padding, segment means (one-hot pooling), state chunks
287
+ // ---------------------------------------------------------------------------
288
+
289
+ /**
290
+ * Pack rendered prompts into tensors + the pooling matrix the torso graph
291
+ * consumes: one row per segment (per item: state, question, options, the
292
+ * final token, then C state-chunk rows), each segment's row is 1/len over its
293
+ * token span, zeros elsewhere. The graph computes pool @ hidden, i.e. exact
294
+ * span means, so no hidden states ever cross into JS.
295
+ *
296
+ * @returns {{inputIds:BigInt64Array, attention:BigInt64Array,
297
+ * pool:Float32Array, poolDims:[number,number,number], n:number, L:number,
298
+ * K:number, qtypes:number[], optMask:Uint8Array, nseg:number}}
299
+ */
300
+ export function packVegaBatch(items, { padId = 0, chunks: C = VEGA_DEFAULTS.memoryChunks } = {}) {
301
+ const n = items.length;
302
+ const L = Math.max(...items.map((it) => it.ids.length));
303
+ const K = Math.max(...items.map((it) => it.options.length));
304
+ const nseg = 3 + K;
305
+ const rows = nseg + C;
306
+ const inputIds = new BigInt64Array(n * L);
307
+ const attention = new BigInt64Array(n * L);
308
+ const pool = new Float32Array(n * rows * L); // [B, nseg + C, L]
309
+ const optMask = new Uint8Array(n * K);
310
+ const qtypes = [];
311
+ items.forEach((it, b) => {
312
+ it.ids.forEach((v, j) => {
313
+ inputIds[b * L + j] = BigInt(v);
314
+ attention[b * L + j] = 1n;
315
+ });
316
+ const setSpan = (row, [a, e]) => {
317
+ const len = Math.max(1, e - a);
318
+ for (let t = a; t < e; t++) pool[(b * rows + row) * L + t] = 1 / len;
319
+ };
320
+ setSpan(0, it.spans.state);
321
+ setSpan(1, it.spans.question);
322
+ it.spans.options.forEach((span, k) => {
323
+ if (k >= K) return;
324
+ setSpan(3 + k, span);
325
+ optMask[b * K + k] = 1;
326
+ });
327
+ pool[(b * rows + 2) * L + it.spans.last] = 1;
328
+ qtypes.push(it.qtype);
329
+ });
330
+ return {
331
+ inputIds, attention, pool, poolDims: [n, rows, L],
332
+ n, L, K, qtypes, optMask, nseg,
333
+ };
334
+ }
335
+
336
+ /**
337
+ * State chunk means for the memory reader: the state span cut into k equal
338
+ * pieces (about one per memoryChunkTokens tokens, at least 2, at most C;
339
+ * states shorter than memoryMinTokens get none) — mirrors _chunk_pool.
340
+ *
341
+ * Fills rows [nseg, nseg + C) of the SAME pooling matrix the segment means
342
+ * use, so the torso graph emits chunk rows in the one forward it already
343
+ * does (a second run would re-read the whole record).
344
+ *
345
+ * @param {Float32Array} pool the [B, nseg + C, L] pooling matrix
346
+ * @param {object[]} items rendered prompts (with spans)
347
+ * @param {number} L padded sequence length
348
+ */
349
+ export function packVegaChunks(pool, items, L, {
350
+ chunks: C = VEGA_DEFAULTS.memoryChunks,
351
+ chunkMinTokens = VEGA_DEFAULTS.memoryMinTokens,
352
+ chunkTokens = VEGA_DEFAULTS.memoryChunkTokens,
353
+ nseg = 3 + Math.max(...items.map((it) => it.options.length)),
354
+ } = {}) {
355
+ const n = items.length;
356
+ const chunkMask = new Uint8Array(n * C);
357
+ items.forEach((it, b) => {
358
+ const [s0, s1] = it.spans.state;
359
+ const len = s1 - s0;
360
+ if (len < Math.max(chunkMinTokens, 2)) return;
361
+ const k = Math.max(2, Math.min(C, Math.floor(len / Math.max(1, chunkTokens))));
362
+ for (let c = 0; c < k; c++) {
363
+ // token t of the state belongs to chunk floor(t * k / len) (the cid map)
364
+ const row = b * (nseg + C) + nseg + c;
365
+ let count = 0;
366
+ for (let t = s0; t < s1; t++) {
367
+ if (Math.floor(((t - s0) * k) / len) === c) { pool[row * L + t] = 1; count++; }
368
+ }
369
+ if (count > 0) {
370
+ for (let t = s0; t < s1; t++) {
371
+ if (pool[row * L + t]) pool[row * L + t] = 1 / count;
372
+ }
373
+ chunkMask[b * C + c] = 1;
374
+ }
375
+ }
376
+ });
377
+ return chunkMask;
378
+ }
379
+
380
+ // ---------------------------------------------------------------------------
381
+ // Thermal calibration + answer decoding (mirrors thermal_tau / decide)
382
+ // ---------------------------------------------------------------------------
383
+
384
+ /** Fallback when a checkpoint ships no calibration block (probabilities raw). */
385
+ export const VEGA_DEFAULT_THETA = [[0], [0], [0]];
386
+
387
+ /**
388
+ * Per-type thermal calibration from the checkpoint's vega_config.json:
389
+ * log_tau = b + w . phys, tau in [0.2, 20]; probs = softmax(logits / tau).
390
+ *
391
+ * `thetaByRow` overrides the per-type lookup with an explicit theta block per
392
+ * item (the split export calibrates each question with the routed adapter's
393
+ * block).
394
+ *
395
+ * @param {{theta:number[][]}} calibration
396
+ * @param {{logits:ArrayLike<number>, rowWidth:number, qtypes:number[],
397
+ * phys:ArrayLike<number>, nOpts:number[], thetaByRow?:number[][][]}} spec
398
+ * phys is [items, 4]
399
+ * @returns {number[][]} probabilities per item, in prompt order
400
+ */
401
+ export function vegaThermalProbabilities({
402
+ logits, rowWidth, qtypes, phys, nOpts,
403
+ theta = VEGA_DEFAULT_THETA,
404
+ thetaByRow = null,
405
+ }) {
406
+ const rows = Number(logits.length / rowWidth);
407
+ const out = [];
408
+ for (let b = 0; b < rows; b++) {
409
+ const k = nOpts[b];
410
+ const t = thetaByRow ? (thetaByRow[b] ?? theta[0]) : (theta[qtypes[b]] ?? theta[0]);
411
+ let logTau = t[0];
412
+ for (let f = 0; f < 4; f++) logTau += t[1 + f] * phys[b * 4 + f];
413
+ const tau = Math.exp(Math.min(Math.log(20), Math.max(Math.log(0.2), logTau)));
414
+ out.push(softmaxWithTemperature(
415
+ Array.from(logits.slice(b * rowWidth, b * rowWidth + k)), tau));
416
+ }
417
+ return out;
418
+ }
419
+
420
+ /**
421
+ * Per-item thermal features: log1p(kinetic), log1p(nearest), steps/steps_max,
422
+ * log(k) — exactly thermal_features().
423
+ */
424
+ export function vegaThermalFeatures({ kinetic, nearest, stepsUsed, nOpts, maxSteps }) {
425
+ const out = [];
426
+ for (let b = 0; b < nOpts.length; b++) {
427
+ out.push([
428
+ Math.log1p(kinetic[b]),
429
+ Math.log1p(nearest[b]),
430
+ stepsUsed[b] / Math.max(1, maxSteps),
431
+ Math.log(Math.max(2, nOpts[b])),
432
+ ]);
433
+ }
434
+ return out;
435
+ }
436
+
437
+ /**
438
+ * Vega answers -> jev-web DecideResult answers (additive vega fields:
439
+ * predictionSet / abstain / unbound / settleSteps / energy, and — when the
440
+ * split export routes per question — adapter / gate).
441
+ *
442
+ * @param {{probs:number[][], qtypes:number[], labels:string[][],
443
+ * physics:{nearest:ArrayLike<number>, stepsUsed:ArrayLike<number>},
444
+ * rawLogits:ArrayLike<number>, rowWidth:number,
445
+ * calibration:object, alpha?:string, risk?:string,
446
+ * calibrations?:object[], adapterNames?:string[], gates?:number[][],
447
+ * routes?:number[]}} spec calibrations/gates are per row when routed
448
+ */
449
+ export function vegaAnswers({
450
+ probs, qtypes, physics, rawLogits, rowWidth, calibration, questions, labels,
451
+ alpha = VEGA_DEFAULTS.alpha, risk = VEGA_DEFAULTS.risk,
452
+ calibrations = null, adapterNames = null, gates = null, routes = null,
453
+ }) {
454
+ const answers = [];
455
+ labels.forEach((optionLabels, r) => {
456
+ const p = probs[r];
457
+ const k = optionLabels.length;
458
+ const qtype = VEGA_QTYPE_NAMES[qtypes[r]];
459
+ const cal = calibrations?.[r] ?? calibration;
460
+ const conf = Math.max(...p);
461
+ const best = p.indexOf(Math.max(...p));
462
+ // LAC prediction set; the top option is always kept so it is never empty.
463
+ const th = cal?.conformal?.[qtype]?.[String(alpha)];
464
+ const predictionSet = th === undefined ? null
465
+ : optionLabels.filter((_, i) => i === best || p[i] >= Math.max(1 - th, 1e-12));
466
+ // energy = -max raw logit over the question's real options
467
+ let bestLogit = -Infinity;
468
+ for (let j = 0; j < k; j++) bestLogit = Math.max(bestLogit, rawLogits[r * rowWidth + j]);
469
+ const a = {
470
+ probabilities: Object.fromEntries(optionLabels.map((l, i) => [l, p[i]])),
471
+ confidence: conf,
472
+ predictionSet,
473
+ abstain: conf < (cal?.abstain_threshold?.[String(risk)] ?? 0),
474
+ unbound: physics.nearest[r] > (cal?.unbound_threshold ?? Infinity),
475
+ settleSteps: physics.stepsUsed[r],
476
+ energy: -bestLogit,
477
+ };
478
+ if (adapterNames) {
479
+ const rt = routes?.[r] ?? -1;
480
+ a.adapter = rt >= 0 ? (adapterNames[rt] ?? null) : null;
481
+ a.gate = gates?.[r]?.length ? Math.max(...gates[r]) : 0;
482
+ }
483
+ if (qtype === "choice") {
484
+ a.type = "choice";
485
+ a.choice = optionLabels[best];
486
+ a.index = best;
487
+ } else if (qtype === "score") {
488
+ a.type = "score";
489
+ a.level = best;
490
+ a.score = p.reduce((acc, pj, j) => acc + pj * j, 0);
491
+ } else {
492
+ a.type = "noul";
493
+ a.noul = p[1];
494
+ }
495
+ answers.push(a);
496
+ });
497
+ return answers;
498
+ }
499
+
500
+ /** Per-question option labels in prompt order (choice keys / score levels / [no, yes]). */
501
+ export function vegaOptionLabels(questions) {
502
+ return questions.map((q) => {
503
+ if (q.type === "noul") return [...q.options];
504
+ if (q.type === "score") return q.options.map((_, i) => i);
505
+ return [...q.options];
506
+ });
507
+ }
508
+
509
+ // ---------------------------------------------------------------------------
510
+ // Split export: JS-side perception (positions, past state, pooling, routing)
511
+ // ---------------------------------------------------------------------------
512
+
513
+ /**
514
+ * Adapter routing (reference route_adapters): the most open adapter if its
515
+ * gate reaches the threshold, else -1 (the base engine calibrates the answer).
516
+ */
517
+ export function vegaRoute(gates, threshold = 0.5) {
518
+ if (!gates?.length) return -1;
519
+ let best = 0;
520
+ for (let i = 1; i < gates.length; i++) if (gates[i] > gates[best]) best = i;
521
+ return gates[best] >= threshold ? best : -1;
522
+ }
523
+
524
+ /** Text M-RoPE positions: 0..L-1 on each of the three rows ([3, 1, L]). */
525
+ export function vegaTextPositions(L) {
526
+ const out = new BigInt64Array(3 * L);
527
+ for (let i = 0; i < L; i++) {
528
+ out[i] = BigInt(i);
529
+ out[L + i] = BigInt(i);
530
+ out[2 * L + i] = BigInt(i);
531
+ }
532
+ return out;
533
+ }
534
+
535
+ /**
536
+ * Zero-filled first-read state for every past_* input the cut decoder keeps
537
+ * (conv / recurrent / key-value caches). Shapes come from the graph's own
538
+ * input metadata: batch_size -> 1, any other symbol (past_sequence_length)
539
+ * -> 0.
540
+ */
541
+ export function emptyVegaPast(session, Tensor) {
542
+ const feeds = {};
543
+ for (const m of session?.inputMetadata ?? []) {
544
+ if (m?.isTensor === false || !String(m.name).startsWith("past_")) continue;
545
+ const dims = (m.shape ?? []).map(
546
+ (d) => (typeof d === "number" ? d : d === "batch_size" ? 1 : 0));
547
+ feeds[m.name] = new Tensor("float32",
548
+ new Float32Array(dims.reduce((a, b) => a * b, 1)), dims);
549
+ }
550
+ return feeds;
551
+ }
552
+
553
+ /**
554
+ * Perception._pooled_batch for one text item, over the hidden states the split
555
+ * decoder emits: the state/question/final-token span means at every feature
556
+ * layer, the option span means at the deepest option layer, and the state cut
557
+ * into up to C equal chunk means for the memory reader (states shorter than
558
+ * chunkMinTokens get none). Image states get no chunks, as in vega_vision.py.
559
+ *
560
+ * @returns {{feats:Float32Array, opts:Float32Array, chunks:Float32Array,
561
+ * cmask:Uint8Array}} feats [2, 3, d] (layer, slot, d)
562
+ */
563
+ export function poolVegaHidden({
564
+ h13, h19, spans, d,
565
+ chunks: C = VEGA_DEFAULTS.memoryChunks,
566
+ chunkMinTokens = VEGA_DEFAULTS.memoryMinTokens,
567
+ chunkTokens = VEGA_DEFAULTS.memoryChunkTokens,
568
+ withChunks = true,
569
+ }) {
570
+ const L = Math.floor(h13.length / d);
571
+ const mean = (h, a, e, out, off) => {
572
+ if (e <= a) return;
573
+ for (let t = a; t < e; t++) {
574
+ for (let j = 0; j < d; j++) out[off + j] += h[t * d + j];
575
+ }
576
+ for (let j = 0; j < d; j++) out[off + j] /= e - a;
577
+ };
578
+ const feats = new Float32Array(2 * 3 * d);
579
+ [h13, h19].forEach((h, l) => {
580
+ mean(h, spans.state[0], spans.state[1], feats, (l * 3 + 0) * d);
581
+ mean(h, spans.question[0], spans.question[1], feats, (l * 3 + 1) * d);
582
+ feats.set(h.subarray((L - 1) * d, L * d), (l * 3 + 2) * d);
583
+ });
584
+ const opts = new Float32Array(spans.options.length * d);
585
+ spans.options.forEach(([a, e], k) => mean(h19, a, e, opts, k * d));
586
+ const chunks = new Float32Array(C * d);
587
+ const cmask = new Uint8Array(C);
588
+ const [s0, s1] = spans.state;
589
+ const m = s1 - s0;
590
+ if (withChunks && m >= Math.max(chunkMinTokens, 2)) {
591
+ const k = Math.max(2, Math.min(C, Math.floor(m / Math.max(1, chunkTokens))));
592
+ // token t goes to chunk floor(t*k/m): chunk c covers [ceil(c*m/k), ceil((c+1)*m/k))
593
+ for (let c = 0; c < k; c++) {
594
+ const a = s0 + Math.ceil((c * m) / k);
595
+ const e = s0 + (c === k - 1 ? m : Math.ceil(((c + 1) * m) / k));
596
+ mean(h19, a, e, chunks, c * d);
597
+ }
598
+ cmask.fill(1, 0, k);
599
+ }
600
+ return { feats, opts, chunks, cmask };
601
+ }
602
+
603
+ // ---------------------------------------------------------------------------
604
+ // The decider
605
+ // ---------------------------------------------------------------------------
606
+
607
+ const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
608
+
609
+ const hfUrl = (model, revision, file) =>
610
+ `https://huggingface.co/${model}/resolve/${revision ? `${revision}/` : ""}${file}`;
611
+
612
+ /**
613
+ * Session opener shared by both Vega formats: graph bytes (plus an external
614
+ * data sibling when the graph has one) go through the injected fetch and the
615
+ * Cache API, then ORT creates the session; a WebGPU create failure retries on
616
+ * wasm. Hosts that load weights differently (local files, bundled, offline
617
+ * mirrors) replace the whole step with sessionFactory(url, opts).
618
+ */
619
+ function openVegaSessions({
620
+ ortMod, fetchImpl, onProgress, sessionFactory, executionProviders, device,
621
+ }) {
622
+ const create = sessionFactory
623
+ ?? (async (url, opts) => {
624
+ const bytes = await fetchCachedBytes(url, {
625
+ fetchImpl, onProgress: (p) => onProgress?.(p), persist: true,
626
+ });
627
+ const external = await fetchOnnxExternalData(url, { fetchImpl });
628
+ const sessionOpts = external ? { ...opts, externalData: [external] } : opts;
629
+ return ortMod.InferenceSession.create(bytes, sessionOpts);
630
+ });
631
+ return async (url) => {
632
+ try {
633
+ return await create(url, { executionProviders, graphOptimizationLevel: "all" });
634
+ } catch (e) {
635
+ if (device !== "webgpu") throw e;
636
+ onProgress?.({ phase: `webgpu unavailable (${e?.message ?? e}) — retrying on wasm` });
637
+ return create(url, { executionProviders: ["wasm"], graphOptimizationLevel: "all" });
638
+ }
639
+ };
640
+ }
641
+
642
+ /**
643
+ * Create a Vega decider.
644
+ *
645
+ * @param {object} [opts]
646
+ * variant — "fp16" (default, this repo's fused export) or "q8"
647
+ * (nvkudva/vega-onnx-q8: 8-bit split embed + decoder +
648
+ * fp32 engine with adapters, wasm-only). Sets
649
+ * model/revision/subfolder/format; explicit
650
+ * model/revision/format still win.
651
+ * format — "fused" | "split" (normally derived from the variant)
652
+ * model/revision/subfolder — export repo and, for split exports, the
653
+ * version folder that holds the graphs (default "v2")
654
+ * torsoFile/engineFile — fused tograph paths within the repo
655
+ * embedFile/decoderFile — split graph names within the subfolder
656
+ * calibrationModel — repo to read the config from (default: model;
657
+ * point it at the source checkpoint for your own export)
658
+ * maxLen — prompt window; null = checkpoint's max_length
659
+ * alpha / risk — conformal risk level and abstain risk
660
+ * (default: the calibration block's default_risk)
661
+ * device, wasmPaths, sessionFactory, onProgress — as in the other families
662
+ */
663
+ export async function createVegaDecider(opts = {}) {
664
+ const variant = opts.variant ?? null;
665
+ const preset = variant === null ? null : VEGA_VARIANTS[variant];
666
+ if (variant !== null && !preset) {
667
+ throw new TypeError(
668
+ `vega: unknown variant "${variant}" (${Object.keys(VEGA_VARIANTS).join(", ")})`);
669
+ }
670
+ const format = opts.format ?? preset?.format ?? "fused";
671
+ if (format !== "fused" && format !== "split") {
672
+ throw new TypeError(`vega: format must be "fused" or "split" (got ${JSON.stringify(format)})`);
673
+ }
674
+ // Like strands: a named variant replaces the *default* model/revision, so
675
+ // createDecisionRuntime({family: "vega", variant: "q8"}) works even though
676
+ // the registry passes the default target through; a deliberately different
677
+ // model/revision still wins.
678
+ const isDefaultTarget = (opts.model ?? VEGA_DEFAULT_MODEL) === VEGA_DEFAULT_MODEL
679
+ && (opts.revision ?? VEGA_DEFAULT_REVISION) === VEGA_DEFAULT_REVISION;
680
+ const usePreset = preset && isDefaultTarget;
681
+ const resolved = {
682
+ model: usePreset ? preset.model : (opts.model ?? VEGA_DEFAULT_MODEL),
683
+ revision: usePreset ? preset.revision : (opts.revision ?? VEGA_DEFAULT_REVISION),
684
+ subfolder: opts.subfolder ?? preset?.subfolder ?? null,
685
+ };
686
+ return format === "split"
687
+ ? createVegaSplitDecider(opts, resolved)
688
+ : createVegaFusedDecider({ ...opts, ...resolved });
689
+ }
690
+
691
+ /** The fused export (one torso graph + the base engine), the default. */
692
+ async function createVegaFusedDecider({
693
+ model = VEGA_DEFAULT_MODEL,
694
+ revision = VEGA_DEFAULT_REVISION,
695
+ torsoFile = VEGA_TORSO_FILE,
696
+ engineFile = VEGA_ENGINE_FILE,
697
+ calibrationModel = null,
698
+ calibrationRevision = null,
699
+ ort = null,
700
+ transformers = null,
701
+ fetchImpl = fetch,
702
+ wasmPaths = null,
703
+ device = "auto",
704
+ sessionFactory = null,
705
+ onProgress = null,
706
+ maxLen = null,
707
+ alpha = VEGA_DEFAULTS.alpha,
708
+ risk = null,
709
+ scope = globalThis,
710
+ } = {}) {
711
+ const ortMod = ort ?? (await import("onnxruntime-web"));
712
+ const tf = transformers ?? (await import("@huggingface/transformers"));
713
+ if (wasmPaths) {
714
+ try { ortMod.env.wasm.wasmPaths = wasmPaths; } catch { /* older runtime */ }
715
+ }
716
+ const resolvedDevice = device !== "auto"
717
+ ? device
718
+ : (scope?.navigator?.gpu ? "webgpu" : "wasm");
719
+ const executionProviders = resolvedDevice === "webgpu" ? ["webgpu", "wasm"] : ["wasm"];
720
+
721
+ // Calibration + architecture constants come from the checkpoint's
722
+ // vega_config.json (thermal theta, conformal thresholds, chunking, window).
723
+ const configUrl = hfUrl(calibrationModel ?? model,
724
+ calibrationRevision ?? (calibrationModel ? "main" : revision),
725
+ "vega_config.json");
726
+ let config = null;
727
+ let configSource = null;
728
+ try {
729
+ const res = await fetchImpl(configUrl);
730
+ if (res?.ok) {
731
+ config = await res.json();
732
+ configSource = configUrl;
733
+ }
734
+ } catch { /* fall through to the shipped defaults */ }
735
+ const cfg = { ...VEGA_DEFAULTS, ...(config ?? {}) };
736
+ const calibration = config?.calibration ?? {};
737
+ const effRisk = risk ?? calibration.default_risk ?? cfg.default_risk ?? VEGA_DEFAULTS.risk;
738
+ const cfgMaxLen = config?.max_len ?? cfg.maxLen;
739
+ const effMaxLen = Number.isFinite(maxLen) && maxLen > 0 ? maxLen : cfgMaxLen;
740
+
741
+ const tok = await tf.AutoTokenizer.from_pretrained(calibrationModel ?? model, {
742
+ revision: calibrationRevision ?? (calibrationModel ? "main" : revision),
743
+ progress_callback: () => {},
744
+ });
745
+ const encode = (text, addSpecial = false) =>
746
+ Array.from(tok(text, { add_special_tokens: addSpecial }).input_ids.data, Number);
747
+
748
+ const open = openVegaSessions({
749
+ ortMod, fetchImpl, onProgress, sessionFactory,
750
+ executionProviders, device: resolvedDevice,
751
+ });
752
+ const torso = await open(hfUrl(model, revision, torsoFile));
753
+ const engine = await open(hfUrl(model, revision, engineFile));
754
+
755
+ const int64 = (arr, dims) => new ortMod.Tensor("int64", arr, dims);
756
+ const f32 = (arr, dims) => new ortMod.Tensor("float32", arr, dims);
757
+ const bool = (arr, dims) => new ortMod.Tensor("bool", arr, dims);
758
+
759
+ return {
760
+ info: {
761
+ family: "vega",
762
+ format: "fused",
763
+ model,
764
+ revision,
765
+ device: resolvedDevice,
766
+ maxLen: effMaxLen,
767
+ alpha: String(alpha),
768
+ risk: String(effRisk),
769
+ calibrationSource: configSource,
770
+ nSteps: cfg.n_steps ?? cfg.nSteps,
771
+ config,
772
+ },
773
+ async decide(state, questions) {
774
+ const t0 = now();
775
+ const normalized = normalizeQuestions(questions);
776
+ const qdefs = normalized.map(vegaQuestionDef);
777
+ const stateText = renderVegaText(state);
778
+ const bosId = tok.bos_token_id ?? null;
779
+
780
+ const items = qdefs.map((qdef) => {
781
+ const { ids, spans } = buildVegaPrompt({
782
+ encode, stateText, qdef, maxLen: effMaxLen,
783
+ bosId,
784
+ queryFirstMaxState: cfg.query_first_max_state ?? cfg.queryFirstMaxState,
785
+ optMaxTokens: cfg.opt_max_tokens ?? cfg.optMaxTokens,
786
+ insMaxTokens: 256,
787
+ });
788
+ return { ids, spans, qtype: VEGA_QTYPE[qdef.t], options: vegaOptionTexts(qdef) };
789
+ });
790
+
791
+ const batch = packVegaBatch(items, {
792
+ chunks: cfg.memory_chunks ?? cfg.memoryChunks,
793
+ });
794
+ const chunkMask = packVegaChunks(batch.pool, items, batch.L, {
795
+ chunks: cfg.memory_chunks ?? cfg.memoryChunks,
796
+ chunkMinTokens: cfg.memory_min_tokens ?? cfg.memoryMinTokens,
797
+ chunkTokens: cfg.memory_chunk_tokens ?? cfg.memoryChunkTokens,
798
+ nseg: batch.nseg,
799
+ });
800
+
801
+ const tRun = now();
802
+ // Torso: one causal read to the deepest captured layer, all span means
803
+ // (segments + state chunks) pooled in-graph from the one forward.
804
+ const torsoOut = await torso.run({
805
+ input_ids: int64(batch.inputIds, [batch.n, batch.L]),
806
+ attention_mask: int64(batch.attention, [batch.n, batch.L]),
807
+ pool: f32(batch.pool, batch.poolDims),
808
+ });
809
+ const pooled = torsoOut.pooled ?? torsoOut[Object.keys(torsoOut)[0]];
810
+ // pooled: [B, nLayers, nseg + C, d] — nLayers = feature_layers,
811
+ // rows 0..2 = state/question/last, 3..3+K = options, then C chunk rows.
812
+ const pd = pooled.dims;
813
+ const d = pd[3];
814
+ const Lc = pd[1];
815
+ const rowsTotal = pd[2];
816
+ const C = rowsTotal - batch.nseg;
817
+ const pv = pooled.data;
818
+ const feats = new Float32Array(batch.n * Lc * 3 * d);
819
+ const opts = new Float32Array(batch.n * batch.K * d);
820
+ const chunks = new Float32Array(batch.n * C * d);
821
+ for (let b = 0; b < batch.n; b++) {
822
+ for (let l = 0; l < Lc; l++) {
823
+ for (let s = 0; s < 3; s++) {
824
+ const row = s; // pooling rows 0/1/2 are state/question/last
825
+ const src = ((b * Lc + l) * rowsTotal + row) * d;
826
+ feats.set(pv.subarray(src, src + d), ((b * Lc + l) * 3 + s) * d);
827
+ }
828
+ }
829
+ // options from the deepest captured layer only (option_layers)
830
+ for (let k = 0; k < batch.K; k++) {
831
+ const src = ((b * Lc + Lc - 1) * rowsTotal + 3 + k) * d;
832
+ opts.set(pv.subarray(src, src + d), (b * batch.K + k) * d);
833
+ }
834
+ // chunk means: deepest feature layer, rows nseg..nseg + C
835
+ for (let c = 0; c < C; c++) {
836
+ const src = ((b * Lc + Lc - 1) * rowsTotal + batch.nseg + c) * d;
837
+ chunks.set(pv.subarray(src, src + d), (b * C + c) * d);
838
+ }
839
+ }
840
+
841
+ // Engine: landscape + integrator -> raw logits and the physics state.
842
+ const engineOut = await engine.run({
843
+ feats: f32(feats, [batch.n, Lc, 3, d]),
844
+ opts: f32(opts, [batch.n, batch.K, d]),
845
+ opt_mask: bool(batch.optMask, [batch.n, batch.K]),
846
+ qtype: int64(batch.qtypes, [batch.n]),
847
+ chunks: f32(chunks, [batch.n, C, d]),
848
+ chunk_mask: bool(chunkMask, [batch.n, C]),
849
+ });
850
+ const logits = engineOut.logits.data;
851
+ const kinetic = engineOut.kinetic.data;
852
+ const nearest = engineOut.nearest.data;
853
+ const stepsUsed = Array.from(engineOut.steps_used.data, Number);
854
+
855
+ const nOpts = items.map((it) => it.options.length);
856
+ const phys = vegaThermalFeatures({
857
+ kinetic, nearest, stepsUsed, nOpts, maxSteps: cfg.n_steps ?? cfg.nSteps,
858
+ });
859
+ const probs = vegaThermalProbabilities({
860
+ logits, rowWidth: batch.K, qtypes: batch.qtypes,
861
+ phys: phys.flat(), nOpts,
862
+ theta: calibration.theta ?? VEGA_DEFAULT_THETA,
863
+ });
864
+ const runMs = now() - tRun;
865
+ const labels = vegaOptionLabels(normalized);
866
+ const answers = vegaAnswers({
867
+ probs, qtypes: batch.qtypes, labels,
868
+ physics: { nearest, stepsUsed },
869
+ rawLogits: logits, rowWidth: batch.K,
870
+ calibration, alpha, risk: effRisk,
871
+ });
872
+ const decode = (ids) => {
873
+ try {
874
+ return tok.decode(Array.from(ids), { skip_special_tokens: false });
875
+ } catch {
876
+ return Array.from(ids).join(" ");
877
+ }
878
+ };
879
+ return {
880
+ answers,
881
+ truncated: false,
882
+ length: Number(batch.attention.reduce((a, v) => a + v, 0n)),
883
+ prompts: items.map((it) => decode(it.ids)),
884
+ timings: { totalMs: Math.round(now() - t0), runMs: Math.round(runMs) },
885
+ };
886
+ },
887
+ async dispose() {
888
+ try { await torso.release?.(); } catch { /* optional */ }
889
+ try { await engine.release?.(); } catch { /* optional */ }
890
+ },
891
+ };
892
+ }
893
+
894
+ /**
895
+ * The split export (nvkudva/vega-onnx-q8): an 8-bit embedding graph, a cut
896
+ * decoder that stops after layer 19 and emits the residual stream at layers
897
+ * 13/19 (the LM head and layers 20-23 never run), and the fp32 engine with
898
+ * both gated task adapters. JS pools the hidden states, each question is
899
+ * calibrated with the theta block of the adapter its gate routes it to, and
900
+ * the conformal / abstain / unbound machinery reads that block's thresholds.
901
+ *
902
+ * Text only in v1: the vision encoder (image as the state) is not wired in.
903
+ * The 8-bit graphs are wasm-only (MatMulNBits has no WebGPU kernel), so
904
+ * device "auto" resolves to wasm.
905
+ */
906
+ async function createVegaSplitDecider({
907
+ embedFile = VEGA_Q8_EMBED_FILE,
908
+ decoderFile = VEGA_Q8_DECODER_FILE,
909
+ engineFile = VEGA_Q8_ENGINE_FILE,
910
+ calibrationModel = null,
911
+ calibrationRevision = null,
912
+ ort = null,
913
+ transformers = null,
914
+ fetchImpl = fetch,
915
+ wasmPaths = null,
916
+ device = "auto",
917
+ sessionFactory = null,
918
+ onProgress = null,
919
+ maxLen = null,
920
+ alpha = VEGA_DEFAULTS.alpha,
921
+ risk = null,
922
+ } = {}, { model, revision, subfolder }) {
923
+ const ortMod = ort ?? (await import("onnxruntime-web"));
924
+ const tf = transformers ?? (await import("@huggingface/transformers"));
925
+ if (wasmPaths) {
926
+ try { ortMod.env.wasm.wasmPaths = wasmPaths; } catch { /* older runtime */ }
927
+ }
928
+ const resolvedDevice = device !== "auto" ? device : "wasm";
929
+ const executionProviders = resolvedDevice === "webgpu" ? ["webgpu", "wasm"] : ["wasm"];
930
+
931
+ const basePath = subfolder ? `${subfolder}/` : "";
932
+ const configRepo = calibrationModel ?? model;
933
+ const configRevision = calibrationRevision ?? (calibrationModel ? "main" : revision);
934
+ const configUrl = hfUrl(configRepo, configRevision, `${basePath}vega_web.json`);
935
+ let config = null;
936
+ let configSource = null;
937
+ try {
938
+ const res = await fetchImpl(configUrl);
939
+ if (res?.ok) {
940
+ config = await res.json();
941
+ configSource = configUrl;
942
+ }
943
+ } catch { /* fall through to the shipped defaults */ }
944
+ const cfg = { ...VEGA_DEFAULTS, ...(config ?? {}) };
945
+ const calibration = config?.calibration ?? {};
946
+ const adapters = Array.isArray(config?.adapters) ? config.adapters : [];
947
+ const effRisk = risk ?? calibration.default_risk ?? cfg.default_risk ?? VEGA_DEFAULTS.risk;
948
+ const cfgMaxLen = config?.max_len ?? cfg.maxLen;
949
+ const effMaxLen = Number.isFinite(maxLen) && maxLen > 0 ? maxLen : cfgMaxLen;
950
+
951
+ // The tokenizer (Qwen3.5) ships inside the version folder, and
952
+ // transformers.js's AutoTokenizer has no subfolder option here, so build
953
+ // PreTrainedTokenizer from the two files directly -- what the export's own
954
+ // runtime does. Older transformers falls back to AutoTokenizer.
955
+ const fetchJson = async (file) => {
956
+ const url = hfUrl(configRepo, configRevision, `${basePath}${file}`);
957
+ const res = await fetchImpl(url);
958
+ if (!res?.ok) throw new Error(`vega: ${url} not found (HTTP ${res?.status ?? "no response"})`);
959
+ return res.json();
960
+ };
961
+ let tok;
962
+ if (typeof tf.PreTrainedTokenizer === "function") {
963
+ const [tokenizerJson, tokenizerConfig] = await Promise.all([
964
+ fetchJson("tokenizer.json"), fetchJson("tokenizer_config.json"),
965
+ ]);
966
+ tok = new tf.PreTrainedTokenizer(tokenizerJson, tokenizerConfig);
967
+ } else {
968
+ tok = await tf.AutoTokenizer.from_pretrained(configRepo, { revision: configRevision });
969
+ }
970
+ const encode = (text, addSpecial = false) => {
971
+ if (typeof tok.encode === "function") {
972
+ const ids = tok.encode(text, { add_special_tokens: addSpecial });
973
+ return Array.isArray(ids) ? ids : Array.from(ids?.input_ids?.data ?? ids, Number);
974
+ }
975
+ return Array.from(tok(text, { add_special_tokens: addSpecial }).input_ids.data, Number);
976
+ };
977
+ const decode = (ids) => {
978
+ try {
979
+ return tok.decode(Array.from(ids), { skip_special_tokens: false });
980
+ } catch {
981
+ return Array.from(ids).join(" ");
982
+ }
983
+ };
984
+
985
+ const open = openVegaSessions({
986
+ ortMod, fetchImpl, onProgress, sessionFactory, executionProviders,
987
+ device: resolvedDevice,
988
+ });
989
+ const embed = await open(hfUrl(model, revision, `${basePath}${embedFile}`));
990
+ const decoder = await open(hfUrl(model, revision, `${basePath}${decoderFile}`));
991
+ const engine = await open(hfUrl(model, revision, `${basePath}${engineFile}`));
992
+ const past = emptyVegaPast(decoder, ortMod.Tensor);
993
+
994
+ const int64 = (arr, dims) => new ortMod.Tensor("int64", arr, dims);
995
+ const f32 = (arr, dims) => new ortMod.Tensor("float32", arr, dims);
996
+ const bool = (arr, dims) => new ortMod.Tensor("bool", arr, dims);
997
+
998
+ return {
999
+ info: {
1000
+ family: "vega",
1001
+ format: "split",
1002
+ model,
1003
+ revision,
1004
+ subfolder: subfolder ?? null,
1005
+ device: resolvedDevice,
1006
+ maxLen: effMaxLen,
1007
+ alpha: String(alpha),
1008
+ risk: String(effRisk),
1009
+ calibrationSource: configSource,
1010
+ nSteps: cfg.n_steps ?? cfg.nSteps,
1011
+ adapters: adapters.map((a) => a.name),
1012
+ config,
1013
+ },
1014
+ async decide(state, questions) {
1015
+ const t0 = now();
1016
+ const normalized = normalizeQuestions(questions);
1017
+ const qdefs = normalized.map(vegaQuestionDef);
1018
+ const stateText = renderVegaText(state);
1019
+ const C = Number(cfg.memory_chunks ?? VEGA_DEFAULTS.memoryChunks);
1020
+ const chunkMinTokens = cfg.memory_min_tokens ?? VEGA_DEFAULTS.memoryMinTokens;
1021
+ const chunkTokens = cfg.memory_chunk_tokens ?? VEGA_DEFAULTS.memoryChunkTokens;
1022
+
1023
+ const items = qdefs.map((qdef) => {
1024
+ const { ids, spans } = buildVegaPrompt({
1025
+ encode, stateText, qdef, maxLen: effMaxLen,
1026
+ bosId: null, // the reference adds no BOS for Qwen, and the export never does
1027
+ queryFirstMaxState: cfg.query_first_max_state ?? cfg.queryFirstMaxState,
1028
+ optMaxTokens: cfg.opt_max_tokens ?? cfg.optMaxTokens,
1029
+ insMaxTokens: 256,
1030
+ truncateState: false, // the export refuses a state that does not fit
1031
+ });
1032
+ return { ids, spans, qtype: VEGA_QTYPE[qdef.t], options: vegaOptionTexts(qdef) };
1033
+ });
1034
+
1035
+ // One decoder pass per question (the export's own reading), pooling in JS.
1036
+ const tRun = now();
1037
+ const rows = [];
1038
+ const prompts = [];
1039
+ for (const it of items) {
1040
+ const L = it.ids.length;
1041
+ const embedOut = await embed.run({
1042
+ input_ids: int64(BigInt64Array.from(it.ids, BigInt), [1, L]),
1043
+ });
1044
+ const embeds = embedOut.inputs_embeds.data;
1045
+ const d = embeds.length / L;
1046
+ const decOut = await decoder.run({
1047
+ ...past,
1048
+ inputs_embeds: f32(embeds, [1, L, d]),
1049
+ attention_mask: int64(new BigInt64Array(L).fill(1n), [1, L]),
1050
+ position_ids: int64(vegaTextPositions(L), [3, 1, L]),
1051
+ });
1052
+ const pooled = poolVegaHidden({
1053
+ h13: decOut.h13.data, h19: decOut.h19.data, spans: it.spans, d,
1054
+ chunks: C, chunkMinTokens, chunkTokens,
1055
+ });
1056
+ const K = it.options.length;
1057
+ const eng = await engine.run({
1058
+ feats: f32(pooled.feats, [1, 2, 3, d]),
1059
+ opts: f32(pooled.opts, [1, K, 1, d]),
1060
+ opt_mask: bool(new Uint8Array(K).fill(1), [1, K]),
1061
+ qtype: int64([BigInt(it.qtype)], [1]),
1062
+ chunks: f32(pooled.chunks, [1, C, d]),
1063
+ chunk_mask: bool(pooled.cmask, [1, C]),
1064
+ });
1065
+ rows.push({
1066
+ qtype: it.qtype,
1067
+ K,
1068
+ logits: Array.from(eng.logits.data).slice(0, K),
1069
+ kinetic: eng.kinetic.data[0],
1070
+ nearest: eng.nearest.data[0],
1071
+ stepsUsed: Number(eng.steps_used.data[0]),
1072
+ gates: Array.from(eng.gates.data),
1073
+ });
1074
+ prompts.push(decode(it.ids));
1075
+ }
1076
+
1077
+ const nOpts = rows.map((r) => r.K);
1078
+ const rowWidth = Math.max(...nOpts);
1079
+ const logits = new Float32Array(rows.length * rowWidth);
1080
+ rows.forEach((r, b) => logits.set(r.logits, b * rowWidth));
1081
+ const phys = vegaThermalFeatures({
1082
+ kinetic: rows.map((r) => r.kinetic),
1083
+ nearest: rows.map((r) => r.nearest),
1084
+ stepsUsed: rows.map((r) => r.stepsUsed),
1085
+ nOpts, maxSteps: cfg.n_steps ?? cfg.nSteps,
1086
+ });
1087
+ // Each question is calibrated by the adapter its gate routes it to
1088
+ // (route_adapters: the most open gate, if it reaches the threshold).
1089
+ const gateThreshold = adapters[0]?.gate_threshold ?? 0.5;
1090
+ const gates = rows.map((r) => r.gates);
1091
+ const routes = gates.map((g) => vegaRoute(g, gateThreshold));
1092
+ const calibrations = routes.map(
1093
+ (rt) => (rt >= 0 ? adapters[rt]?.calibration : null) ?? calibration);
1094
+ const thetaByRow = calibrations.map(
1095
+ (cal, b) => cal?.theta?.[rows[b].qtype] ?? VEGA_DEFAULT_THETA[rows[b].qtype]);
1096
+ const probs = vegaThermalProbabilities({
1097
+ logits, rowWidth, qtypes: rows.map((r) => r.qtype),
1098
+ phys: phys.flat(), nOpts, thetaByRow,
1099
+ });
1100
+ const runMs = now() - tRun;
1101
+ const answers = vegaAnswers({
1102
+ probs, qtypes: rows.map((r) => r.qtype), labels: vegaOptionLabels(normalized),
1103
+ physics: {
1104
+ nearest: rows.map((r) => r.nearest),
1105
+ stepsUsed: rows.map((r) => r.stepsUsed),
1106
+ },
1107
+ rawLogits: logits, rowWidth,
1108
+ calibration, calibrations, alpha, risk: effRisk,
1109
+ adapterNames: adapters.map((a) => a.name), gates, routes,
1110
+ });
1111
+ return {
1112
+ answers,
1113
+ truncated: false,
1114
+ length: items.reduce((n, it) => n + it.ids.length, 0),
1115
+ prompts,
1116
+ timings: { totalMs: Math.round(now() - t0), runMs: Math.round(runMs) },
1117
+ };
1118
+ },
1119
+ async dispose() {
1120
+ try { await embed.release?.(); } catch { /* optional */ }
1121
+ try { await decoder.release?.(); } catch { /* optional */ }
1122
+ try { await engine.release?.(); } catch { /* optional */ }
1123
+ },
1124
+ };
1125
+ }