@adhd/sox-embedding-provider 0.1.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.
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Shared worker thread for @huggingface/transformers-based ONNX inference —
3
+ * cross-encoder rerank and NLI verify.
4
+ *
5
+ * Runs in isolation from the main thread (BL-11 boundary) — onnxruntime-node's
6
+ * thread pool never shares a thread context with better-sqlite3 + sqlite-vec.
7
+ *
8
+ * Supports two operation types:
9
+ * 1. Cross-encoder rerank — 'init' (type: 'rerank'), 'rerank', 'rerankBatch'
10
+ * 2. NLI verification — 'init' (type: 'verify'), 'verify'
11
+ *
12
+ * Protocol:
13
+ * request: { id, type: 'init', type: 'rerank', modelId: string }
14
+ * request: { id, type: 'init', type: 'verify', modelId: string, modelVersion: string }
15
+ * request: { id, type: 'rerank', query: string, candidates: Array<{id, text}> }
16
+ * request: { id, type: 'rerankBatch', queries: string[], candidateSets: ... }
17
+ * request: { id, type: 'verify', jobId: string, claimText: string, sourceText: string }
18
+ * response: { id, initOk: true, dim }
19
+ * response: { id, scores: number[] }
20
+ * response: { id, allScores: number[][] }
21
+ * response: { id, result: { entailment, confidence, ... } }
22
+ * response: { id, error: string }
23
+ * internal: { __shutdown: true }
24
+ *
25
+ * ── BL-238/BL-171 ── This worker is the ONE place cross-encoder rerank and
26
+ * NLI verify (both `@huggingface/transformers`, onnxruntime-node@1.24.3) run,
27
+ * loaded into exactly ONE process-wide `worker_threads.Worker` (constructed
28
+ * exclusively by `sharedOnnxWorker.ts`'s `getSharedOnnxWorker()` singleton —
29
+ * never directly by `@adhd/sox-hybrid-search`'s cross-encoder or
30
+ * `@adhd/sox-claim-verification`'s worker proxy).
31
+ *
32
+ * Root cause #1 (cross-isolate, whole-process fatal — the reason there must
33
+ * be only ONE onnxruntime-bearing `worker_threads.Worker`, proven via a
34
+ * from-scratch minimal repro, no test harness, no mocks): onnxruntime-node's
35
+ * native N-API addon fatally crashes the ENTIRE process — not just the
36
+ * offending worker — with
37
+ *
38
+ * FATAL ERROR: HandleScope::HandleScope Entering the V8 API without
39
+ * proper locking in place
40
+ * ... Napi::FunctionReference::New(...)
41
+ * ... OrtValueToNapiValue(Napi::Env, Ort::Value&&)
42
+ * ... InferenceSessionWrap::Run(...)
43
+ *
44
+ * whenever 2+ *separate* `worker_threads.Worker` instances (i.e. 2+ separate
45
+ * V8 isolates) each hold an active onnxruntime-node `InferenceSession` and
46
+ * run inference concurrently — reproduced even with TWO workers using the
47
+ * exact SAME onnxruntime-node version, so this is a genuine thread-safety
48
+ * limitation of the addon itself, not an ABI/version-mismatch issue (see root
49
+ * `BACKLOG.md` BL-238 for the full repro matrix).
50
+ *
51
+ * fastembed (onnxruntime-node@1.21.0) is DELIBERATELY NOT hosted in this
52
+ * worker, for a SECOND, independent reason (root cause #2): even a single
53
+ * shared worker hosting BOTH onnxruntime-node@1.21.0 (fastembed) AND
54
+ * onnxruntime-node@1.24.3 (transformers) — loaded strictly sequentially, with
55
+ * every JS `await` fully resolved before the next `init` begins (proven via
56
+ * instrumented tracing showing zero JS-level overlap between the two
57
+ * `init`s) — still deterministically threw `std::bad_alloc` the moment
58
+ * fastembed initialised second. That means the two onnxruntime-node major
59
+ * versions leave lingering native state (e.g. background native thread-pool
60
+ * teardown) not synchronized by the JS Promise resolving — a hazard below
61
+ * what JS-level scheduling/serialization can observe or prevent. Only a real
62
+ * OS process boundary is proven safe for fastembed; see
63
+ * `fastembedProcessHost.ts` / `sharedFastembedProcess.ts` for where fastembed
64
+ * actually runs (its own dedicated child PROCESS, never a
65
+ * `worker_threads.Worker`, never sharing an address space with this worker).
66
+ *
67
+ * Fix: every rerank/verify consumer routes through
68
+ * `getSharedOnnxWorker().request(...)` instead of constructing its own
69
+ * `Worker`; every fastembed consumer routes through
70
+ * `getSharedFastembedProcess().request(...)` instead of constructing its own
71
+ * `Worker`/process. There is never a second onnxruntime-bearing WORKER THREAD
72
+ * alive in the process, and fastembed never shares a thread (or process) with
73
+ * this worker at all — both crash classes above are structurally impossible,
74
+ * not merely statistically less likely.
75
+ */
76
+ import { parentPort } from 'node:worker_threads';
77
+ // ── Rerank (cross-encoder) — real ONNX inference ──────────────────────────────
78
+ //
79
+ // Cross-encoder scoring uses a sequence-classification ONNX model run through
80
+ // @huggingface/transformers (which drives onnxruntime-node under the hood on
81
+ // Node — the same "load ONNX in a worker thread" pattern as fastembed above,
82
+ // BL-11). `modelId` is a logical name resolved to a concrete HuggingFace ONNX
83
+ // repo; unknown ids pass through unchanged so any Xenova-converted
84
+ // cross-encoder repo can be wired directly.
85
+ //
86
+ // Primary: MS-MARCO MiniLM cross-encoder (relevance regression — single
87
+ // logit per query/candidate pair, squashed to [0,1] via sigmoid so "higher
88
+ // score = more relevant" per the CrossEncoder contract).
89
+ const RERANK_MODEL_MAP = {
90
+ MiniCheck: 'Xenova/ms-marco-MiniLM-L-6-v2',
91
+ 'ms-marco-MiniLM-L-6-v2': 'Xenova/ms-marco-MiniLM-L-6-v2',
92
+ 'cross-encoder/ms-marco-MiniLM-L-6-v2': 'Xenova/ms-marco-MiniLM-L-6-v2',
93
+ };
94
+ let _rerankResolvedModelId = '';
95
+ let _rerankTokenizer = null;
96
+ let _rerankModel = null;
97
+ let _rerankLoadPromise = null;
98
+ async function setRerankModel(modelId) {
99
+ const resolved = RERANK_MODEL_MAP[modelId] ?? modelId;
100
+ if (_rerankModel && _rerankResolvedModelId === resolved)
101
+ return;
102
+ if (_rerankLoadPromise && _rerankResolvedModelId === resolved)
103
+ return _rerankLoadPromise;
104
+ _rerankResolvedModelId = resolved;
105
+ _rerankLoadPromise = (async () => {
106
+ const { AutoTokenizer, AutoModelForSequenceClassification } = await import('@huggingface/transformers');
107
+ _rerankTokenizer = await AutoTokenizer.from_pretrained(resolved);
108
+ _rerankModel = (await AutoModelForSequenceClassification.from_pretrained(resolved, {
109
+ dtype: 'q8',
110
+ }));
111
+ })();
112
+ try {
113
+ await _rerankLoadPromise;
114
+ }
115
+ finally {
116
+ _rerankLoadPromise = null;
117
+ }
118
+ }
119
+ async function computeRerankScores(query, candidates) {
120
+ if (!_rerankTokenizer || !_rerankModel) {
121
+ throw new Error('Rerank model not initialized — send init{initType:"rerank"} first');
122
+ }
123
+ if (candidates.length === 0)
124
+ return [];
125
+ const queries = new Array(candidates.length).fill(query);
126
+ const texts = candidates.map((c) => c.text);
127
+ const features = _rerankTokenizer(queries, {
128
+ text_pair: texts,
129
+ padding: true,
130
+ truncation: true,
131
+ });
132
+ const output = (await _rerankModel(features));
133
+ const rows = output.logits.tolist();
134
+ return rows.map((row) => sigmoid(row[0] ?? 0));
135
+ }
136
+ function sigmoid(x) {
137
+ return 1 / (1 + Math.exp(-x));
138
+ }
139
+ // ── Verify (NLI) — real ONNX inference ─────────────────────────────────────────
140
+ //
141
+ // NLI verification uses a 3-way entailment/contradiction/neutral cross-encoder
142
+ // (SPEC accuracy-optimized family: cross-encoder/nli-deberta-v3-*). The
143
+ // premise is the source passage, the hypothesis is the claim. Softmax over
144
+ // the model's own `id2label` (never hardcoded ordering) yields entailment +
145
+ // confidence; `entailment`/`contradiction`/`neutral` are mapped onto the
146
+ // frozen wire vocabulary `'entails'|'contradicts'|'neutral'`.
147
+ const VERIFY_MODEL_MAP = {
148
+ MiniCheck: 'Xenova/nli-deberta-v3-xsmall',
149
+ 'nli-deberta-v3-xsmall': 'Xenova/nli-deberta-v3-xsmall',
150
+ 'cross-encoder/nli-deberta-v3-xsmall': 'Xenova/nli-deberta-v3-xsmall',
151
+ };
152
+ const NLI_LABEL_MAP = {
153
+ entailment: 'entails',
154
+ contradiction: 'contradicts',
155
+ neutral: 'neutral',
156
+ };
157
+ let _verifyResolvedModelId = '';
158
+ let _verifyTokenizer = null;
159
+ let _verifyModel = null;
160
+ let _verifyId2Label = {};
161
+ let _verifyLoadPromise = null;
162
+ async function setVerifyModel(modelId, _modelVersion) {
163
+ const resolved = VERIFY_MODEL_MAP[modelId] ?? modelId;
164
+ if (_verifyModel && _verifyResolvedModelId === resolved)
165
+ return;
166
+ if (_verifyLoadPromise && _verifyResolvedModelId === resolved)
167
+ return _verifyLoadPromise;
168
+ _verifyResolvedModelId = resolved;
169
+ _verifyLoadPromise = (async () => {
170
+ const { AutoTokenizer, AutoModelForSequenceClassification } = await import('@huggingface/transformers');
171
+ _verifyTokenizer = await AutoTokenizer.from_pretrained(resolved);
172
+ const model = await AutoModelForSequenceClassification.from_pretrained(resolved, {
173
+ dtype: 'q8',
174
+ });
175
+ _verifyModel = model;
176
+ const config = model
177
+ .config;
178
+ _verifyId2Label = { ...(config?.id2label ?? {}) };
179
+ })();
180
+ try {
181
+ await _verifyLoadPromise;
182
+ }
183
+ finally {
184
+ _verifyLoadPromise = null;
185
+ }
186
+ }
187
+ async function computeVerification(claimText, sourceText) {
188
+ const start = Date.now();
189
+ if (!_verifyTokenizer || !_verifyModel) {
190
+ throw new Error('Verify model not initialized — send init{initType:"verify"} first');
191
+ }
192
+ // Premise = source (what we're checking against), hypothesis = claim.
193
+ const features = _verifyTokenizer([sourceText], {
194
+ text_pair: [claimText],
195
+ padding: true,
196
+ truncation: true,
197
+ });
198
+ const output = (await _verifyModel(features));
199
+ const row = output.logits.tolist()[0] ?? [];
200
+ const probs = softmax(row);
201
+ let bestIdx = 0;
202
+ for (let i = 1; i < probs.length; i++) {
203
+ const candidate = probs[i];
204
+ const current = probs[bestIdx];
205
+ if (candidate !== undefined && (current === undefined || candidate > current))
206
+ bestIdx = i;
207
+ }
208
+ const rawLabel = _verifyId2Label[bestIdx] ?? 'neutral';
209
+ const entailment = NLI_LABEL_MAP[rawLabel] ?? 'neutral';
210
+ const confidence = probs[bestIdx] ?? 0;
211
+ return { entailment, confidence, timingMs: Date.now() - start };
212
+ }
213
+ function softmax(logits) {
214
+ if (logits.length === 0)
215
+ return [];
216
+ const max = Math.max(...logits);
217
+ const exps = logits.map((v) => Math.exp(v - max));
218
+ const sum = exps.reduce((a, b) => a + b, 0);
219
+ return exps.map((v) => v / sum);
220
+ }
221
+ // ── Main message handler ──────────────────────────────────────────────────────
222
+ //
223
+ // Requests are processed by a single, strictly serialized async queue
224
+ // (`_queue`) rather than fired-and-forgotten independently. Rerank and verify
225
+ // share the same onnxruntime-node@1.24.3 addon (proven safe to run
226
+ // concurrently — see BACKLOG.md BL-238 repro (a)/(d): 2x transformers.js
227
+ // workers, and rerank+verify together, both coexist cleanly), so this is
228
+ // defense-in-depth rather than a required fix for THIS worker specifically;
229
+ // it costs nothing (both workloads are CPU-bound single-model calls) and
230
+ // keeps this worker's request handling consistent with
231
+ // `fastembedProcessHost.ts`'s own serialized queue.
232
+ if (!parentPort) {
233
+ throw new Error('embedWorker must be run as a worker_thread, not directly');
234
+ }
235
+ let _queue = Promise.resolve();
236
+ /** Enqueue a request handler so it runs strictly after every previously queued one. */
237
+ function enqueue(task) {
238
+ _queue = _queue.then(task, task);
239
+ }
240
+ async function handleMessage(msg) {
241
+ // ── Init variants ────────────────────────────────────────────────────────────
242
+ if (msg.type === 'init' && msg.initType === 'rerank') {
243
+ try {
244
+ await setRerankModel(msg.modelId);
245
+ parentPort.postMessage({ id: msg.id, initOk: true, dim: 0 });
246
+ }
247
+ catch (e) {
248
+ parentPort.postMessage({
249
+ id: msg.id,
250
+ error: String(e instanceof Error ? e.message : e),
251
+ });
252
+ }
253
+ return;
254
+ }
255
+ if (msg.type === 'init' && msg.initType === 'verify') {
256
+ try {
257
+ await setVerifyModel(msg.modelId, msg.modelVersion);
258
+ parentPort.postMessage({ id: msg.id, initOk: true, dim: 0 });
259
+ }
260
+ catch (e) {
261
+ parentPort.postMessage({
262
+ id: msg.id,
263
+ error: String(e instanceof Error ? e.message : e),
264
+ });
265
+ }
266
+ return;
267
+ }
268
+ // ── Rerank (cross-encoder) ──────────────────────────────────────────────────
269
+ if (msg.type === 'rerank') {
270
+ try {
271
+ const scores = await computeRerankScores(msg.query, msg.candidates);
272
+ parentPort.postMessage({ id: msg.id, scores });
273
+ }
274
+ catch (err) {
275
+ parentPort.postMessage({
276
+ id: msg.id,
277
+ error: String(err instanceof Error ? err.message : err),
278
+ });
279
+ }
280
+ return;
281
+ }
282
+ if (msg.type === 'rerankBatch') {
283
+ try {
284
+ const allScores = await Promise.all(msg.queries.map((q, i) => {
285
+ const set = msg.candidateSets[i];
286
+ if (!set)
287
+ return Promise.resolve([]);
288
+ return computeRerankScores(q, set);
289
+ }));
290
+ parentPort.postMessage({ id: msg.id, allScores });
291
+ }
292
+ catch (err) {
293
+ parentPort.postMessage({
294
+ id: msg.id,
295
+ error: String(err instanceof Error ? err.message : err),
296
+ });
297
+ }
298
+ return;
299
+ }
300
+ // ── Verify (NLI) ─────────────────────────────────────────────────────────────
301
+ if (msg.type === 'verify') {
302
+ try {
303
+ const result = await computeVerification(msg.claimText, msg.sourceText);
304
+ parentPort.postMessage({ id: msg.id, result });
305
+ }
306
+ catch (err) {
307
+ parentPort.postMessage({
308
+ id: msg.id,
309
+ error: String(err instanceof Error ? err.message : err),
310
+ });
311
+ }
312
+ return;
313
+ }
314
+ }
315
+ parentPort.on('message', (msg) => {
316
+ if ('__shutdown' in msg) {
317
+ try {
318
+ parentPort.close();
319
+ }
320
+ catch { /* ignore */ }
321
+ process.exit(0);
322
+ return;
323
+ }
324
+ enqueue(() => handleMessage(msg));
325
+ });
326
+ //# sourceMappingURL=embedWorker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embedWorker.js","sourceRoot":"","sources":["../src/embedWorker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import type { EmbeddingHealth, EmbeddingProvider, EmbeddingProviderMetadata, EmbedRole, FastEmbedModelConfig } from './index.js';
2
+ declare const MODEL_CONFIGS: Record<string, FastEmbedModelConfig>;
3
+ /** @deprecated Use MODEL_CONFIGS[modelId].dim instead. */
4
+ declare const MODEL_DIMS: Record<string, number>;
5
+ /** @deprecated Use MODEL_CONFIGS[modelId].maxTokens instead. */
6
+ declare const MODEL_MAX_TOKENS: Record<string, number>;
7
+ declare const DEFAULT_MODEL = "bge-base-en-v1.5";
8
+ declare const DEFAULT_BATCH_SIZE = 256;
9
+ /**
10
+ * Real ONNX embedding provider using fastembed-js.
11
+ *
12
+ * Runs inference in a dedicated child PROCESS (BL-238/BL-171), never the
13
+ * main thread and never a `worker_threads.Worker` shared with
14
+ * `@huggingface/transformers`-based inference (rerank/verify) — see
15
+ * `sharedFastembedProcess.ts` / `fastembedProcessHost.ts` for the full
16
+ * root-cause writeup on why fastembed's onnxruntime-node@1.21.0 cannot
17
+ * safely share a thread with onnxruntime-node@1.24.3, even sequentially.
18
+ */
19
+ export declare class FastembedProvider implements EmbeddingProvider {
20
+ readonly metadata: EmbeddingProviderMetadata;
21
+ private model;
22
+ private cacheDir;
23
+ private shared;
24
+ private ready;
25
+ private readyPromise;
26
+ private embedDim;
27
+ private maxTokensVal;
28
+ private _lastError;
29
+ constructor(model: string, dimensions: number, cacheDir: string);
30
+ health(): EmbeddingHealth;
31
+ embedSingle(text: string, _role?: EmbedRole): Promise<Float32Array>;
32
+ embedBatch(texts: string[], opts?: {
33
+ role?: EmbedRole;
34
+ batchSize?: number;
35
+ }): AsyncIterable<Float32Array>;
36
+ warmUp(texts: string[]): Promise<void>;
37
+ /**
38
+ * Rough token estimation: ~4 characters per token.
39
+ * Used for chunk-then-mean-pool boundary detection.
40
+ */
41
+ private estimateTokens;
42
+ /**
43
+ * Split text into chunks that fit within maxTokens.
44
+ * Splits on whitespace boundaries near the token limit for clean breaks.
45
+ */
46
+ private chunkText;
47
+ /**
48
+ * Mean-pool multiple embedding vectors into one.
49
+ * All vectors must have the same length.
50
+ */
51
+ private meanPool;
52
+ /**
53
+ * Lazily initialise this provider's model on the shared ONNX worker.
54
+ * Idempotent: concurrent callers await the same in-flight init.
55
+ */
56
+ private ensureReady;
57
+ private initModel;
58
+ private sendBatch;
59
+ private toFloat32Normalised;
60
+ }
61
+ export { MODEL_CONFIGS, MODEL_DIMS, MODEL_MAX_TOKENS, DEFAULT_MODEL, DEFAULT_BATCH_SIZE };
62
+ //# sourceMappingURL=fastembed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fastembed.d.ts","sourceRoot":"","sources":["../src/fastembed.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAejI,QAAA,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAoCvD,CAAC;AAEF,0DAA0D;AAC1D,QAAA,MAAM,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAEtC,CAAC;AAEF,gEAAgE;AAChE,QAAA,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAE5C,CAAC;AAEF,QAAA,MAAM,aAAa,qBAAqB,CAAC;AACzC,QAAA,MAAM,kBAAkB,MAAM,CAAC;AAE/B;;;;;;;;;GASG;AACH,qBAAa,iBAAkB,YAAW,iBAAiB;IACzD,QAAQ,CAAC,QAAQ,EAAE,yBAAyB,CAAC;IAC7C,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,QAAQ,CAAS;IAKzB,OAAO,CAAC,MAAM,CAA6D;IAC3E,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,YAAY,CAAO;IAC3B,OAAO,CAAC,UAAU,CAAuB;gBAE7B,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;IAgB/D,MAAM,IAAI,eAAe;IAkBnB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC;IAkBlE,UAAU,CACf,KAAK,EAAE,MAAM,EAAE,EACf,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,SAAS,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAC9C,aAAa,CAAC,YAAY,CAAC;IAuBxB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAO5C;;;OAGG;IACH,OAAO,CAAC,cAAc;IAItB;;;OAGG;IACH,OAAO,CAAC,SAAS;IAmBjB;;;OAGG;IACH,OAAO,CAAC,QAAQ;IA0BhB;;;OAGG;IACH,OAAO,CAAC,WAAW;YAQL,SAAS;YAqBT,SAAS;IAKvB,OAAO,CAAC,mBAAmB;CAc5B;AAED,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,gBAAgB,EAAE,aAAa,EAAE,kBAAkB,EAAE,CAAC"}
@@ -0,0 +1,259 @@
1
+ import { warmupTimeoutMs } from './index.js';
2
+ import { getSharedFastembedProcess } from './sharedFastembedProcess.js';
3
+ const MODEL_CONFIGS = {
4
+ 'bge-small-en-v1.5': {
5
+ modelId: 'bge-small-en-v1.5',
6
+ hfRepoId: 'fast-bge-small-en-v1.5',
7
+ dim: 384,
8
+ maxTokens: 512,
9
+ description: 'BGE Small English v1.5 — lightweight 384-dim embedding, ~33M params',
10
+ },
11
+ 'bge-base-en-v1.5': {
12
+ modelId: 'bge-base-en-v1.5',
13
+ hfRepoId: 'fast-bge-base-en-v1.5',
14
+ dim: 768,
15
+ maxTokens: 512,
16
+ description: 'BGE Base English v1.5 — balanced 768-dim embedding, ~110M params',
17
+ },
18
+ 'multilingual-e5-large': {
19
+ modelId: 'multilingual-e5-large',
20
+ hfRepoId: 'fast-multilingual-e5-large',
21
+ dim: 1024,
22
+ maxTokens: 512,
23
+ description: 'Multilingual E5 Large — 1024-dim, 100+ languages, ~335M params',
24
+ },
25
+ 'bge-m3': {
26
+ modelId: 'bge-m3',
27
+ hfRepoId: 'BAAI/bge-m3',
28
+ dim: 1024,
29
+ maxTokens: 8192,
30
+ description: 'BGE-M3 — 570M params, 8192-token context, 100+ languages, ONNX INT8',
31
+ },
32
+ 'codexembed-400m': {
33
+ modelId: 'codexembed-400m',
34
+ hfRepoId: 'microsoft/codexembed-400m',
35
+ dim: 1024,
36
+ maxTokens: 8192,
37
+ description: 'CodeXEmbed-400M — code-only CPU, ~1.6GB RAM, 8192-token context',
38
+ },
39
+ };
40
+ /** @deprecated Use MODEL_CONFIGS[modelId].dim instead. */
41
+ const MODEL_DIMS = Object.fromEntries(Object.entries(MODEL_CONFIGS).map(([id, cfg]) => [id, cfg.dim]));
42
+ /** @deprecated Use MODEL_CONFIGS[modelId].maxTokens instead. */
43
+ const MODEL_MAX_TOKENS = Object.fromEntries(Object.entries(MODEL_CONFIGS).map(([id, cfg]) => [id, cfg.maxTokens]));
44
+ const DEFAULT_MODEL = 'bge-base-en-v1.5';
45
+ const DEFAULT_BATCH_SIZE = 256;
46
+ /**
47
+ * Real ONNX embedding provider using fastembed-js.
48
+ *
49
+ * Runs inference in a dedicated child PROCESS (BL-238/BL-171), never the
50
+ * main thread and never a `worker_threads.Worker` shared with
51
+ * `@huggingface/transformers`-based inference (rerank/verify) — see
52
+ * `sharedFastembedProcess.ts` / `fastembedProcessHost.ts` for the full
53
+ * root-cause writeup on why fastembed's onnxruntime-node@1.21.0 cannot
54
+ * safely share a thread with onnxruntime-node@1.24.3, even sequentially.
55
+ */
56
+ export class FastembedProvider {
57
+ metadata;
58
+ model;
59
+ cacheDir;
60
+ // BL-238/BL-171 fix: delegate ALL fastembed ONNX inference to the
61
+ // process-wide shared fastembed CHILD PROCESS singleton instead of
62
+ // spawning our own `Worker`/process — see `sharedFastembedProcess.ts` for
63
+ // the full root-cause writeup.
64
+ shared = getSharedFastembedProcess();
65
+ ready = false;
66
+ readyPromise = null;
67
+ embedDim = 0;
68
+ maxTokensVal = 512;
69
+ _lastError = null;
70
+ constructor(model, dimensions, cacheDir) {
71
+ this.model = model;
72
+ this.cacheDir = cacheDir;
73
+ this.embedDim = dimensions;
74
+ const cfg = MODEL_CONFIGS[model];
75
+ this.maxTokensVal = cfg?.maxTokens ?? 512;
76
+ this.metadata = {
77
+ modelId: model,
78
+ dimensions,
79
+ maxTokens: this.maxTokensVal,
80
+ isRemote: false,
81
+ isDeterministic: false,
82
+ providerUri: `local:onnx:${model}`,
83
+ };
84
+ }
85
+ health() {
86
+ let state = 'uninitialized';
87
+ if (this._lastError) {
88
+ state = 'error';
89
+ }
90
+ else if (this.ready) {
91
+ state = 'real';
92
+ }
93
+ else if (this.readyPromise) {
94
+ state = 'warming';
95
+ }
96
+ return {
97
+ configured: `fastembed:${this.model}`,
98
+ active: this.ready ? this.metadata.modelId : null,
99
+ state,
100
+ dimensions: this.embedDim || this.metadata.dimensions,
101
+ last_error: this._lastError,
102
+ };
103
+ }
104
+ async embedSingle(text, _role) {
105
+ // Chunk-then-mean-pool for text exceeding maxTokens (D7: no truncation)
106
+ if (this.estimateTokens(text) > this.maxTokensVal) {
107
+ const chunks = this.chunkText(text, this.maxTokensVal);
108
+ const embeddings = [];
109
+ await this.ensureReady();
110
+ for (const chunk of chunks) {
111
+ const res = await this.shared.request({ type: 'embed', text: chunk });
112
+ embeddings.push(this.toFloat32Normalised(res.embedding));
113
+ }
114
+ return this.meanPool(embeddings);
115
+ }
116
+ await this.ensureReady();
117
+ const res = await this.shared.request({ type: 'embed', text });
118
+ return this.toFloat32Normalised(res.embedding);
119
+ }
120
+ async *embedBatch(texts, opts) {
121
+ void opts?.role;
122
+ await this.ensureReady();
123
+ const batchSize = opts?.batchSize ?? DEFAULT_BATCH_SIZE;
124
+ for (let i = 0; i < texts.length; i += batchSize) {
125
+ const batch = texts.slice(i, i + batchSize);
126
+ // Check if any text in the batch exceeds maxTokens
127
+ const needsChunking = batch.some((t) => this.estimateTokens(t) > this.maxTokensVal);
128
+ if (needsChunking) {
129
+ // Process each text individually with chunk-then-mean-pool
130
+ for (const text of batch) {
131
+ yield await this.embedSingle(text, opts?.role);
132
+ }
133
+ }
134
+ else {
135
+ const embeddings = await this.sendBatch(batch);
136
+ for (const vec of embeddings) {
137
+ yield this.toFloat32Normalised(vec);
138
+ }
139
+ }
140
+ }
141
+ }
142
+ async warmUp(texts) {
143
+ // No-op: isDeterministic is false, cache would be unreliable.
144
+ // Real warmup requires the worker to be initialized, which happens
145
+ // lazily on the first embedSingle/embedBatch call.
146
+ void texts;
147
+ }
148
+ /**
149
+ * Rough token estimation: ~4 characters per token.
150
+ * Used for chunk-then-mean-pool boundary detection.
151
+ */
152
+ estimateTokens(text) {
153
+ return Math.ceil(text.length / 4);
154
+ }
155
+ /**
156
+ * Split text into chunks that fit within maxTokens.
157
+ * Splits on whitespace boundaries near the token limit for clean breaks.
158
+ */
159
+ chunkText(text, maxTokens) {
160
+ const maxChars = maxTokens * 4;
161
+ if (text.length <= maxChars)
162
+ return [text];
163
+ const chunks = [];
164
+ let start = 0;
165
+ while (start < text.length) {
166
+ let end = Math.min(start + maxChars, text.length);
167
+ // Back up to nearest whitespace if not at end of text
168
+ if (end < text.length) {
169
+ const lastSpace = text.lastIndexOf(' ', end);
170
+ if (lastSpace > start)
171
+ end = lastSpace;
172
+ }
173
+ chunks.push(text.slice(start, end));
174
+ start = end;
175
+ }
176
+ return chunks;
177
+ }
178
+ /**
179
+ * Mean-pool multiple embedding vectors into one.
180
+ * All vectors must have the same length.
181
+ */
182
+ meanPool(vectors) {
183
+ if (vectors.length === 0)
184
+ return new Float32Array(0);
185
+ if (vectors.length === 1)
186
+ return vectors[0];
187
+ const dim = vectors[0].length;
188
+ const pooled = new Float32Array(dim);
189
+ for (const vec of vectors) {
190
+ for (let i = 0; i < dim; i++) {
191
+ pooled[i] += vec[i];
192
+ }
193
+ }
194
+ const n = vectors.length;
195
+ for (let i = 0; i < dim; i++) {
196
+ pooled[i] = pooled[i] / n;
197
+ }
198
+ // Normalise the pooled vector
199
+ let norm = 0;
200
+ for (let i = 0; i < dim; i++) {
201
+ norm += pooled[i] * pooled[i];
202
+ }
203
+ norm = Math.sqrt(norm) || 1;
204
+ for (let i = 0; i < dim; i++) {
205
+ pooled[i] = pooled[i] / norm;
206
+ }
207
+ return pooled;
208
+ }
209
+ /**
210
+ * Lazily initialise this provider's model on the shared ONNX worker.
211
+ * Idempotent: concurrent callers await the same in-flight init.
212
+ */
213
+ ensureReady() {
214
+ if (this.ready)
215
+ return Promise.resolve();
216
+ if (!this.readyPromise) {
217
+ this.readyPromise = this.initModel();
218
+ }
219
+ return this.readyPromise;
220
+ }
221
+ async initModel() {
222
+ try {
223
+ const res = await this.shared.request({ type: 'init', model: this.model, cacheDir: this.cacheDir }, warmupTimeoutMs());
224
+ if (res.dim > 0) {
225
+ this.embedDim = res.dim;
226
+ }
227
+ this.ready = true;
228
+ this._lastError = null;
229
+ }
230
+ catch (e) {
231
+ const err = e instanceof Error ? e : new Error(String(e));
232
+ this._lastError = err.message;
233
+ // Allow a subsequent call to retry initialisation rather than being
234
+ // permanently stuck on a failed readyPromise.
235
+ this.readyPromise = null;
236
+ throw err;
237
+ }
238
+ }
239
+ async sendBatch(texts) {
240
+ const res = await this.shared.request({ type: 'embedBatch', texts });
241
+ return res.embeddings;
242
+ }
243
+ toFloat32Normalised(raw) {
244
+ const dim = this.embedDim || raw.length;
245
+ const vec = new Float32Array(dim);
246
+ let norm = 0;
247
+ for (let i = 0; i < dim && i < raw.length; i++) {
248
+ vec[i] = raw[i] ?? 0;
249
+ norm += vec[i] * vec[i];
250
+ }
251
+ norm = Math.sqrt(norm) || 1;
252
+ for (let i = 0; i < dim; i++) {
253
+ vec[i] = vec[i] / norm;
254
+ }
255
+ return vec;
256
+ }
257
+ }
258
+ export { MODEL_CONFIGS, MODEL_DIMS, MODEL_MAX_TOKENS, DEFAULT_MODEL, DEFAULT_BATCH_SIZE };
259
+ //# sourceMappingURL=fastembed.js.map