@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.
- package/README.md +20 -0
- package/dist/cache.d.ts +70 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +217 -0
- package/dist/cache.js.map +1 -0
- package/dist/embedWorker.d.ts +77 -0
- package/dist/embedWorker.d.ts.map +1 -0
- package/dist/embedWorker.js +326 -0
- package/dist/embedWorker.js.map +1 -0
- package/dist/fastembed.d.ts +62 -0
- package/dist/fastembed.d.ts.map +1 -0
- package/dist/fastembed.js +259 -0
- package/dist/fastembed.js.map +1 -0
- package/dist/fastembedProcessHost.d.ts +60 -0
- package/dist/fastembedProcessHost.d.ts.map +1 -0
- package/dist/fastembedProcessHost.js +166 -0
- package/dist/fastembedProcessHost.js.map +1 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +138 -0
- package/dist/index.js.map +1 -0
- package/dist/package.json +52 -0
- package/dist/remote.d.ts +23 -0
- package/dist/remote.d.ts.map +1 -0
- package/dist/remote.js +91 -0
- package/dist/remote.js.map +1 -0
- package/dist/sharedFastembedProcess.d.ts +56 -0
- package/dist/sharedFastembedProcess.d.ts.map +1 -0
- package/dist/sharedFastembedProcess.js +180 -0
- package/dist/sharedFastembedProcess.js.map +1 -0
- package/dist/sharedOnnxWorker.d.ts +135 -0
- package/dist/sharedOnnxWorker.d.ts.map +1 -0
- package/dist/sharedOnnxWorker.js +255 -0
- package/dist/sharedOnnxWorker.js.map +1 -0
- package/package.json +51 -0
|
@@ -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
|