libsql-search 0.5.0 → 0.7.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 +8 -9
- package/dist/index.cjs +233 -146
- package/dist/index.d.ts +9 -1
- package/dist/index.esm.js +233 -146
- package/docs/API.md +33 -4
- package/docs/INDEXING.md +6 -2
- package/docs/INTEGRATIONS.md +21 -7
- package/docs/PROVIDERS.md +128 -18
- package/docs/README.md +2 -2
- package/docs/TROUBLESHOOTING-SHARP.md +7 -6
- package/docs/TROUBLESHOOTING.md +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -20,10 +20,11 @@ Use it when you want:
|
|
|
20
20
|
|
|
21
21
|
- Markdown indexing from local directories with frontmatter via `gray-matter`
|
|
22
22
|
- libSQL/Turso storage and vector search
|
|
23
|
-
- Embedding providers: local `Xenova/all-MiniLM-L6-v2`,
|
|
24
|
-
`@cf/baai/bge-m3`, Mistral `mistral-embed`, Google Gemini
|
|
23
|
+
- Embedding providers: local Hugging Face `Xenova/all-MiniLM-L6-v2`,
|
|
24
|
+
Cloudflare Workers AI `@cf/baai/bge-m3`, Mistral `mistral-embed`, Google Gemini
|
|
25
25
|
`gemini-embedding-2`, and OpenAI `text-embedding-3-small` /
|
|
26
|
-
`text-embedding-3-large
|
|
26
|
+
`text-embedding-3-large`; self-hosted OpenAI-compatible endpoints are also
|
|
27
|
+
available for TEI and similar trusted deployments
|
|
27
28
|
- npm distribution plus JSR publishing
|
|
28
29
|
|
|
29
30
|
## Install
|
|
@@ -65,14 +66,13 @@ const client = createClient({
|
|
|
65
66
|
authToken: "your-auth-token",
|
|
66
67
|
});
|
|
67
68
|
|
|
68
|
-
await createTable(client
|
|
69
|
+
await createTable(client);
|
|
69
70
|
|
|
70
71
|
await indexContent({
|
|
71
72
|
client,
|
|
72
73
|
contentPath: "./content",
|
|
73
74
|
embeddingOptions: {
|
|
74
75
|
provider: "local",
|
|
75
|
-
dimensions: 768,
|
|
76
76
|
},
|
|
77
77
|
});
|
|
78
78
|
|
|
@@ -82,7 +82,6 @@ const results = await search({
|
|
|
82
82
|
limit: 5,
|
|
83
83
|
embeddingOptions: {
|
|
84
84
|
provider: "local",
|
|
85
|
-
dimensions: 768,
|
|
86
85
|
},
|
|
87
86
|
});
|
|
88
87
|
|
|
@@ -98,9 +97,9 @@ Important behavior:
|
|
|
98
97
|
- Call `createTable()` before indexing or searching.
|
|
99
98
|
- Keep dimensions aligned across table creation, indexing, and search queries.
|
|
100
99
|
- `indexContent()` clears existing rows before rebuilding the index.
|
|
101
|
-
- `local` is the default offline provider
|
|
102
|
-
option. Cloudflare and Mistral use 1024 dimensions.
|
|
103
|
-
dimensions and supports 128-3072.
|
|
100
|
+
- `local` is the default offline provider and uses 384 dimensions. Cloudflare is
|
|
101
|
+
the recommended hosted option. Cloudflare and Mistral use 1024 dimensions.
|
|
102
|
+
Gemini defaults to 3072 dimensions and supports 128-3072.
|
|
104
103
|
|
|
105
104
|
## Core API
|
|
106
105
|
|
package/dist/index.cjs
CHANGED
|
@@ -4,7 +4,9 @@ var promises = require('fs/promises');
|
|
|
4
4
|
var path = require('path');
|
|
5
5
|
var matter = require('gray-matter');
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const OPENAI_DEFAULT_DIMENSIONS = 768;
|
|
8
|
+
const OPENAI_COMPATIBLE_DEFAULT_BATCH_SIZE = 32;
|
|
9
|
+
const LOCAL_DIMENSIONS = 384;
|
|
8
10
|
const DEFAULT_MAX_LENGTH = 8e3;
|
|
9
11
|
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
10
12
|
const LOCAL_MODEL = "Xenova/all-MiniLM-L6-v2";
|
|
@@ -33,17 +35,66 @@ function getEnvironmentVariable(name) {
|
|
|
33
35
|
return void 0;
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
|
-
|
|
38
|
+
function deletePendingLocalModelCache(modelName, entry) {
|
|
39
|
+
if (!entry.settled && entry.waiters === 0 && localModelCacheByModel.get(modelName) === entry) {
|
|
40
|
+
localModelCacheByModel.delete(modelName);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function getLocalEmbeddingModel(modelName, signal) {
|
|
37
44
|
const cached = localModelCacheByModel.get(modelName);
|
|
38
|
-
if (cached
|
|
39
|
-
|
|
45
|
+
if (cached) {
|
|
46
|
+
cached.waiters++;
|
|
47
|
+
try {
|
|
48
|
+
return await waitForLocalEmbeddingModel(modelName, cached, signal);
|
|
49
|
+
} finally {
|
|
50
|
+
cached.waiters--;
|
|
51
|
+
deletePendingLocalModelCache(modelName, cached);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const modelPromise = (async () => {
|
|
55
|
+
console.log(`Loading local embedding model (${modelName})...`);
|
|
56
|
+
const { pipeline } = await import('@huggingface/transformers');
|
|
57
|
+
const model = await pipeline("feature-extraction", modelName);
|
|
58
|
+
console.log("Local model loaded successfully");
|
|
59
|
+
return model;
|
|
60
|
+
})();
|
|
61
|
+
const entry = {
|
|
62
|
+
promise: modelPromise,
|
|
63
|
+
settled: false,
|
|
64
|
+
waiters: 1
|
|
65
|
+
};
|
|
66
|
+
localModelCacheByModel.set(modelName, entry);
|
|
67
|
+
modelPromise.then(() => {
|
|
68
|
+
entry.settled = true;
|
|
69
|
+
}).catch(() => {
|
|
70
|
+
localModelCacheByModel.delete(modelName);
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
return await waitForLocalEmbeddingModel(modelName, entry, signal);
|
|
74
|
+
} finally {
|
|
75
|
+
entry.waiters--;
|
|
76
|
+
deletePendingLocalModelCache(modelName, entry);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function waitForLocalEmbeddingModel(modelName, entry, signal) {
|
|
80
|
+
if (signal.aborted) {
|
|
81
|
+
deletePendingLocalModelCache(modelName, entry);
|
|
82
|
+
throw providerError("local", "model inference was aborted");
|
|
83
|
+
}
|
|
84
|
+
let rejectAbort = () => {
|
|
85
|
+
};
|
|
86
|
+
const abortPromise = new Promise((_resolve, reject) => {
|
|
87
|
+
rejectAbort = reject;
|
|
88
|
+
});
|
|
89
|
+
const onAbort = () => {
|
|
90
|
+
rejectAbort(providerError("local", "model inference was aborted"));
|
|
91
|
+
};
|
|
92
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
93
|
+
try {
|
|
94
|
+
return await Promise.race([entry.promise, abortPromise]);
|
|
95
|
+
} finally {
|
|
96
|
+
signal.removeEventListener("abort", onAbort);
|
|
40
97
|
}
|
|
41
|
-
console.log(`Loading local embedding model (${modelName})...`);
|
|
42
|
-
const { pipeline } = await import('@xenova/transformers');
|
|
43
|
-
const model = await pipeline("feature-extraction", modelName);
|
|
44
|
-
localModelCacheByModel.set(modelName, { model });
|
|
45
|
-
console.log("Local model loaded successfully");
|
|
46
|
-
return model;
|
|
47
98
|
}
|
|
48
99
|
function getPositiveInteger(value, optionName) {
|
|
49
100
|
if (!Number.isInteger(value) || value <= 0) {
|
|
@@ -61,6 +112,7 @@ function resolveProviderName(provider) {
|
|
|
61
112
|
case "openai":
|
|
62
113
|
case "mistral":
|
|
63
114
|
case "cloudflare":
|
|
115
|
+
case "openai-compatible":
|
|
64
116
|
return provider ?? "local";
|
|
65
117
|
default:
|
|
66
118
|
throw new Error(`Unknown embedding provider: ${String(provider)}`);
|
|
@@ -85,6 +137,13 @@ function getOptionalTrimmedCredential(value) {
|
|
|
85
137
|
const trimmed = value?.trim();
|
|
86
138
|
return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
|
|
87
139
|
}
|
|
140
|
+
function getRequiredTrimmedString(value, optionName) {
|
|
141
|
+
const trimmed = getOptionalTrimmedCredential(value);
|
|
142
|
+
if (!trimmed) {
|
|
143
|
+
throw new Error(`Invalid ${optionName}: expected a non-empty string`);
|
|
144
|
+
}
|
|
145
|
+
return trimmed;
|
|
146
|
+
}
|
|
88
147
|
function hasOwnProperty(value, property) {
|
|
89
148
|
return Object.prototype.hasOwnProperty.call(value, property);
|
|
90
149
|
}
|
|
@@ -245,7 +304,27 @@ async function embedSequentially(provider, operation, texts, signal, embedOne) {
|
|
|
245
304
|
}
|
|
246
305
|
return results;
|
|
247
306
|
}
|
|
248
|
-
function
|
|
307
|
+
function normalizeOpenAICompatibleEmbeddingsUrl(baseUrl) {
|
|
308
|
+
let url;
|
|
309
|
+
try {
|
|
310
|
+
url = new URL(baseUrl);
|
|
311
|
+
} catch {
|
|
312
|
+
throw new Error("Invalid baseUrl: expected an absolute http or https URL");
|
|
313
|
+
}
|
|
314
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
315
|
+
throw new Error("Invalid baseUrl: expected an absolute http or https URL");
|
|
316
|
+
}
|
|
317
|
+
if (url.username || url.password) {
|
|
318
|
+
throw new Error("Invalid baseUrl: URL credentials are not allowed");
|
|
319
|
+
}
|
|
320
|
+
if (url.search || url.hash) {
|
|
321
|
+
throw new Error("Invalid baseUrl: query strings and fragments are not allowed");
|
|
322
|
+
}
|
|
323
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
324
|
+
url.pathname = path.endsWith("/embeddings") ? path : `${path}/embeddings`;
|
|
325
|
+
return url.toString();
|
|
326
|
+
}
|
|
327
|
+
function createProviderMetadata(provider, dimensions, model) {
|
|
249
328
|
switch (provider) {
|
|
250
329
|
case "local":
|
|
251
330
|
return Object.freeze({
|
|
@@ -282,9 +361,25 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
282
361
|
dimensions: CLOUDFLARE_DIMENSIONS,
|
|
283
362
|
batch: Object.freeze({ mode: "native" })
|
|
284
363
|
});
|
|
364
|
+
case "openai-compatible":
|
|
365
|
+
return Object.freeze({
|
|
366
|
+
name: "openai-compatible",
|
|
367
|
+
model: getRequiredTrimmedString(model, "model"),
|
|
368
|
+
dimensions,
|
|
369
|
+
batch: Object.freeze({ mode: "native" })
|
|
370
|
+
});
|
|
285
371
|
}
|
|
286
372
|
}
|
|
287
373
|
function getEffectiveDimensions(provider, dimensions) {
|
|
374
|
+
if (provider === "local") {
|
|
375
|
+
if (dimensions !== void 0 && dimensions !== LOCAL_DIMENSIONS) {
|
|
376
|
+
throw providerError(
|
|
377
|
+
"local",
|
|
378
|
+
`${LOCAL_MODEL} returns ${LOCAL_DIMENSIONS} dimensions; received dimensions ${String(dimensions)}`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
return LOCAL_DIMENSIONS;
|
|
382
|
+
}
|
|
288
383
|
if (provider === "gemini") {
|
|
289
384
|
const effectiveDimensions = dimensions ?? GEMINI_DIMENSIONS;
|
|
290
385
|
if (!Number.isInteger(effectiveDimensions) || effectiveDimensions < GEMINI_MIN_DIMENSIONS || effectiveDimensions > GEMINI_MAX_DIMENSIONS) {
|
|
@@ -313,7 +408,13 @@ function getEffectiveDimensions(provider, dimensions) {
|
|
|
313
408
|
}
|
|
314
409
|
return CLOUDFLARE_DIMENSIONS;
|
|
315
410
|
}
|
|
316
|
-
|
|
411
|
+
if (provider === "openai-compatible") {
|
|
412
|
+
if (dimensions === void 0) {
|
|
413
|
+
throw new Error("Invalid dimensions: expected a positive integer");
|
|
414
|
+
}
|
|
415
|
+
return getPositiveInteger(dimensions, "dimensions");
|
|
416
|
+
}
|
|
417
|
+
return getPositiveInteger(dimensions ?? OPENAI_DEFAULT_DIMENSIONS, "dimensions");
|
|
317
418
|
}
|
|
318
419
|
function assertBatchSize(metadata, count) {
|
|
319
420
|
if (metadata.batch.maxSize !== void 0 && count > metadata.batch.maxSize) {
|
|
@@ -323,6 +424,13 @@ function assertBatchSize(metadata, count) {
|
|
|
323
424
|
);
|
|
324
425
|
}
|
|
325
426
|
}
|
|
427
|
+
function chunkTexts(texts, batchSize) {
|
|
428
|
+
const chunks = [];
|
|
429
|
+
for (let index = 0; index < texts.length; index += batchSize) {
|
|
430
|
+
chunks.push(texts.slice(index, index + batchSize));
|
|
431
|
+
}
|
|
432
|
+
return chunks;
|
|
433
|
+
}
|
|
326
434
|
function createEmbeddingBatchResult(metadata, intent, embeddings) {
|
|
327
435
|
return Object.freeze({
|
|
328
436
|
embeddings,
|
|
@@ -350,14 +458,13 @@ class LocalEmbeddingProvider {
|
|
|
350
458
|
"model inference",
|
|
351
459
|
this.#timeoutMs,
|
|
352
460
|
async (signal) => {
|
|
353
|
-
const model = await getLocalEmbeddingModel(this.metadata.model);
|
|
461
|
+
const model = await getLocalEmbeddingModel(this.metadata.model, signal);
|
|
354
462
|
return embedSequentially("local", "model inference", texts, signal, async (text) => {
|
|
355
463
|
const output = await model(text, {
|
|
356
464
|
pooling: "mean",
|
|
357
465
|
normalize: true
|
|
358
466
|
});
|
|
359
|
-
|
|
360
|
-
return padEmbedding(embedding, this.metadata.dimensions);
|
|
467
|
+
return Array.from(output.data);
|
|
361
468
|
});
|
|
362
469
|
},
|
|
363
470
|
options.signal
|
|
@@ -424,18 +531,23 @@ async function requestOpenAICompatibleEmbeddings(request) {
|
|
|
424
531
|
if (request.encodingFormat !== void 0) {
|
|
425
532
|
body.encoding_format = request.encodingFormat;
|
|
426
533
|
}
|
|
534
|
+
const headers = {
|
|
535
|
+
"Content-Type": "application/json"
|
|
536
|
+
};
|
|
537
|
+
if (request.apiKey !== void 0) {
|
|
538
|
+
headers.Authorization = `Bearer ${request.apiKey}`;
|
|
539
|
+
}
|
|
427
540
|
const response = await fetch(request.endpoint, {
|
|
428
541
|
method: "POST",
|
|
429
|
-
headers
|
|
430
|
-
"Content-Type": "application/json",
|
|
431
|
-
"Authorization": `Bearer ${request.apiKey}`
|
|
432
|
-
},
|
|
542
|
+
headers,
|
|
433
543
|
signal: request.signal,
|
|
434
|
-
body: JSON.stringify(body)
|
|
544
|
+
body: JSON.stringify(body),
|
|
545
|
+
...request.redirect ? { redirect: request.redirect } : {}
|
|
435
546
|
});
|
|
436
547
|
if (!response.ok) {
|
|
437
|
-
const
|
|
438
|
-
const
|
|
548
|
+
const statusHeader = request.statusHeader ?? { name: "x-request-id", label: "request" };
|
|
549
|
+
const statusHeaderValue = getSafeResponseHeader(response, statusHeader.name);
|
|
550
|
+
const context = statusHeaderValue ? ` status ${response.status}, ${statusHeader.label} ${statusHeaderValue}` : ` status ${response.status}`;
|
|
439
551
|
throw new Error(context);
|
|
440
552
|
}
|
|
441
553
|
const data = await response.json();
|
|
@@ -459,92 +571,13 @@ async function requestOpenAICompatibleEmbeddings(request) {
|
|
|
459
571
|
return parsed;
|
|
460
572
|
});
|
|
461
573
|
}
|
|
462
|
-
class
|
|
463
|
-
metadata;
|
|
464
|
-
#apiKey;
|
|
465
|
-
#timeoutMs;
|
|
466
|
-
constructor(metadata, apiKey, timeoutMs) {
|
|
467
|
-
this.metadata = metadata;
|
|
468
|
-
this.#apiKey = apiKey;
|
|
469
|
-
this.#timeoutMs = timeoutMs;
|
|
470
|
-
}
|
|
471
|
-
async embed(texts, options = {}) {
|
|
472
|
-
const intent = resolveIntent(options.intent);
|
|
473
|
-
if (texts.length === 0) {
|
|
474
|
-
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
475
|
-
}
|
|
476
|
-
assertBatchSize(this.metadata, texts.length);
|
|
477
|
-
const items = await withTimeout(
|
|
478
|
-
"openai",
|
|
479
|
-
"API request",
|
|
480
|
-
this.#timeoutMs,
|
|
481
|
-
async (signal) => requestOpenAICompatibleEmbeddings({
|
|
482
|
-
responseLabel: "OpenAI",
|
|
483
|
-
endpoint: OPENAI_EMBEDDINGS_URL,
|
|
484
|
-
apiKey: this.#apiKey,
|
|
485
|
-
model: this.metadata.model,
|
|
486
|
-
texts,
|
|
487
|
-
signal,
|
|
488
|
-
dimensions: this.metadata.dimensions
|
|
489
|
-
}),
|
|
490
|
-
options.signal,
|
|
491
|
-
[this.#apiKey]
|
|
492
|
-
);
|
|
493
|
-
return createEmbeddingBatchResult(
|
|
494
|
-
this.metadata,
|
|
495
|
-
intent,
|
|
496
|
-
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "openai")
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
class MistralEmbeddingProvider {
|
|
501
|
-
metadata;
|
|
502
|
-
#apiKey;
|
|
503
|
-
#timeoutMs;
|
|
504
|
-
constructor(metadata, apiKey, timeoutMs) {
|
|
505
|
-
this.metadata = metadata;
|
|
506
|
-
this.#apiKey = apiKey;
|
|
507
|
-
this.#timeoutMs = timeoutMs;
|
|
508
|
-
}
|
|
509
|
-
async embed(texts, options = {}) {
|
|
510
|
-
const intent = resolveIntent(options.intent);
|
|
511
|
-
if (texts.length === 0) {
|
|
512
|
-
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
513
|
-
}
|
|
514
|
-
assertBatchSize(this.metadata, texts.length);
|
|
515
|
-
const items = await withTimeout(
|
|
516
|
-
"mistral",
|
|
517
|
-
"API request",
|
|
518
|
-
this.#timeoutMs,
|
|
519
|
-
async (signal) => requestOpenAICompatibleEmbeddings({
|
|
520
|
-
responseLabel: "Mistral",
|
|
521
|
-
endpoint: MISTRAL_EMBEDDINGS_URL,
|
|
522
|
-
apiKey: this.#apiKey,
|
|
523
|
-
model: this.metadata.model,
|
|
524
|
-
texts,
|
|
525
|
-
signal,
|
|
526
|
-
encodingFormat: "float",
|
|
527
|
-
requireIndex: true
|
|
528
|
-
}),
|
|
529
|
-
options.signal,
|
|
530
|
-
[this.#apiKey]
|
|
531
|
-
);
|
|
532
|
-
return createEmbeddingBatchResult(
|
|
533
|
-
this.metadata,
|
|
534
|
-
intent,
|
|
535
|
-
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "mistral")
|
|
536
|
-
);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
class CloudflareEmbeddingProvider {
|
|
574
|
+
class OpenAICompatibleEmbeddingProvider {
|
|
540
575
|
metadata;
|
|
541
|
-
#accountId;
|
|
542
|
-
#apiToken;
|
|
543
576
|
#timeoutMs;
|
|
544
|
-
|
|
577
|
+
#profile;
|
|
578
|
+
constructor(metadata, profile, timeoutMs) {
|
|
545
579
|
this.metadata = metadata;
|
|
546
|
-
this.#
|
|
547
|
-
this.#apiToken = apiToken;
|
|
580
|
+
this.#profile = profile;
|
|
548
581
|
this.#timeoutMs = timeoutMs;
|
|
549
582
|
}
|
|
550
583
|
async embed(texts, options = {}) {
|
|
@@ -553,53 +586,45 @@ class CloudflareEmbeddingProvider {
|
|
|
553
586
|
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
554
587
|
}
|
|
555
588
|
assertBatchSize(this.metadata, texts.length);
|
|
556
|
-
const endpoint = createCloudflareEmbeddingsUrl(this.#accountId);
|
|
557
589
|
const items = await withTimeout(
|
|
558
|
-
|
|
590
|
+
this.#profile.provider,
|
|
559
591
|
"API request",
|
|
560
592
|
this.#timeoutMs,
|
|
561
593
|
async (signal) => {
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
model: this.
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
594
|
+
const batches = this.#profile.batchSize === void 0 ? [texts] : chunkTexts(texts, this.#profile.batchSize);
|
|
595
|
+
const results = [];
|
|
596
|
+
for (const batch of batches) {
|
|
597
|
+
throwIfAborted(this.#profile.provider, "API request", signal);
|
|
598
|
+
const batchItems = await requestOpenAICompatibleEmbeddings({
|
|
599
|
+
responseLabel: this.#profile.responseLabel,
|
|
600
|
+
endpoint: this.#profile.endpoint,
|
|
601
|
+
...this.#profile.apiKey !== void 0 ? { apiKey: this.#profile.apiKey } : {},
|
|
602
|
+
model: this.#profile.model,
|
|
603
|
+
texts: batch,
|
|
604
|
+
signal,
|
|
605
|
+
...this.#profile.includeDimensions ? { dimensions: this.#profile.dimensions } : {},
|
|
606
|
+
...this.#profile.encodingFormat ? { encodingFormat: this.#profile.encodingFormat } : {},
|
|
607
|
+
...this.#profile.requireIndex !== void 0 ? { requireIndex: this.#profile.requireIndex } : {},
|
|
608
|
+
...this.#profile.redirect ? { redirect: this.#profile.redirect } : {},
|
|
609
|
+
...this.#profile.statusHeader ? { statusHeader: this.#profile.statusHeader } : {}
|
|
610
|
+
});
|
|
611
|
+
results.push(...validateEmbeddingBatch(
|
|
612
|
+
batchItems,
|
|
613
|
+
batch.length,
|
|
614
|
+
this.metadata.dimensions,
|
|
615
|
+
this.#profile.provider
|
|
616
|
+
));
|
|
617
|
+
throwIfAborted(this.#profile.provider, "API request", signal);
|
|
582
618
|
}
|
|
583
|
-
return
|
|
584
|
-
const typed = item;
|
|
585
|
-
const embedding = typed.embedding;
|
|
586
|
-
if (!Array.isArray(embedding)) {
|
|
587
|
-
throw new Error("Cloudflare response item did not include an embedding array");
|
|
588
|
-
}
|
|
589
|
-
const parsed = { embedding };
|
|
590
|
-
if (hasOwnProperty(typed, "index")) {
|
|
591
|
-
parsed.index = typed.index;
|
|
592
|
-
}
|
|
593
|
-
return parsed;
|
|
594
|
-
});
|
|
619
|
+
return results;
|
|
595
620
|
},
|
|
596
621
|
options.signal,
|
|
597
|
-
|
|
622
|
+
this.#profile.exactSecrets ?? []
|
|
598
623
|
);
|
|
599
624
|
return createEmbeddingBatchResult(
|
|
600
625
|
this.metadata,
|
|
601
626
|
intent,
|
|
602
|
-
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions,
|
|
627
|
+
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, this.#profile.provider)
|
|
603
628
|
);
|
|
604
629
|
}
|
|
605
630
|
}
|
|
@@ -624,7 +649,16 @@ function createEmbeddingProvider(options = {}) {
|
|
|
624
649
|
if (!key) {
|
|
625
650
|
throw new Error("OPENAI_API_KEY is required for OpenAI embeddings");
|
|
626
651
|
}
|
|
627
|
-
return new
|
|
652
|
+
return new OpenAICompatibleEmbeddingProvider(metadata, {
|
|
653
|
+
provider: "openai",
|
|
654
|
+
responseLabel: "OpenAI",
|
|
655
|
+
endpoint: OPENAI_EMBEDDINGS_URL,
|
|
656
|
+
apiKey: key,
|
|
657
|
+
model: metadata.model,
|
|
658
|
+
dimensions: metadata.dimensions,
|
|
659
|
+
includeDimensions: true,
|
|
660
|
+
exactSecrets: [key]
|
|
661
|
+
}, timeoutMs);
|
|
628
662
|
}
|
|
629
663
|
case "mistral": {
|
|
630
664
|
const key = getOptionalTrimmedCredential(
|
|
@@ -633,7 +667,17 @@ function createEmbeddingProvider(options = {}) {
|
|
|
633
667
|
if (!key) {
|
|
634
668
|
throw new Error("MISTRAL_API_KEY is required for Mistral embeddings");
|
|
635
669
|
}
|
|
636
|
-
return new
|
|
670
|
+
return new OpenAICompatibleEmbeddingProvider(metadata, {
|
|
671
|
+
provider: "mistral",
|
|
672
|
+
responseLabel: "Mistral",
|
|
673
|
+
endpoint: MISTRAL_EMBEDDINGS_URL,
|
|
674
|
+
apiKey: key,
|
|
675
|
+
model: metadata.model,
|
|
676
|
+
dimensions: metadata.dimensions,
|
|
677
|
+
encodingFormat: "float",
|
|
678
|
+
requireIndex: true,
|
|
679
|
+
exactSecrets: [key]
|
|
680
|
+
}, timeoutMs);
|
|
637
681
|
}
|
|
638
682
|
case "cloudflare": {
|
|
639
683
|
const accountId = getOptionalTrimmedCredential(
|
|
@@ -648,13 +692,56 @@ function createEmbeddingProvider(options = {}) {
|
|
|
648
692
|
if (!apiToken) {
|
|
649
693
|
throw new Error("CLOUDFLARE_API_TOKEN is required for Cloudflare embeddings");
|
|
650
694
|
}
|
|
651
|
-
|
|
695
|
+
const endpoint = createCloudflareEmbeddingsUrl(accountId);
|
|
696
|
+
return new OpenAICompatibleEmbeddingProvider(metadata, {
|
|
697
|
+
provider: "cloudflare",
|
|
698
|
+
responseLabel: "Cloudflare",
|
|
699
|
+
endpoint,
|
|
700
|
+
apiKey: apiToken,
|
|
701
|
+
model: metadata.model,
|
|
702
|
+
dimensions: metadata.dimensions,
|
|
703
|
+
statusHeader: { name: "cf-ray", label: "cf-ray" },
|
|
704
|
+
exactSecrets: [apiToken, accountId, encodeURIComponent(accountId), endpoint]
|
|
705
|
+
}, timeoutMs);
|
|
706
|
+
}
|
|
707
|
+
case "openai-compatible": {
|
|
708
|
+
const baseUrl = getRequiredTrimmedString(options.baseUrl, "baseUrl");
|
|
709
|
+
const endpoint = normalizeOpenAICompatibleEmbeddingsUrl(baseUrl);
|
|
710
|
+
const apiKey = getOptionalTrimmedCredential(options.apiKey);
|
|
711
|
+
const batchSize = getPositiveInteger(
|
|
712
|
+
options.batchSize ?? OPENAI_COMPATIBLE_DEFAULT_BATCH_SIZE,
|
|
713
|
+
"batchSize"
|
|
714
|
+
);
|
|
715
|
+
return new OpenAICompatibleEmbeddingProvider(metadata, {
|
|
716
|
+
provider: "openai-compatible",
|
|
717
|
+
responseLabel: "OpenAI-compatible",
|
|
718
|
+
endpoint,
|
|
719
|
+
...apiKey ? { apiKey } : {},
|
|
720
|
+
model: metadata.model,
|
|
721
|
+
dimensions: metadata.dimensions,
|
|
722
|
+
includeDimensions: true,
|
|
723
|
+
encodingFormat: "float",
|
|
724
|
+
requireIndex: true,
|
|
725
|
+
redirect: "error",
|
|
726
|
+
batchSize,
|
|
727
|
+
exactSecrets: [
|
|
728
|
+
...apiKey ? [apiKey] : [],
|
|
729
|
+
baseUrl,
|
|
730
|
+
endpoint
|
|
731
|
+
]
|
|
732
|
+
}, timeoutMs);
|
|
652
733
|
}
|
|
653
734
|
}
|
|
654
735
|
}
|
|
655
736
|
function getEmbeddingProviderMetadata(options = {}) {
|
|
656
737
|
const provider = resolveProviderName(options.provider);
|
|
657
|
-
|
|
738
|
+
if (provider === "openai-compatible") {
|
|
739
|
+
normalizeOpenAICompatibleEmbeddingsUrl(getRequiredTrimmedString(options.baseUrl, "baseUrl"));
|
|
740
|
+
if (options.batchSize !== void 0) {
|
|
741
|
+
getPositiveInteger(options.batchSize, "batchSize");
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return createProviderMetadata(provider, getEffectiveDimensions(provider, options.dimensions), options.model);
|
|
658
745
|
}
|
|
659
746
|
async function generateEmbeddings(texts, options = {}) {
|
|
660
747
|
const maxLength = getPositiveInteger(options.maxLength ?? DEFAULT_MAX_LENGTH, "maxLength");
|
|
@@ -824,7 +911,7 @@ async function insertDocument(client, document, quotedTableName) {
|
|
|
824
911
|
]
|
|
825
912
|
});
|
|
826
913
|
}
|
|
827
|
-
async function createTable(client, tableName = "articles", dimensions =
|
|
914
|
+
async function createTable(client, tableName = "articles", dimensions = 384) {
|
|
828
915
|
const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
|
|
829
916
|
const vectorDimensions = normalizeVectorDimensions(dimensions);
|
|
830
917
|
const quotedEmbeddingIndexName = quoteSqlIdentifier(`${tableName}_embedding_idx`, "embedding index name");
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Client } from '@libsql/client';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Multi-provider embedding generation
|
|
5
|
+
* Supports local Hugging Face Transformers, Gemini, OpenAI, Mistral,
|
|
6
|
+
* Cloudflare Workers AI, and custom OpenAI-compatible endpoints
|
|
7
|
+
*/
|
|
8
|
+
type EmbeddingProvider = 'local' | 'gemini' | 'openai' | 'mistral' | 'cloudflare' | 'openai-compatible';
|
|
4
9
|
type EmbeddingIntent = 'document' | 'query';
|
|
5
10
|
type EmbeddingBatchMode = 'native' | 'sequential';
|
|
6
11
|
interface EmbeddingBatchBehavior {
|
|
@@ -33,6 +38,9 @@ interface EmbeddingOptions {
|
|
|
33
38
|
apiKey?: string;
|
|
34
39
|
accountId?: string;
|
|
35
40
|
apiToken?: string;
|
|
41
|
+
baseUrl?: string;
|
|
42
|
+
model?: string;
|
|
43
|
+
batchSize?: number;
|
|
36
44
|
dimensions?: number;
|
|
37
45
|
maxLength?: number;
|
|
38
46
|
intent?: EmbeddingIntent;
|