jev-web 0.8.0 → 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/README.md +180 -32
- package/docs/demo-model-picker.png +0 -0
- package/package.json +9 -5
- package/src/clef.mjs +498 -0
- package/src/decision3.mjs +571 -0
- package/src/index.mjs +59 -0
- package/src/questions.mjs +8 -1
- package/src/registry.mjs +34 -4
- package/src/vega.mjs +539 -50
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
|
+
}
|