edgeinfer 0.0.0-stage → 2.0.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/events.ts ADDED
@@ -0,0 +1,17 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024-2026 Soumya Debnath
3
+
4
+ export class EventEmitter {
5
+ private listeners: Record<string, Function[]> = {};
6
+
7
+ on(event: string, callback: Function) {
8
+ if (!this.listeners[event]) this.listeners[event] = [];
9
+ this.listeners[event].push(callback);
10
+ }
11
+
12
+ emit(event: string, ...args: any[]) {
13
+ if (this.listeners[event]) {
14
+ this.listeners[event].forEach(cb => cb(...args));
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,35 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024-2026 Soumya Debnath
3
+
4
+ export class ImageProcessor {
5
+ static imageDataToFloat32Array(
6
+ imageData: ImageData,
7
+ options: { normalize?: boolean; layout?: 'NCHW' | 'NHWC' } = {}
8
+ ): Float32Array {
9
+ const { normalize = true, layout = 'NCHW' } = options;
10
+ const { width, height, data } = imageData;
11
+ const size = width * height;
12
+ const float32Data = new Float32Array(size * 3);
13
+
14
+ for (let i = 0; i < size; i++) {
15
+ const r = data[i * 4];
16
+ const g = data[i * 4 + 1];
17
+ const b = data[i * 4 + 2];
18
+
19
+ const nr = normalize ? r / 255 : r;
20
+ const ng = normalize ? g / 255 : g;
21
+ const nb = normalize ? b / 255 : b;
22
+
23
+ if (layout === 'NCHW') {
24
+ float32Data[i] = nr;
25
+ float32Data[size + i] = ng;
26
+ float32Data[2 * size + i] = nb;
27
+ } else {
28
+ float32Data[i * 3] = nr;
29
+ float32Data[i * 3 + 1] = ng;
30
+ float32Data[i * 3 + 2] = nb;
31
+ }
32
+ }
33
+ return float32Data;
34
+ }
35
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024-2026 Soumya Debnath
3
+
4
+ export * from './types';
5
+ export * from './events';
6
+ export * from './model-cache';
7
+ export * from './runtime-manager';
8
+ export * from './tokenizer';
9
+ export * from './image-processor';
10
+ export * from './edgeinfer';
11
+
12
+ export type { DownloadOptions } from './download';
13
+ export * from './matryoshka';
14
+ export * from './speculative';
15
+ export * from './onnx-lm';
@@ -0,0 +1,441 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024-2026 Soumya Debnath
3
+ //
4
+ // Browser-local two-stage Matryoshka retrieval.
5
+ // Stage 1: INT8 truncated vectors (default 64 dims) over the full index.
6
+ // Stage 2: FP32 re-ranking of the top candidates at the full dimension.
7
+
8
+ export interface VectorRecord {
9
+ id: string;
10
+ text?: string;
11
+ embedding: Float32Array;
12
+ metadata?: Record<string, unknown>;
13
+ }
14
+
15
+ export interface SearchResult {
16
+ id: string;
17
+ text?: string;
18
+ metadata?: Record<string, unknown>;
19
+ score: number;
20
+ coarseScore: number;
21
+ }
22
+
23
+ export interface VectorStore {
24
+ add(records: VectorRecord[]): Promise<void>;
25
+ getAll(): Promise<VectorRecord[]>;
26
+ clear(): Promise<void>;
27
+ count(): Promise<number>;
28
+ close?(): void;
29
+ }
30
+
31
+ export interface MatryoshkaOptions {
32
+ dimensions?: number;
33
+ coarseDimension?: number;
34
+ refineTopK?: number;
35
+ store?: VectorStore;
36
+ }
37
+
38
+ export interface SearchOptions {
39
+ topK?: number;
40
+ refineTopK?: number;
41
+ coarseDimension?: number;
42
+ refine?: boolean;
43
+ }
44
+
45
+ export function l2Normalize(vector: Float32Array, dimension = vector.length): Float32Array {
46
+ const output = new Float32Array(dimension);
47
+ if (dimension > vector.length) {
48
+ throw new Error(`Cannot normalize ${dimension} dims from a ${vector.length}-dim vector`);
49
+ }
50
+ let norm = 0;
51
+ for (let i = 0; i < dimension; i++) norm += vector[i] * vector[i];
52
+ norm = Math.sqrt(norm);
53
+ if (norm === 0) return output;
54
+ for (let i = 0; i < dimension; i++) output[i] = vector[i] / norm;
55
+ return output;
56
+ }
57
+
58
+ export function truncateAndNormalize(vector: Float32Array, dimension: number): Float32Array {
59
+ if (!Number.isInteger(dimension) || dimension < 1) {
60
+ throw new Error("dimension must be a positive integer");
61
+ }
62
+ return l2Normalize(vector, dimension);
63
+ }
64
+
65
+ export function quantizeInt8(
66
+ vector: Float32Array,
67
+ dimension = vector.length
68
+ ): { data: Int8Array; scale: number } {
69
+ let maxAbs = 0;
70
+ for (let i = 0; i < dimension; i++) {
71
+ const value = Math.abs(vector[i]);
72
+ if (value > maxAbs) maxAbs = value;
73
+ }
74
+ const scale = maxAbs > 0 ? maxAbs / 127 : 1;
75
+ const data = new Int8Array(dimension);
76
+ for (let i = 0; i < dimension; i++) {
77
+ const value = Math.round(vector[i] / scale);
78
+ data[i] = value > 127 ? 127 : value < -127 ? -127 : value;
79
+ }
80
+ return { data, scale };
81
+ }
82
+
83
+ export function dequantizeInt8(data: Int8Array, scale: number): Float32Array {
84
+ const output = new Float32Array(data.length);
85
+ for (let i = 0; i < data.length; i++) output[i] = data[i] * scale;
86
+ return output;
87
+ }
88
+
89
+ function dot(a: Float32Array | Int8Array, b: Float32Array | Int8Array, dimension: number): number {
90
+ let sum = 0;
91
+ for (let i = 0; i < dimension; i++) sum += a[i] * b[i];
92
+ return sum;
93
+ }
94
+
95
+ class TopKHeap {
96
+ private items: Array<{ index: number; score: number }> = [];
97
+
98
+ constructor(private readonly k: number) {
99
+ if (k < 1) throw new Error("top-k must be positive");
100
+ }
101
+
102
+ push(index: number, score: number): void {
103
+ if (this.items.length < this.k) {
104
+ this.items.push({ index, score });
105
+ if (this.items.length === this.k) this.heapify();
106
+ return;
107
+ }
108
+ if (score > this.items[0].score) {
109
+ this.items[0] = { index, score };
110
+ this.siftDown(0);
111
+ }
112
+ }
113
+
114
+ private heapify(): void {
115
+ for (let i = Math.floor(this.items.length / 2) - 1; i >= 0; i--) this.siftDown(i);
116
+ }
117
+
118
+ private siftDown(start: number): void {
119
+ let index = start;
120
+ const length = this.items.length;
121
+ for (;;) {
122
+ const left = index * 2 + 1;
123
+ const right = left + 1;
124
+ let smallest = index;
125
+ if (left < length && this.items[left].score < this.items[smallest].score) smallest = left;
126
+ if (right < length && this.items[right].score < this.items[smallest].score) smallest = right;
127
+ if (smallest === index) break;
128
+ [this.items[index], this.items[smallest]] = [this.items[smallest], this.items[index]];
129
+ index = smallest;
130
+ }
131
+ }
132
+
133
+ values(): Array<{ index: number; score: number }> {
134
+ return [...this.items].sort((a, b) => b.score - a.score);
135
+ }
136
+ }
137
+
138
+ export class MemoryVectorStore implements VectorStore {
139
+ private records: VectorRecord[] = [];
140
+
141
+ async add(records: VectorRecord[]): Promise<void> {
142
+ this.records.push(...records);
143
+ }
144
+
145
+ async getAll(): Promise<VectorRecord[]> {
146
+ return this.records.slice();
147
+ }
148
+
149
+ async clear(): Promise<void> {
150
+ this.records = [];
151
+ }
152
+
153
+ async count(): Promise<number> {
154
+ return this.records.length;
155
+ }
156
+ }
157
+
158
+ interface StoredVectorRecord {
159
+ id: string;
160
+ text?: string;
161
+ metadata?: Record<string, unknown>;
162
+ embedding: ArrayBuffer;
163
+ }
164
+
165
+ export class IndexedDbVectorStore implements VectorStore {
166
+ private dbPromise: Promise<IDBDatabase> | null = null;
167
+
168
+ constructor(
169
+ private readonly dbName = "edgeinfer-matryoshka",
170
+ private readonly storeName = "vectors"
171
+ ) {}
172
+
173
+ private open(): Promise<IDBDatabase> {
174
+ if (this.dbPromise) return this.dbPromise;
175
+ if (typeof indexedDB === "undefined") {
176
+ return Promise.reject(new Error("IndexedDB is unavailable in this environment"));
177
+ }
178
+ this.dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
179
+ const request = indexedDB.open(this.dbName, 1);
180
+ request.onupgradeneeded = () => {
181
+ const db = request.result;
182
+ if (!db.objectStoreNames.contains(this.storeName)) {
183
+ db.createObjectStore(this.storeName, { keyPath: "id" });
184
+ }
185
+ };
186
+ request.onsuccess = () => resolve(request.result);
187
+ request.onerror = () => reject(request.error ?? new Error("IndexedDB open failed"));
188
+ });
189
+ return this.dbPromise;
190
+ }
191
+
192
+ async add(records: VectorRecord[]): Promise<void> {
193
+ const db = await this.open();
194
+ await new Promise<void>((resolve, reject) => {
195
+ const transaction = db.transaction(this.storeName, "readwrite");
196
+ const store = transaction.objectStore(this.storeName);
197
+ for (const record of records) {
198
+ const stored: StoredVectorRecord = {
199
+ id: record.id,
200
+ text: record.text,
201
+ metadata: record.metadata,
202
+ embedding: record.embedding.slice().buffer,
203
+ };
204
+ store.put(stored);
205
+ }
206
+ transaction.oncomplete = () => resolve();
207
+ transaction.onerror = () => reject(transaction.error ?? new Error("IndexedDB write failed"));
208
+ transaction.onabort = () => reject(transaction.error ?? new Error("IndexedDB write aborted"));
209
+ });
210
+ }
211
+
212
+ async getAll(): Promise<VectorRecord[]> {
213
+ const db = await this.open();
214
+ return new Promise<VectorRecord[]>((resolve, reject) => {
215
+ const transaction = db.transaction(this.storeName, "readonly");
216
+ const store = transaction.objectStore(this.storeName);
217
+ const request = store.getAll();
218
+ request.onsuccess = () => {
219
+ const records = (request.result as StoredVectorRecord[]).map((stored) => ({
220
+ id: stored.id,
221
+ text: stored.text,
222
+ metadata: stored.metadata,
223
+ embedding: new Float32Array(stored.embedding),
224
+ }));
225
+ resolve(records);
226
+ };
227
+ request.onerror = () => reject(request.error ?? new Error("IndexedDB read failed"));
228
+ });
229
+ }
230
+
231
+ async clear(): Promise<void> {
232
+ const db = await this.open();
233
+ await new Promise<void>((resolve, reject) => {
234
+ const transaction = db.transaction(this.storeName, "readwrite");
235
+ transaction.objectStore(this.storeName).clear();
236
+ transaction.oncomplete = () => resolve();
237
+ transaction.onerror = () => reject(transaction.error ?? new Error("IndexedDB clear failed"));
238
+ });
239
+ }
240
+
241
+ async count(): Promise<number> {
242
+ const db = await this.open();
243
+ return new Promise<number>((resolve, reject) => {
244
+ const transaction = db.transaction(this.storeName, "readonly");
245
+ const request = transaction.objectStore(this.storeName).count();
246
+ request.onsuccess = () => resolve(request.result);
247
+ request.onerror = () => reject(request.error ?? new Error("IndexedDB count failed"));
248
+ });
249
+ }
250
+
251
+ close(): void {
252
+ void this.dbPromise?.then((db) => db.close());
253
+ this.dbPromise = null;
254
+ }
255
+ }
256
+
257
+ export class MatryoshkaIndex {
258
+ readonly dimensions: number;
259
+ readonly coarseDimension: number;
260
+ readonly refineTopK: number;
261
+ private store?: VectorStore;
262
+ private ids: string[] = [];
263
+ private texts: Array<string | undefined> = [];
264
+ private metadata: Array<Record<string, unknown> | undefined> = [];
265
+ private full: Float32Array = new Float32Array(0);
266
+ private coarse: Int8Array = new Int8Array(0);
267
+ private scales: Float32Array = new Float32Array(0);
268
+ private size = 0;
269
+
270
+ constructor(options: MatryoshkaOptions = {}) {
271
+ this.dimensions = options.dimensions ?? 768;
272
+ this.coarseDimension = options.coarseDimension ?? 64;
273
+ this.refineTopK = options.refineTopK ?? 50;
274
+ this.store = options.store;
275
+ if (this.coarseDimension > this.dimensions) {
276
+ throw new Error("coarseDimension cannot exceed dimensions");
277
+ }
278
+ }
279
+
280
+ get count(): number {
281
+ return this.size;
282
+ }
283
+
284
+ async build(records: VectorRecord[], { persist = true } = {}): Promise<void> {
285
+ if (records.length === 0) throw new Error("At least one record is required");
286
+ for (const record of records) {
287
+ if (record.embedding.length !== this.dimensions) {
288
+ throw new Error(
289
+ `Record ${record.id} has ${record.embedding.length} dims; expected ${this.dimensions}`
290
+ );
291
+ }
292
+ }
293
+
294
+ this.ids = new Array(records.length);
295
+ this.texts = new Array(records.length);
296
+ this.metadata = new Array(records.length);
297
+ this.full = new Float32Array(records.length * this.dimensions);
298
+ this.coarse = new Int8Array(records.length * this.coarseDimension);
299
+ this.scales = new Float32Array(records.length);
300
+ this.size = records.length;
301
+
302
+ for (let row = 0; row < records.length; row++) {
303
+ const record = records[row];
304
+ const normalized = l2Normalize(record.embedding, this.dimensions);
305
+ this.ids[row] = record.id;
306
+ this.texts[row] = record.text;
307
+ this.metadata[row] = record.metadata;
308
+ this.full.set(normalized, row * this.dimensions);
309
+
310
+ const quantized = quantizeInt8(normalized, this.coarseDimension);
311
+ this.coarse.set(quantized.data, row * this.coarseDimension);
312
+ this.scales[row] = quantized.scale;
313
+ }
314
+
315
+ if (persist && this.store) {
316
+ await this.store.clear();
317
+ const normalizedRecords = records.map((record, row) => ({
318
+ ...record,
319
+ embedding: this.full.slice(row * this.dimensions, (row + 1) * this.dimensions),
320
+ }));
321
+ await this.store.add(normalizedRecords);
322
+ }
323
+ }
324
+
325
+ static async load(store: VectorStore, options: MatryoshkaOptions = {}): Promise<MatryoshkaIndex> {
326
+ const index = new MatryoshkaIndex({ ...options, store });
327
+ const records = await store.getAll();
328
+ await index.build(records, { persist: false });
329
+ return index;
330
+ }
331
+
332
+ search(query: Float32Array, options: SearchOptions = {}): SearchResult[] {
333
+ if (this.size === 0) throw new Error("Index is empty; call build() first");
334
+ const topK = options.topK ?? 10;
335
+ const refine = options.refine ?? true;
336
+ const coarseDimension = options.coarseDimension ?? this.coarseDimension;
337
+ if (coarseDimension > this.coarseDimension) {
338
+ throw new Error(
339
+ `coarseDimension ${coarseDimension} exceeds the indexed stride ${this.coarseDimension}`
340
+ );
341
+ }
342
+ const queryFull = l2Normalize(query, this.dimensions);
343
+ const queryCoarse = quantizeInt8(queryFull, coarseDimension);
344
+ const refineTopK = Math.max(
345
+ topK,
346
+ Math.min(this.refineTopK, options.refineTopK ?? this.refineTopK, this.size)
347
+ );
348
+
349
+ const heap = new TopKHeap(refineTopK);
350
+ for (let row = 0; row < this.size; row++) {
351
+ const offset = row * this.coarseDimension;
352
+ let sum = 0;
353
+ for (let i = 0; i < coarseDimension; i++) {
354
+ sum += queryCoarse.data[i] * this.coarse[offset + i];
355
+ }
356
+ heap.push(row, sum * this.scales[row]);
357
+ }
358
+
359
+ let candidates: Array<{ index: number; score: number; coarseScore: number }> =
360
+ heap.values().map((candidate) => ({ ...candidate, coarseScore: candidate.score }));
361
+ if (refine) {
362
+ candidates = candidates
363
+ .map((candidate) => ({
364
+ ...candidate,
365
+ score: dot(
366
+ queryFull,
367
+ this.full.subarray(
368
+ candidate.index * this.dimensions,
369
+ (candidate.index + 1) * this.dimensions
370
+ ),
371
+ this.dimensions
372
+ ),
373
+ }))
374
+ .sort((a, b) => b.score - a.score);
375
+ }
376
+
377
+ return candidates.slice(0, topK).map((candidate) => ({
378
+ id: this.ids[candidate.index],
379
+ text: this.texts[candidate.index],
380
+ metadata: this.metadata[candidate.index],
381
+ score: candidate.score,
382
+ coarseScore: candidate.coarseScore,
383
+ }));
384
+ }
385
+
386
+ exactSearchAtDimension(
387
+ query: Float32Array,
388
+ dimension: number,
389
+ topK = 10
390
+ ): Array<{ index: number; score: number }> {
391
+ if (dimension > this.dimensions) throw new Error("dimension exceeds index dimensions");
392
+ const queryNormalized = truncateAndNormalize(query, dimension);
393
+ const scored: Array<{ index: number; score: number }> = [];
394
+ for (let row = 0; row < this.size; row++) {
395
+ const vector = truncateAndNormalize(
396
+ this.full.subarray(row * this.dimensions, (row + 1) * this.dimensions),
397
+ dimension
398
+ );
399
+ scored.push({ index: row, score: dot(queryNormalized, vector, dimension) });
400
+ }
401
+ return scored.sort((a, b) => b.score - a.score).slice(0, topK);
402
+ }
403
+
404
+ recallAtK(
405
+ queries: Float32Array[],
406
+ groundTruth: string[][],
407
+ dimension: number,
408
+ topK = 10
409
+ ): number {
410
+ let hits = 0;
411
+ let total = 0;
412
+ for (let q = 0; q < queries.length; q++) {
413
+ const expected = new Set(groundTruth[q].slice(0, topK));
414
+ const candidates = this.exactSearchAtDimension(queries[q], dimension, topK);
415
+ for (const candidate of candidates) {
416
+ if (expected.has(this.ids[candidate.index])) hits++;
417
+ }
418
+ total += Math.min(topK, expected.size);
419
+ }
420
+ return total === 0 ? 0 : hits / total;
421
+ }
422
+
423
+ get idList(): readonly string[] {
424
+ return this.ids;
425
+ }
426
+ }
427
+
428
+ export type EmbedFn = (text: string) => Promise<Float32Array> | Float32Array;
429
+
430
+ export async function embedMatryoshka(
431
+ text: string,
432
+ dimension: number,
433
+ embed: EmbedFn
434
+ ): Promise<Float32Array> {
435
+ const full = await embed(text);
436
+ return truncateAndNormalize(full, dimension);
437
+ }
438
+
439
+ export function createMatryoshkaEmbedder(embed: EmbedFn) {
440
+ return (text: string, dimension = 768) => embedMatryoshka(text, dimension, embed);
441
+ }
@@ -0,0 +1,46 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024-2026 Soumya Debnath
3
+
4
+ import { EventEmitter } from './events';
5
+ import { withDownloadDeadline, DownloadOptions, checkDownloadAbort } from './download';
6
+
7
+ export class ModelCache extends EventEmitter {
8
+ private cacheName: string;
9
+
10
+ constructor(cacheName: string = 'edgeinfer-models') {
11
+ super();
12
+ this.cacheName = cacheName;
13
+ }
14
+
15
+ async getModel(url: string, forceDownload: boolean = false, options: DownloadOptions = {}): Promise<ArrayBuffer> {
16
+ return withDownloadDeadline(async signal => {
17
+ const cache = typeof caches === 'undefined' ? undefined : await caches.open(this.cacheName);
18
+ checkDownloadAbort(signal);
19
+ const cached = !forceDownload && cache ? await cache.match(url) : undefined;
20
+ checkDownloadAbort(signal);
21
+ if (cached) {
22
+ const buffer = await cached.arrayBuffer();
23
+ checkDownloadAbort(signal);
24
+ return buffer;
25
+ }
26
+ this.emit('downloadStart', url);
27
+ checkDownloadAbort(signal);
28
+ const response = await fetch(url, { signal });
29
+ if (!response.ok) {
30
+ await response.body?.cancel();
31
+ throw new Error(`EdgeInfer: failed to fetch model from ${url} - HTTP ${response.status} ${response.statusText}`);
32
+ }
33
+ if (response.status === 206 || response.headers.has('content-range')) {
34
+ await response.body?.cancel();
35
+ throw new Error('EdgeInfer: partial model responses are not supported');
36
+ }
37
+ // Read completely before committing, so aborted/truncated bodies cannot be cached.
38
+ const buffer = await response.arrayBuffer();
39
+ checkDownloadAbort(signal);
40
+ if (cache) await cache.put(url, new Response(buffer, { headers: response.headers }));
41
+ checkDownloadAbort(signal);
42
+ this.emit('downloadComplete', url);
43
+ return buffer;
44
+ }, options);
45
+ }
46
+ }
package/src/onnx-lm.ts ADDED
@@ -0,0 +1,104 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024-2026 Soumya Debnath
3
+ //
4
+ // ONNX Runtime Web adapter for causal language models. This adapter recomputes
5
+ // the full token sequence on each call, so it is correctness-first and works
6
+ // with standard exports that accept input_ids/attention_mask and return logits.
7
+ // Cache-aware adapters can implement LanguageModel.truncate and store KV tensors
8
+ // in ModelState.cache without changing the speculative decoder.
9
+
10
+ import * as ort from 'onnxruntime-web';
11
+ import { LanguageModel, ModelState, DecodeResult } from './speculative';
12
+
13
+ export interface OnnxCausalOptions {
14
+ inputIdsName?: string;
15
+ attentionMaskName?: string;
16
+ positionIdsName?: string;
17
+ logitsName?: string;
18
+ }
19
+
20
+ export class OnnxCausalLM implements LanguageModel {
21
+ readonly vocabSize: number;
22
+ private readonly inputIdsName: string;
23
+ private readonly attentionMaskName: string;
24
+ private readonly positionIdsName?: string;
25
+ private readonly logitsName: string;
26
+
27
+ constructor(
28
+ private readonly session: ort.InferenceSession,
29
+ vocabSize: number,
30
+ options: OnnxCausalOptions = {}
31
+ ) {
32
+ if (!Number.isInteger(vocabSize) || vocabSize < 1) {
33
+ throw new Error('vocabSize must be a positive integer');
34
+ }
35
+ this.vocabSize = vocabSize;
36
+ this.inputIdsName = options.inputIdsName ?? 'input_ids';
37
+ this.attentionMaskName = options.attentionMaskName ?? 'attention_mask';
38
+ this.positionIdsName = options.positionIdsName;
39
+ this.logitsName = options.logitsName ?? 'logits';
40
+ }
41
+
42
+ async prefill(tokens: number[]): Promise<ModelState> {
43
+ if (tokens.length === 0) throw new Error('prefill requires a non-empty prompt');
44
+ return { tokens: tokens.slice() };
45
+ }
46
+
47
+ async decode(state: ModelState, tokens: number[]): Promise<DecodeResult> {
48
+ const all = [...state.tokens, ...tokens];
49
+ if (all.length === 0) throw new Error('decode requires at least one token');
50
+ if (state.tokens.length === 0) throw new Error('decode requires a prefilled state');
51
+
52
+ const feeds: Record<string, ort.Tensor> = {
53
+ [this.inputIdsName]: new ort.Tensor(
54
+ 'int64',
55
+ BigInt64Array.from(all, (token) => BigInt(token)),
56
+ [1, all.length]
57
+ ),
58
+ [this.attentionMaskName]: new ort.Tensor(
59
+ 'int64',
60
+ BigInt64Array.from({ length: all.length }, () => 1n),
61
+ [1, all.length]
62
+ ),
63
+ };
64
+ if (this.positionIdsName) {
65
+ feeds[this.positionIdsName] = new ort.Tensor(
66
+ 'int64',
67
+ BigInt64Array.from(all, (_, index) => BigInt(index)),
68
+ [1, all.length]
69
+ );
70
+ }
71
+
72
+ const outputs = await this.session.run(feeds);
73
+ const tensor = outputs[this.logitsName];
74
+ if (!tensor) {
75
+ throw new Error(`ONNX model did not return "${this.logitsName}"`);
76
+ }
77
+ const dims = tensor.dims;
78
+ const vocab = dims[dims.length - 1];
79
+ if (vocab !== this.vocabSize) {
80
+ throw new Error(
81
+ `ONNX logits vocabulary ${vocab} does not match configured ${this.vocabSize}`
82
+ );
83
+ }
84
+ const data = tensor.data as Float32Array;
85
+ const seq = all.length;
86
+ const expected = seq * vocab;
87
+ if (data.length < expected) {
88
+ throw new Error(`ONNX logits tensor is too small (${data.length} < ${expected})`);
89
+ }
90
+
91
+ const start = state.tokens.length - 1;
92
+ const logits: Float32Array[] = [];
93
+ for (let i = 0; i <= tokens.length; i++) {
94
+ const position = start + i;
95
+ const offset = dims.length === 3 ? position * vocab : position * vocab;
96
+ logits.push(new Float32Array(data.subarray(offset, offset + vocab)));
97
+ }
98
+ return { state: { tokens: all }, logits };
99
+ }
100
+
101
+ truncate(state: ModelState, length: number): ModelState {
102
+ return { tokens: state.tokens.slice(0, length) };
103
+ }
104
+ }