@delali/narsil 0.1.6 → 0.1.7
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 +130 -2
- package/dist/adapters/filesystem.d.ts +1 -1
- package/dist/adapters/filesystem.mjs +1 -1
- package/dist/adapters/indexeddb.d.ts +1 -1
- package/dist/adapters/indexeddb.mjs +1 -1
- package/dist/adapters/memory.d.ts +1 -1
- package/dist/{adapters-XODT2F3h.d.ts → adapters-BbRkoiu9.d.ts} +7 -1
- package/dist/{chunk-WYC635GI.mjs → chunk-I24SGLFS.mjs} +5 -6
- package/dist/{chunk-TFWBALU4.mjs → chunk-KHL4RPC3.mjs} +1957 -1053
- package/dist/{chunk-7GKPWGCN.mjs → chunk-R2C2EQGO.mjs} +12 -2
- package/dist/{chunk-GVN2W4VA.mjs → chunk-VVORJOXR.mjs} +273 -84
- package/dist/direct-executor-LBJLGMJ2.mjs +4 -0
- package/dist/embeddings/openai.d.ts +13 -0
- package/dist/embeddings/openai.mjs +248 -0
- package/dist/index.browser.mjs +7887 -5557
- package/dist/index.d.ts +83 -49
- package/dist/index.mjs +1886 -702
- package/dist/invalidation/broadcast-channel.d.ts +1 -1
- package/dist/invalidation/filesystem.d.ts +1 -1
- package/dist/invalidation/noop.d.ts +1 -1
- package/dist/languages/english.mjs +1 -1
- package/dist/vector/hnsw-build-worker.d.ts +9 -2
- package/dist/vector/hnsw-build-worker.mjs +54 -10
- package/dist/workers/entry.mjs +1 -1
- package/package.json +10 -3
- package/dist/direct-executor-6TSG7GWH.mjs +0 -4
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { NarsilError, ErrorCodes } from '../chunk-R2C2EQGO.mjs';
|
|
2
|
+
|
|
3
|
+
// src/embeddings/openai.ts
|
|
4
|
+
var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503]);
|
|
5
|
+
var MAX_BACKOFF_MS = 3e4;
|
|
6
|
+
var BASE_BACKOFF_MS = 1e3;
|
|
7
|
+
var MAX_JITTER_MS = 1e3;
|
|
8
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
9
|
+
var MAX_INPUTS_PER_REQUEST = 2048;
|
|
10
|
+
function isRetryableStatus(status) {
|
|
11
|
+
return RETRYABLE_STATUS_CODES.has(status);
|
|
12
|
+
}
|
|
13
|
+
function computeBackoffMs(attempt, retryAfterSeconds) {
|
|
14
|
+
if (retryAfterSeconds !== null && retryAfterSeconds > 0) {
|
|
15
|
+
return Math.min(retryAfterSeconds * 1e3, MAX_BACKOFF_MS);
|
|
16
|
+
}
|
|
17
|
+
const exponential = Math.min(BASE_BACKOFF_MS * 2 ** attempt, MAX_BACKOFF_MS);
|
|
18
|
+
const jitter = Math.random() * MAX_JITTER_MS;
|
|
19
|
+
return exponential + jitter;
|
|
20
|
+
}
|
|
21
|
+
function parseRetryAfterHeader(headers) {
|
|
22
|
+
const raw = headers.get("retry-after");
|
|
23
|
+
if (raw === null) return null;
|
|
24
|
+
const seconds = Number(raw);
|
|
25
|
+
if (Number.isFinite(seconds) && seconds > 0) return seconds;
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
async function resolveApiKey(apiKey) {
|
|
29
|
+
const resolved = typeof apiKey === "function" ? await apiKey() : apiKey;
|
|
30
|
+
if (typeof resolved !== "string" || resolved.trim().length === 0) {
|
|
31
|
+
throw new NarsilError(
|
|
32
|
+
ErrorCodes.EMBEDDING_CONFIG_INVALID,
|
|
33
|
+
"OpenAI embedding adapter apiKey resolved to an empty or non-string value"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return resolved;
|
|
37
|
+
}
|
|
38
|
+
function buildSignal(callerSignal, timeoutMs) {
|
|
39
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
40
|
+
if (callerSignal === void 0) return timeoutSignal;
|
|
41
|
+
return AbortSignal.any([callerSignal, timeoutSignal]);
|
|
42
|
+
}
|
|
43
|
+
async function sleep(ms, signal) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
if (signal?.aborted) {
|
|
46
|
+
reject(signal.reason);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const onAbort = () => {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
reject(signal?.reason);
|
|
52
|
+
};
|
|
53
|
+
const timer = setTimeout(() => {
|
|
54
|
+
signal?.removeEventListener("abort", onAbort);
|
|
55
|
+
resolve();
|
|
56
|
+
}, ms);
|
|
57
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function parseEmbeddingItems(data) {
|
|
61
|
+
if (!Array.isArray(data)) {
|
|
62
|
+
throw new NarsilError(ErrorCodes.EMBEDDING_FAILED, 'OpenAI response missing "data" array');
|
|
63
|
+
}
|
|
64
|
+
const items = [];
|
|
65
|
+
for (let i = 0; i < data.length; i++) {
|
|
66
|
+
const item = data[i];
|
|
67
|
+
if (item === void 0 || item === null || typeof item !== "object") {
|
|
68
|
+
throw new NarsilError(ErrorCodes.EMBEDDING_FAILED, `OpenAI response item at position ${i} is not an object`);
|
|
69
|
+
}
|
|
70
|
+
if (typeof item.index !== "number") {
|
|
71
|
+
throw new NarsilError(
|
|
72
|
+
ErrorCodes.EMBEDDING_FAILED,
|
|
73
|
+
`OpenAI response item at position ${i} missing numeric "index"`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (!Array.isArray(item.embedding)) {
|
|
77
|
+
throw new NarsilError(
|
|
78
|
+
ErrorCodes.EMBEDDING_FAILED,
|
|
79
|
+
`OpenAI response item at position ${i} missing "embedding" array`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
items.push({ index: item.index, embedding: item.embedding });
|
|
83
|
+
}
|
|
84
|
+
return items;
|
|
85
|
+
}
|
|
86
|
+
async function parseErrorBody(response) {
|
|
87
|
+
try {
|
|
88
|
+
const body = await response.json();
|
|
89
|
+
const errorType = body?.error?.type ?? "unknown";
|
|
90
|
+
const errorMessage = body?.error?.message ?? `HTTP ${response.status}`;
|
|
91
|
+
return { type: errorType, message: errorMessage };
|
|
92
|
+
} catch {
|
|
93
|
+
return { type: "unknown", message: `HTTP ${response.status}` };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function executeWithRetry(url, requestBody, apiKey, maxRetries, signal) {
|
|
97
|
+
let lastError = null;
|
|
98
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
99
|
+
let response;
|
|
100
|
+
try {
|
|
101
|
+
response = await fetch(url, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: {
|
|
104
|
+
Authorization: `Bearer ${apiKey}`,
|
|
105
|
+
"Content-Type": "application/json"
|
|
106
|
+
},
|
|
107
|
+
body: requestBody,
|
|
108
|
+
signal
|
|
109
|
+
});
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (err instanceof DOMException || err instanceof Error && err.name === "AbortError") {
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
115
|
+
if (attempt < maxRetries) {
|
|
116
|
+
await sleep(computeBackoffMs(attempt, null), signal);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
throw new NarsilError(
|
|
120
|
+
ErrorCodes.EMBEDDING_FAILED,
|
|
121
|
+
`OpenAI request failed after ${maxRetries + 1} attempts: network error`,
|
|
122
|
+
{
|
|
123
|
+
cause: lastError.message
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (response.ok) {
|
|
128
|
+
let parsed;
|
|
129
|
+
try {
|
|
130
|
+
parsed = await response.json();
|
|
131
|
+
} catch {
|
|
132
|
+
throw new NarsilError(ErrorCodes.EMBEDDING_FAILED, "OpenAI returned invalid JSON in response body");
|
|
133
|
+
}
|
|
134
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
135
|
+
throw new NarsilError(ErrorCodes.EMBEDDING_FAILED, "OpenAI returned non-object JSON response");
|
|
136
|
+
}
|
|
137
|
+
return parsed;
|
|
138
|
+
}
|
|
139
|
+
const retryAfter = parseRetryAfterHeader(response.headers);
|
|
140
|
+
const errorInfo = await parseErrorBody(response);
|
|
141
|
+
if (!isRetryableStatus(response.status) || attempt >= maxRetries) {
|
|
142
|
+
throw new NarsilError(
|
|
143
|
+
ErrorCodes.EMBEDDING_FAILED,
|
|
144
|
+
`OpenAI API error (${response.status}): [${errorInfo.type}] ${errorInfo.message}`,
|
|
145
|
+
{ status: response.status, errorType: errorInfo.type }
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
lastError = new Error(`HTTP ${response.status}: ${errorInfo.type}`);
|
|
149
|
+
await sleep(computeBackoffMs(attempt, retryAfter), signal);
|
|
150
|
+
}
|
|
151
|
+
throw new NarsilError(ErrorCodes.EMBEDDING_FAILED, "OpenAI request failed: exhausted all retries", {
|
|
152
|
+
cause: lastError?.message
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function createOpenAIEmbedding(config) {
|
|
156
|
+
if (!config.baseUrl || config.baseUrl.trim().length === 0) {
|
|
157
|
+
throw new NarsilError(ErrorCodes.EMBEDDING_CONFIG_INVALID, "OpenAI embedding adapter requires a non-empty baseUrl");
|
|
158
|
+
}
|
|
159
|
+
if (!config.model || config.model.trim().length === 0) {
|
|
160
|
+
throw new NarsilError(ErrorCodes.EMBEDDING_CONFIG_INVALID, "OpenAI embedding adapter requires a non-empty model");
|
|
161
|
+
}
|
|
162
|
+
if (!Number.isInteger(config.dimensions) || config.dimensions < 1) {
|
|
163
|
+
throw new NarsilError(
|
|
164
|
+
ErrorCodes.EMBEDDING_CONFIG_INVALID,
|
|
165
|
+
"OpenAI embedding adapter requires dimensions to be a positive integer"
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (config.timeout !== void 0 && (!Number.isFinite(config.timeout) || config.timeout <= 0)) {
|
|
169
|
+
throw new NarsilError(
|
|
170
|
+
ErrorCodes.EMBEDDING_CONFIG_INVALID,
|
|
171
|
+
"OpenAI embedding adapter timeout must be a positive number"
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (config.maxRetries !== void 0 && (!Number.isInteger(config.maxRetries) || config.maxRetries < 0)) {
|
|
175
|
+
throw new NarsilError(
|
|
176
|
+
ErrorCodes.EMBEDDING_CONFIG_INVALID,
|
|
177
|
+
"OpenAI embedding adapter maxRetries must be a non-negative integer"
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
if (typeof config.apiKey === "string" && config.apiKey.trim().length === 0) {
|
|
181
|
+
throw new NarsilError(ErrorCodes.EMBEDDING_CONFIG_INVALID, "OpenAI embedding adapter requires a non-empty apiKey");
|
|
182
|
+
}
|
|
183
|
+
if (typeof config.apiKey !== "string" && typeof config.apiKey !== "function") {
|
|
184
|
+
throw new NarsilError(
|
|
185
|
+
ErrorCodes.EMBEDDING_CONFIG_INVALID,
|
|
186
|
+
"OpenAI embedding adapter apiKey must be a string or a function returning a string"
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const url = `${config.baseUrl.replace(/\/+$/, "")}/embeddings`;
|
|
190
|
+
const model = config.model;
|
|
191
|
+
const dimensionCount = config.dimensions;
|
|
192
|
+
const timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
193
|
+
const maxRetries = config.maxRetries ?? 3;
|
|
194
|
+
return {
|
|
195
|
+
get dimensions() {
|
|
196
|
+
return dimensionCount;
|
|
197
|
+
},
|
|
198
|
+
async embed(input, _purpose, signal) {
|
|
199
|
+
const apiKey = await resolveApiKey(config.apiKey);
|
|
200
|
+
const body = JSON.stringify({ input: [input], model, dimensions: dimensionCount });
|
|
201
|
+
const combinedSignal = buildSignal(signal, timeoutMs);
|
|
202
|
+
const result = await executeWithRetry(url, body, apiKey, maxRetries, combinedSignal);
|
|
203
|
+
const items = parseEmbeddingItems(result.data);
|
|
204
|
+
if (items.length === 0) {
|
|
205
|
+
throw new NarsilError(
|
|
206
|
+
ErrorCodes.EMBEDDING_FAILED,
|
|
207
|
+
"OpenAI returned empty data array for single embedding request"
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
return new Float32Array(items[0].embedding);
|
|
211
|
+
},
|
|
212
|
+
async embedBatch(inputs, _purpose, signal) {
|
|
213
|
+
if (inputs.length === 0) return [];
|
|
214
|
+
const apiKey = await resolveApiKey(config.apiKey);
|
|
215
|
+
const combinedSignal = buildSignal(signal, timeoutMs);
|
|
216
|
+
const executeChunk = async (chunk) => {
|
|
217
|
+
const body = JSON.stringify({ input: chunk, model, dimensions: dimensionCount });
|
|
218
|
+
const result = await executeWithRetry(url, body, apiKey, maxRetries, combinedSignal);
|
|
219
|
+
const items = parseEmbeddingItems(result.data);
|
|
220
|
+
if (items.length !== chunk.length) {
|
|
221
|
+
throw new NarsilError(
|
|
222
|
+
ErrorCodes.EMBEDDING_FAILED,
|
|
223
|
+
`OpenAI returned ${items.length} embeddings for ${chunk.length} inputs`,
|
|
224
|
+
{ expected: chunk.length, actual: items.length }
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const sorted = items.slice().sort((a, b) => a.index - b.index);
|
|
228
|
+
return sorted.map((item) => new Float32Array(item.embedding));
|
|
229
|
+
};
|
|
230
|
+
if (inputs.length <= MAX_INPUTS_PER_REQUEST) {
|
|
231
|
+
return executeChunk(inputs);
|
|
232
|
+
}
|
|
233
|
+
const results = [];
|
|
234
|
+
for (let start = 0; start < inputs.length; start += MAX_INPUTS_PER_REQUEST) {
|
|
235
|
+
const chunk = inputs.slice(start, start + MAX_INPUTS_PER_REQUEST);
|
|
236
|
+
const chunkResults = await executeChunk(chunk);
|
|
237
|
+
for (const vec of chunkResults) {
|
|
238
|
+
results.push(vec);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return results;
|
|
242
|
+
},
|
|
243
|
+
async shutdown() {
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export { createOpenAIEmbedding };
|