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/clef.mjs ADDED
@@ -0,0 +1,498 @@
1
+ /**
2
+ * clef.mjs — FluidInference clef-text-0.6b family (Qwen3 torso + Cloudflare's
3
+ * joint schema head).
4
+ *
5
+ * clef-text-0.6b is a 0.70B text decision model distilled from Cloudflare's
6
+ * clef-flash 9B: a Qwen3-0.6B torso (LoRA merged) plus Clef's joint schema
7
+ * head — one pass that decides every field of a record jointly. The
8
+ * checkpoint ships Cloudflare's unchanged reference module
9
+ * (`joint_schema_model.py`, Apache-2.0) and speaks the same Jev/SystemOne
10
+ * contract as every other family here: one probability per option of every
11
+ * typed question, one forward pass.
12
+ *
13
+ * record (built by encodeClefRecord — mirrors encode_record):
14
+ * <|im_start|>system\nRead the complete state and schema. Decide every field
15
+ * jointly. Each answer must be exactly one of that field's allowed options.
16
+ * <|im_end|>\n<|im_start|>user\nSTATE:\n<state>
17
+ * \n\nSCHEMA FIELDS:\n
18
+ * \nFIELD 1\nID: <id>\nTYPE: <type>\nINSTRUCTION: <instructions>
19
+ * \nALLOWED OPTIONS:\nOPTION 1: {"description": …, "option_id": "…"}\n…END FIELD\n
20
+ * \n<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n
21
+ * JOINT SCHEMA DECISIONS:
22
+ * head: span means of the normalized hidden states (question + option
23
+ * spans), span means of the tied output embedding (the option lexical
24
+ * prior), a cross-option evidence pass over the record's options
25
+ * attending to the whole sequence, per-question routing summaries, and a
26
+ * transformer-decoder field pass — logits per option of every question.
27
+ * answers: softmax per question; choice/score/noul with the reference
28
+ * SystemOne shapes (choice/score confidence is the top probability, score
29
+ * carries its legend).
30
+ *
31
+ * One fused ONNX graph ships: the head's evidence and field passes attend to
32
+ * the full hidden sequence, so the torso never leaves the graph. Span means
33
+ * travel as pooling matrices (the Vega trick), so no hidden states cross into
34
+ * JS.
35
+ */
36
+ import { normalizeQuestions } from "./questions.mjs";
37
+ import { softmaxWithTemperature } from "./answers.mjs";
38
+ import { fetchCachedBytes, fetchOnnxExternalData } from "./laya.mjs";
39
+
40
+ export const CLEF_DEFAULT_MODEL = "warsang/clef-text-0.6b-web";
41
+ // Pinned to the uploaded browser export (export/clef/04_upload.py).
42
+ export const CLEF_DEFAULT_REVISION = "d01e9cfd11e66d593e9c517bb2bc204ffbf67623";
43
+ export const CLEF_DEFAULT_FILE = "onnx/clef_text_fp16.onnx";
44
+
45
+ /** Reference record window (joint_schema_model.encode_record default). */
46
+ export const CLEF_DEFAULT_MAX_LEN = 16384;
47
+
48
+ export const CLEF_SYSTEM_PROMPT =
49
+ "Read the complete state and schema. Decide every field jointly. Each answer "
50
+ + "must be exactly one of that field's allowed options.";
51
+
52
+ /** Reference QUESTION_TYPES: noul 0, choice 1, score 2. */
53
+ export const CLEF_QTYPE = { noul: 0, choice: 1, score: 2 };
54
+ export const CLEF_QTYPE_NAMES = ["noul", "choice", "score"];
55
+
56
+ /** Fixed noul slot descriptions (overridable through question criteria). */
57
+ export const CLEF_NOUL_DESCRIPTIONS = {
58
+ true: "The proposition is true or the answer is yes.",
59
+ false: "The proposition is false or the answer is no.",
60
+ };
61
+
62
+ export const CLEF_DEFAULTS = {
63
+ maxLen: CLEF_DEFAULT_MAX_LEN,
64
+ maxStateTokens: null,
65
+ };
66
+
67
+ const CLEF_PREFIX = (system) =>
68
+ `<|im_start|>system\n${system}<|im_end|>\n<|im_start|>user\nSTATE:\n`;
69
+ const CLEF_SUFFIX =
70
+ "\n<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\nJOINT SCHEMA DECISIONS:";
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Protocol: content rendering, question options, record encoding
74
+ // ---------------------------------------------------------------------------
75
+
76
+ /**
77
+ * Reference render(): strings pass through; everything else is compact JSON
78
+ * with sorted keys (json.dumps(ensure_ascii=False, separators=(",", ":"),
79
+ * sort_keys=True)).
80
+ */
81
+ export function renderClefContent(content) {
82
+ if (typeof content === "string") return content;
83
+ return JSON.stringify(sortKeysDeep(content));
84
+ }
85
+
86
+ /**
87
+ * Code-point key order — Python's sorted() on str compares code points; the
88
+ * default JS sort compares UTF-16 code units and disagrees past BMP.
89
+ */
90
+ function compareCodePoints(a, b) {
91
+ const x = Array.from(a);
92
+ const y = Array.from(b);
93
+ for (let i = 0; i < Math.min(x.length, y.length); i++) {
94
+ const cx = x[i].codePointAt(0);
95
+ const cy = y[i].codePointAt(0);
96
+ if (cx !== cy) return cx - cy;
97
+ }
98
+ return x.length - y.length;
99
+ }
100
+
101
+ function sortKeysDeep(value) {
102
+ if (Array.isArray(value)) return value.map(sortKeysDeep);
103
+ if (value !== null && typeof value === "object") {
104
+ return Object.fromEntries(
105
+ Object.keys(value)
106
+ .sort(compareCodePoints)
107
+ .map((k) => [k, sortKeysDeep(value[k])]),
108
+ );
109
+ }
110
+ return value;
111
+ }
112
+
113
+ /**
114
+ * Reference question_options(): the option keys and their descriptions, in
115
+ * the order the prompt and the logits use.
116
+ *
117
+ * - noul: fixed keys true/false (default descriptions, criteria overrides
118
+ * accepted under either {true,false} or {yes,no});
119
+ * - choice: one entry per question option, sorted by key (the reference sorts
120
+ * the criteria mapping);
121
+ * - score: numeric keys 0..n-1 over the ordered levels.
122
+ *
123
+ * @returns {{keys:string[], descriptions:any[]}}
124
+ */
125
+ export function clefQuestionOptions(question) {
126
+ if (question.type === "noul") {
127
+ const c = question.criteria ?? {};
128
+ const trueDesc = c.true ?? c.yes ?? CLEF_NOUL_DESCRIPTIONS.true;
129
+ const falseDesc = c.false ?? c.no ?? CLEF_NOUL_DESCRIPTIONS.false;
130
+ return { keys: ["true", "false"], descriptions: [trueDesc, falseDesc] };
131
+ }
132
+ if (question.type === "choice") {
133
+ const descriptions = question.criteria ?? {};
134
+ const entries = question.options
135
+ .map((label) => [String(label), descriptions[label]]);
136
+ entries.sort((a, b) => compareCodePoints(a[0], b[0]));
137
+ return {
138
+ keys: entries.map(([k]) => k),
139
+ descriptions: entries.map(([, d]) => (d === null || d === undefined ? null : d)),
140
+ };
141
+ }
142
+ if (question.type === "score") {
143
+ return {
144
+ keys: question.options.map((_, i) => String(i)),
145
+ descriptions: question.options,
146
+ };
147
+ }
148
+ throw new TypeError(`clef: unsupported question type ${question.type}`);
149
+ }
150
+
151
+ /**
152
+ * Encode one record exactly like the reference encode_record: every piece is
153
+ * tokenized separately, the state is truncated (head kept) to fit the window,
154
+ * and the question/option token spans are recorded (they become the pooling
155
+ * matrices the graph consumes).
156
+ *
157
+ * @param {{encode:(text:string)=>number[], stateText:string, questions:object[],
158
+ * maxLength?:number, maxStateTokens?:number|null}} spec
159
+ * @returns {{ids:number[], questions:Array<{id:string, type:number,
160
+ * questionSpan:[number,number], optionSpans:[number,number][], optionIds:string[]}>,
161
+ * length:number, truncated:boolean}}
162
+ */
163
+ export function encodeClefRecord({
164
+ encode,
165
+ stateText,
166
+ questions,
167
+ maxLength = CLEF_DEFAULT_MAX_LEN,
168
+ maxStateTokens = null,
169
+ }) {
170
+ const schema = [];
171
+ const encoded = [];
172
+ questions.forEach((q, i) => {
173
+ const id = q.id ?? String(i);
174
+ const instructions = q.instructions === null || q.instructions === undefined || q.instructions === ""
175
+ ? String(id)
176
+ : q.instructions;
177
+ const { keys, descriptions } = clefQuestionOptions(q);
178
+ schema.push(...encode(`\nFIELD ${i + 1}\nID: ${id}\nTYPE: ${q.type}\nINSTRUCTION: `));
179
+ const qStart = schema.length;
180
+ schema.push(...encode(renderClefContent(instructions)));
181
+ const qEnd = schema.length;
182
+ schema.push(...encode("\nALLOWED OPTIONS:\n"));
183
+ const optionSpans = [];
184
+ keys.forEach((key, j) => {
185
+ schema.push(...encode(`OPTION ${j + 1}: `));
186
+ const oStart = schema.length;
187
+ const semantics = { option_id: key };
188
+ const d = descriptions[j];
189
+ if (d !== null && d !== undefined) semantics.description = d;
190
+ schema.push(...encode(renderClefContent(semantics)));
191
+ optionSpans.push([oStart, schema.length]);
192
+ schema.push(...encode("\n"));
193
+ });
194
+ schema.push(...encode("END FIELD\n"));
195
+ encoded.push({
196
+ id: String(id),
197
+ type: CLEF_QTYPE[q.type],
198
+ instructions,
199
+ optionIds: keys,
200
+ questionSpan: [qStart, qEnd],
201
+ optionSpans,
202
+ });
203
+ });
204
+ if (encoded.length === 0) throw new TypeError("clef: the record has no questions");
205
+
206
+ const shortState = "\n\nSCHEMA FIELDS:\n";
207
+ const prefix = encode(CLEF_PREFIX(CLEF_SYSTEM_PROMPT));
208
+ const suffix = encode(CLEF_SUFFIX);
209
+ const schemaHead = encode(shortState);
210
+ const prefixSchema = [...schemaHead, ...schema];
211
+
212
+ let stateIds = encode(renderClefContent(stateText ?? ""));
213
+ const fullStateLength = stateIds.length;
214
+ if (maxStateTokens !== null && maxStateTokens !== undefined) {
215
+ stateIds = stateIds.slice(0, maxStateTokens);
216
+ }
217
+ const fixedLength = prefix.length + prefixSchema.length + suffix.length;
218
+ if (fixedLength > maxLength) {
219
+ throw new RangeError(
220
+ `clef: schema requires ${fixedLength} tokens before the state; maximum is ${maxLength}`);
221
+ }
222
+ stateIds = stateIds.slice(0, maxLength - fixedLength);
223
+
224
+ const offset = prefix.length + stateIds.length + schemaHead.length;
225
+ const ids = [...prefix, ...stateIds, ...prefixSchema, ...suffix];
226
+ return {
227
+ ids,
228
+ questions: encoded.map((q) => ({
229
+ ...q,
230
+ questionSpan: [q.questionSpan[0] + offset, q.questionSpan[1] + offset],
231
+ optionSpans: q.optionSpans.map(([a, e]) => [a + offset, e + offset]),
232
+ })),
233
+ length: ids.length,
234
+ truncated: stateIds.length < fullStateLength,
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Span means for the graph's pooling matrices.
240
+ *
241
+ * Option slots are question-major: slot = q*K + k. Invalid slots (a question
242
+ * with fewer than K options) keep a zero pooling row and optMask[s] = 0; the
243
+ * graph excludes them from routing and the JS ignores their logits.
244
+ *
245
+ * @returns {{Q:number, K:number, S:number, L:number, poolGlobal:Float32Array,
246
+ * poolQuestion:Float32Array, poolOption:Float32Array, slotQuestion:number[],
247
+ * typeIds:number[], optMask:Uint8Array, counts:number[]}}
248
+ */
249
+ export function packClefBatch(record) {
250
+ const Q = record.questions.length;
251
+ const K = Math.max(...record.questions.map((q) => q.optionIds.length));
252
+ const S = Q * K;
253
+ const L = record.ids.length;
254
+ const poolGlobal = new Float32Array(L);
255
+ const poolQuestion = new Float32Array(Q * L);
256
+ const poolOption = new Float32Array(S * L);
257
+ const slotQuestion = new Array(S);
258
+ const typeIds = new Array(Q);
259
+ const optMask = new Uint8Array(S);
260
+ const counts = new Array(Q);
261
+ const setSpan = (view, row, [a, e]) => {
262
+ if (e <= a) return;
263
+ const w = 1 / (e - a);
264
+ for (let t = a; t < e; t++) view[row * L + t] = w;
265
+ };
266
+ poolGlobal[L - 1] = 1; // the final token (the record's global vector)
267
+ for (let q = 0; q < Q; q++) {
268
+ const item = record.questions[q];
269
+ typeIds[q] = item.type;
270
+ counts[q] = item.optionIds.length;
271
+ setSpan(poolQuestion, q, item.questionSpan);
272
+ for (let k = 0; k < K; k++) {
273
+ const s = q * K + k;
274
+ slotQuestion[s] = q;
275
+ if (k >= item.optionSpans.length) continue;
276
+ setSpan(poolOption, s, item.optionSpans[k]);
277
+ optMask[s] = 1;
278
+ }
279
+ }
280
+ return {
281
+ Q, K, S, L,
282
+ poolGlobal, poolQuestion, poolOption,
283
+ slotQuestion, typeIds, optMask, counts,
284
+ };
285
+ }
286
+
287
+ /**
288
+ * jev-web answers from per-question option probabilities.
289
+ *
290
+ * @param {{probs:number[][], questions:object[]}} spec probs are in
291
+ * clefQuestionOptions() key order, sliced to each question's options.
292
+ */
293
+ export function clefAnswers({ probs, questions }) {
294
+ return questions.map((q, i) => {
295
+ const p = probs[i];
296
+ if (q.type === "choice") {
297
+ const { keys } = clefQuestionOptions(q);
298
+ const best = p.indexOf(Math.max(...p));
299
+ const choice = keys[best];
300
+ const index = q.options.indexOf(choice);
301
+ return {
302
+ type: "choice",
303
+ choice,
304
+ index: index === -1 ? best : index,
305
+ probabilities: Object.fromEntries(
306
+ q.options.map((label) => [label, p[keys.indexOf(String(label))]])),
307
+ confidence: Math.max(...p),
308
+ };
309
+ }
310
+ if (q.type === "noul") {
311
+ const pTrue = p[0]; // keys are ["true", "false"]
312
+ const pFalse = p[1];
313
+ const [noLabel, yesLabel] = q.options;
314
+ return {
315
+ type: "noul",
316
+ noul: pTrue,
317
+ probabilities: { [noLabel]: pFalse, [yesLabel]: pTrue },
318
+ confidence: Math.max(pTrue, pFalse),
319
+ };
320
+ }
321
+ // score: keys are 0..n-1
322
+ const values = q.options.map((_, j) => j);
323
+ return {
324
+ type: "score",
325
+ score: p.reduce((acc, pj, j) => acc + pj * values[j], 0),
326
+ level: p.indexOf(Math.max(...p)),
327
+ legend: Object.fromEntries(q.options.map((level, j) => [String(j), level])),
328
+ probabilities: Object.fromEntries(p.map((pj, j) => [String(j), pj])),
329
+ confidence: Math.max(...p),
330
+ };
331
+ });
332
+ }
333
+
334
+ // ---------------------------------------------------------------------------
335
+ // The decider
336
+ // ---------------------------------------------------------------------------
337
+
338
+ const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
339
+
340
+ const hfUrl = (model, revision, file) =>
341
+ `https://huggingface.co/${model}/resolve/${revision ? `${revision}/` : ""}${file}`;
342
+
343
+ /**
344
+ * Create a clef decider.
345
+ *
346
+ * @param {object} [opts]
347
+ * model/revision — browser export repo (fused graph, tokenizer,
348
+ * clef_config.json)
349
+ * file — graph path within the repo
350
+ * tokenizerModel — repo to read the tokenizer from (default: model)
351
+ * configUrl — override for clef_config.json
352
+ * maxLen — record window; null = the checkpoint's max_len
353
+ * maxStateTokens — cap on the tokenized state (default: none)
354
+ * ort, transformers, fetchImpl, wasmPaths, device, sessionFactory,
355
+ * onProgress, scope — as in the other families
356
+ */
357
+ export async function createClefDecider({
358
+ model = CLEF_DEFAULT_MODEL,
359
+ revision = CLEF_DEFAULT_REVISION,
360
+ file = CLEF_DEFAULT_FILE,
361
+ tokenizerModel = null,
362
+ tokenizerRevision = null,
363
+ configUrl = null,
364
+ ort = null,
365
+ transformers = null,
366
+ fetchImpl = fetch,
367
+ wasmPaths = null,
368
+ device = "auto",
369
+ sessionFactory = null,
370
+ onProgress = null,
371
+ maxLen = null,
372
+ maxStateTokens = CLEF_DEFAULTS.maxStateTokens,
373
+ scope = globalThis,
374
+ } = {}) {
375
+ const ortMod = ort ?? (await import("onnxruntime-web"));
376
+ const tf = transformers ?? (await import("@huggingface/transformers"));
377
+ if (wasmPaths) {
378
+ try { ortMod.env.wasm.wasmPaths = wasmPaths; } catch { /* older runtime */ }
379
+ }
380
+ const resolvedDevice = device !== "auto"
381
+ ? device
382
+ : (scope?.navigator?.gpu ? "webgpu" : "wasm");
383
+ const executionProviders = resolvedDevice === "webgpu" ? ["webgpu", "wasm"] : ["wasm"];
384
+
385
+ // The export repo's clef_config.json carries the window and the head shape
386
+ // (head weights are baked into the graph; the config is informational).
387
+ const url = configUrl ?? hfUrl(model, revision, "clef_config.json");
388
+ let config = null;
389
+ let configSource = null;
390
+ try {
391
+ const res = await fetchImpl(url);
392
+ if (res?.ok) {
393
+ config = await res.json();
394
+ configSource = url;
395
+ }
396
+ } catch { /* fall through to the shipped defaults */ }
397
+ const cfg = { ...CLEF_DEFAULTS, ...(config ?? {}) };
398
+ const effMaxLen = Number.isFinite(maxLen) && maxLen > 0 ? maxLen : cfg.maxLen;
399
+
400
+ const tok = await tf.AutoTokenizer.from_pretrained(tokenizerModel ?? model, {
401
+ revision: tokenizerRevision ?? (tokenizerModel ? "main" : revision),
402
+ progress_callback: () => {},
403
+ });
404
+ const encode = (text) =>
405
+ Array.from(tok(text, { add_special_tokens: false }).input_ids.data, Number);
406
+
407
+ const createSession = sessionFactory
408
+ ?? (async (u, opts) => {
409
+ const bytes = await fetchCachedBytes(u, {
410
+ fetchImpl, onProgress: (p) => onProgress?.(p), persist: true,
411
+ });
412
+ const external = await fetchOnnxExternalData(u, { fetchImpl });
413
+ const sessionOpts = external ? { ...opts, externalData: [external] } : opts;
414
+ return ortMod.InferenceSession.create(bytes, sessionOpts);
415
+ });
416
+ let session;
417
+ try {
418
+ session = await createSession(hfUrl(model, revision, file), {
419
+ executionProviders, graphOptimizationLevel: "all",
420
+ });
421
+ } catch (e) {
422
+ if (resolvedDevice !== "webgpu") throw e;
423
+ onProgress?.({ phase: `webgpu unavailable (${e?.message ?? e}) — retrying on wasm` });
424
+ session = await createSession(hfUrl(model, revision, file),
425
+ { executionProviders: ["wasm"], graphOptimizationLevel: "all" });
426
+ }
427
+
428
+ const int64 = (arr, dims) => new ortMod.Tensor("int64", BigInt64Array.from(arr, BigInt), dims);
429
+ const f32 = (arr, dims) => new ortMod.Tensor("float32", arr, dims);
430
+ const bool = (arr, dims) => new ortMod.Tensor("bool", Uint8Array.from(arr), dims);
431
+
432
+ return {
433
+ info: {
434
+ family: "clef",
435
+ model,
436
+ revision,
437
+ device: resolvedDevice,
438
+ maxLen: effMaxLen,
439
+ maxStateTokens: maxStateTokens ?? cfg.max_state_tokens ?? null,
440
+ head: config?.head ?? null,
441
+ configSource,
442
+ },
443
+ async decide(state, questions) {
444
+ const t0 = now();
445
+ const normalized = normalizeQuestions(questions);
446
+ const stateText = renderClefContent(state ?? "");
447
+ const record = encodeClefRecord({
448
+ encode, stateText, questions: normalized,
449
+ maxLength: effMaxLen,
450
+ maxStateTokens: maxStateTokens ?? cfg.max_state_tokens ?? null,
451
+ });
452
+ const batch = packClefBatch(record);
453
+
454
+ const tRun = now();
455
+ const out = await session.run({
456
+ input_ids: int64(record.ids, [1, batch.L]),
457
+ attention_mask: int64(record.ids.map(() => 1), [1, batch.L]),
458
+ pool_global: f32(batch.poolGlobal, [1, 1, batch.L]),
459
+ pool_question: f32(batch.poolQuestion, [1, batch.Q, batch.L]),
460
+ pool_option: f32(batch.poolOption, [1, batch.S, batch.L]),
461
+ type_ids: int64(batch.typeIds, [batch.Q]),
462
+ slot_question: int64(batch.slotQuestion, [batch.S]),
463
+ opt_mask: bool(batch.optMask, [batch.S]),
464
+ });
465
+ const runMs = now() - tRun;
466
+ const logits = out.logits ?? out[Object.keys(out)[0]];
467
+ const dims = logits.dims;
468
+ if (dims.length !== 2 || dims[0] !== 1 || dims[1] !== batch.S) {
469
+ throw new Error(
470
+ `clef: invalid logits shape [${dims}] for ${batch.Q} questions with ${batch.S} option slots`);
471
+ }
472
+ const probs = [];
473
+ for (let q = 0; q < batch.Q; q++) {
474
+ const row = Array.from(
475
+ { length: batch.counts[q] },
476
+ (_, k) => Number(logits.data[q * batch.K + k]));
477
+ probs.push(softmaxWithTemperature(row, 1));
478
+ }
479
+ const answers = clefAnswers({ probs, questions: normalized });
480
+ let prompt;
481
+ try {
482
+ prompt = tok.decode(record.ids, { skip_special_tokens: false });
483
+ } catch {
484
+ prompt = record.ids.join(" ");
485
+ }
486
+ return {
487
+ answers,
488
+ truncated: record.truncated,
489
+ length: record.length,
490
+ prompts: [prompt],
491
+ timings: { totalMs: Math.round(now() - t0), runMs: Math.round(runMs) },
492
+ };
493
+ },
494
+ async dispose() {
495
+ try { await session.release?.(); } catch { /* optional */ }
496
+ },
497
+ };
498
+ }
package/src/config.mjs CHANGED
@@ -1,65 +1,65 @@
1
- // jev-web — model defaults, backend resolution, and model-side config.
2
- //
3
- // Defaults point at the reference ONNX export of com-kotobalabs/open-jev-
4
- // deberta-v3-large (Apache-2.0), pinned by revision for reproducibility. Any
5
- // repo with the same typed-decision graph layout works: pass `model` and
6
- // optionally `revision`.
7
-
8
- export const DEFAULT_MODEL = "onnx-community/open-jev-deberta-v3-large-ONNX";
9
- export const DEFAULT_REVISION = "7c79f25b5ac496089f448a969c801872ad59d31c";
10
- export const DEFAULT_TEMPERATURE = 1.05;
11
- export const DEFAULT_MARKERS = ["[STATE]", "[Q]", "[OPT]"];
12
- export const DEFAULT_MAX_STATE_TOKENS = 256;
13
- export const DEFAULT_MAX_LEN = 512;
14
-
15
- const HF_BASE = "https://huggingface.co";
16
-
17
- export function resolveDevice({ device = "auto", webgpu = false } = {}) {
18
- if (device && device !== "auto") return device;
19
- return webgpu ? "webgpu" : "wasm";
20
- }
21
-
22
- // Quantized WebGPU kernels are f16-friendly (q4f16); WASM wants the
23
- // non-f16 q4 build. fp32/fp16/q8 stay explicit opt-ins.
24
- export function resolveDtype(device, dtype = "auto") {
25
- if (dtype && dtype !== "auto") return dtype;
26
- return device === "webgpu" ? "q4f16" : "q4";
27
- }
28
-
29
- export async function detectWebGPU(scope = globalThis) {
30
- try {
31
- if (!scope?.navigator?.gpu?.requestAdapter) return false;
32
- return !!(await scope.navigator.gpu.requestAdapter());
33
- } catch {
34
- return false;
35
- }
36
- }
37
-
38
- // Read the model's own calibration/marker config when the repo ships one
39
- // (open_jev_config.json), else fall back to the published reference values.
40
- export async function loadModelConfig({ model = DEFAULT_MODEL, revision = DEFAULT_REVISION, fetchImpl = fetch } = {}) {
41
- const fallback = {
42
- temperature: DEFAULT_TEMPERATURE,
43
- markers: [...DEFAULT_MARKERS],
44
- maxStateTokens: DEFAULT_MAX_STATE_TOKENS,
45
- maxLen: DEFAULT_MAX_LEN,
46
- pool: "span",
47
- source: "defaults",
48
- };
49
- const url = `${HF_BASE}/${model}/resolve/${revision ? `${revision}/` : ""}open_jev_config.json`;
50
- try {
51
- const res = await fetchImpl(url, { cache: "no-store" });
52
- if (!res.ok) return fallback;
53
- const cfg = await res.json();
54
- return {
55
- temperature: Number.isFinite(Number(cfg.temperature)) ? Number(cfg.temperature) : fallback.temperature,
56
- markers: Array.isArray(cfg.markers) && cfg.markers.length === 3 ? cfg.markers.map(String) : fallback.markers,
57
- maxStateTokens: Number.isFinite(Number(cfg.max_state_tokens)) ? Number(cfg.max_state_tokens) : fallback.maxStateTokens,
58
- maxLen: Number.isFinite(Number(cfg.max_len)) ? Number(cfg.max_len) : fallback.maxLen,
59
- pool: typeof cfg.pool === "string" ? cfg.pool : fallback.pool,
60
- source: "model",
61
- };
62
- } catch {
63
- return fallback;
64
- }
65
- }
1
+ // jev-web — model defaults, backend resolution, and model-side config.
2
+ //
3
+ // Defaults point at the reference ONNX export of com-kotobalabs/open-jev-
4
+ // deberta-v3-large (Apache-2.0), pinned by revision for reproducibility. Any
5
+ // repo with the same typed-decision graph layout works: pass `model` and
6
+ // optionally `revision`.
7
+
8
+ export const DEFAULT_MODEL = "onnx-community/open-jev-deberta-v3-large-ONNX";
9
+ export const DEFAULT_REVISION = "7c79f25b5ac496089f448a969c801872ad59d31c";
10
+ export const DEFAULT_TEMPERATURE = 1.05;
11
+ export const DEFAULT_MARKERS = ["[STATE]", "[Q]", "[OPT]"];
12
+ export const DEFAULT_MAX_STATE_TOKENS = 256;
13
+ export const DEFAULT_MAX_LEN = 512;
14
+
15
+ const HF_BASE = "https://huggingface.co";
16
+
17
+ export function resolveDevice({ device = "auto", webgpu = false } = {}) {
18
+ if (device && device !== "auto") return device;
19
+ return webgpu ? "webgpu" : "wasm";
20
+ }
21
+
22
+ // Quantized WebGPU kernels are f16-friendly (q4f16); WASM wants the
23
+ // non-f16 q4 build. fp32/fp16/q8 stay explicit opt-ins.
24
+ export function resolveDtype(device, dtype = "auto") {
25
+ if (dtype && dtype !== "auto") return dtype;
26
+ return device === "webgpu" ? "q4f16" : "q4";
27
+ }
28
+
29
+ export async function detectWebGPU(scope = globalThis) {
30
+ try {
31
+ if (!scope?.navigator?.gpu?.requestAdapter) return false;
32
+ return !!(await scope.navigator.gpu.requestAdapter());
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ // Read the model's own calibration/marker config when the repo ships one
39
+ // (open_jev_config.json), else fall back to the published reference values.
40
+ export async function loadModelConfig({ model = DEFAULT_MODEL, revision = DEFAULT_REVISION, fetchImpl = fetch } = {}) {
41
+ const fallback = {
42
+ temperature: DEFAULT_TEMPERATURE,
43
+ markers: [...DEFAULT_MARKERS],
44
+ maxStateTokens: DEFAULT_MAX_STATE_TOKENS,
45
+ maxLen: DEFAULT_MAX_LEN,
46
+ pool: "span",
47
+ source: "defaults",
48
+ };
49
+ const url = `${HF_BASE}/${model}/resolve/${revision ? `${revision}/` : ""}open_jev_config.json`;
50
+ try {
51
+ const res = await fetchImpl(url, { cache: "no-store" });
52
+ if (!res.ok) return fallback;
53
+ const cfg = await res.json();
54
+ return {
55
+ temperature: Number.isFinite(Number(cfg.temperature)) ? Number(cfg.temperature) : fallback.temperature,
56
+ markers: Array.isArray(cfg.markers) && cfg.markers.length === 3 ? cfg.markers.map(String) : fallback.markers,
57
+ maxStateTokens: Number.isFinite(Number(cfg.max_state_tokens)) ? Number(cfg.max_state_tokens) : fallback.maxStateTokens,
58
+ maxLen: Number.isFinite(Number(cfg.max_len)) ? Number(cfg.max_len) : fallback.maxLen,
59
+ pool: typeof cfg.pool === "string" ? cfg.pool : fallback.pool,
60
+ source: "model",
61
+ };
62
+ } catch {
63
+ return fallback;
64
+ }
65
+ }