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/dist/index.esm.js
CHANGED
|
@@ -2,7 +2,9 @@ import { readdir, readFile } from 'fs/promises';
|
|
|
2
2
|
import { join, extname, relative, dirname } from 'path';
|
|
3
3
|
import matter from 'gray-matter';
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const OPENAI_DEFAULT_DIMENSIONS = 768;
|
|
6
|
+
const OPENAI_COMPATIBLE_DEFAULT_BATCH_SIZE = 32;
|
|
7
|
+
const LOCAL_DIMENSIONS = 384;
|
|
6
8
|
const DEFAULT_MAX_LENGTH = 8e3;
|
|
7
9
|
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
8
10
|
const LOCAL_MODEL = "Xenova/all-MiniLM-L6-v2";
|
|
@@ -31,17 +33,66 @@ function getEnvironmentVariable(name) {
|
|
|
31
33
|
return void 0;
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
|
-
|
|
36
|
+
function deletePendingLocalModelCache(modelName, entry) {
|
|
37
|
+
if (!entry.settled && entry.waiters === 0 && localModelCacheByModel.get(modelName) === entry) {
|
|
38
|
+
localModelCacheByModel.delete(modelName);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function getLocalEmbeddingModel(modelName, signal) {
|
|
35
42
|
const cached = localModelCacheByModel.get(modelName);
|
|
36
|
-
if (cached
|
|
37
|
-
|
|
43
|
+
if (cached) {
|
|
44
|
+
cached.waiters++;
|
|
45
|
+
try {
|
|
46
|
+
return await waitForLocalEmbeddingModel(modelName, cached, signal);
|
|
47
|
+
} finally {
|
|
48
|
+
cached.waiters--;
|
|
49
|
+
deletePendingLocalModelCache(modelName, cached);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const modelPromise = (async () => {
|
|
53
|
+
console.log(`Loading local embedding model (${modelName})...`);
|
|
54
|
+
const { pipeline } = await import('@huggingface/transformers');
|
|
55
|
+
const model = await pipeline("feature-extraction", modelName);
|
|
56
|
+
console.log("Local model loaded successfully");
|
|
57
|
+
return model;
|
|
58
|
+
})();
|
|
59
|
+
const entry = {
|
|
60
|
+
promise: modelPromise,
|
|
61
|
+
settled: false,
|
|
62
|
+
waiters: 1
|
|
63
|
+
};
|
|
64
|
+
localModelCacheByModel.set(modelName, entry);
|
|
65
|
+
modelPromise.then(() => {
|
|
66
|
+
entry.settled = true;
|
|
67
|
+
}).catch(() => {
|
|
68
|
+
localModelCacheByModel.delete(modelName);
|
|
69
|
+
});
|
|
70
|
+
try {
|
|
71
|
+
return await waitForLocalEmbeddingModel(modelName, entry, signal);
|
|
72
|
+
} finally {
|
|
73
|
+
entry.waiters--;
|
|
74
|
+
deletePendingLocalModelCache(modelName, entry);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function waitForLocalEmbeddingModel(modelName, entry, signal) {
|
|
78
|
+
if (signal.aborted) {
|
|
79
|
+
deletePendingLocalModelCache(modelName, entry);
|
|
80
|
+
throw providerError("local", "model inference was aborted");
|
|
81
|
+
}
|
|
82
|
+
let rejectAbort = () => {
|
|
83
|
+
};
|
|
84
|
+
const abortPromise = new Promise((_resolve, reject) => {
|
|
85
|
+
rejectAbort = reject;
|
|
86
|
+
});
|
|
87
|
+
const onAbort = () => {
|
|
88
|
+
rejectAbort(providerError("local", "model inference was aborted"));
|
|
89
|
+
};
|
|
90
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
91
|
+
try {
|
|
92
|
+
return await Promise.race([entry.promise, abortPromise]);
|
|
93
|
+
} finally {
|
|
94
|
+
signal.removeEventListener("abort", onAbort);
|
|
38
95
|
}
|
|
39
|
-
console.log(`Loading local embedding model (${modelName})...`);
|
|
40
|
-
const { pipeline } = await import('@xenova/transformers');
|
|
41
|
-
const model = await pipeline("feature-extraction", modelName);
|
|
42
|
-
localModelCacheByModel.set(modelName, { model });
|
|
43
|
-
console.log("Local model loaded successfully");
|
|
44
|
-
return model;
|
|
45
96
|
}
|
|
46
97
|
function getPositiveInteger(value, optionName) {
|
|
47
98
|
if (!Number.isInteger(value) || value <= 0) {
|
|
@@ -59,6 +110,7 @@ function resolveProviderName(provider) {
|
|
|
59
110
|
case "openai":
|
|
60
111
|
case "mistral":
|
|
61
112
|
case "cloudflare":
|
|
113
|
+
case "openai-compatible":
|
|
62
114
|
return provider ?? "local";
|
|
63
115
|
default:
|
|
64
116
|
throw new Error(`Unknown embedding provider: ${String(provider)}`);
|
|
@@ -83,6 +135,13 @@ function getOptionalTrimmedCredential(value) {
|
|
|
83
135
|
const trimmed = value?.trim();
|
|
84
136
|
return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
|
|
85
137
|
}
|
|
138
|
+
function getRequiredTrimmedString(value, optionName) {
|
|
139
|
+
const trimmed = getOptionalTrimmedCredential(value);
|
|
140
|
+
if (!trimmed) {
|
|
141
|
+
throw new Error(`Invalid ${optionName}: expected a non-empty string`);
|
|
142
|
+
}
|
|
143
|
+
return trimmed;
|
|
144
|
+
}
|
|
86
145
|
function hasOwnProperty(value, property) {
|
|
87
146
|
return Object.prototype.hasOwnProperty.call(value, property);
|
|
88
147
|
}
|
|
@@ -243,7 +302,27 @@ async function embedSequentially(provider, operation, texts, signal, embedOne) {
|
|
|
243
302
|
}
|
|
244
303
|
return results;
|
|
245
304
|
}
|
|
246
|
-
function
|
|
305
|
+
function normalizeOpenAICompatibleEmbeddingsUrl(baseUrl) {
|
|
306
|
+
let url;
|
|
307
|
+
try {
|
|
308
|
+
url = new URL(baseUrl);
|
|
309
|
+
} catch {
|
|
310
|
+
throw new Error("Invalid baseUrl: expected an absolute http or https URL");
|
|
311
|
+
}
|
|
312
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
313
|
+
throw new Error("Invalid baseUrl: expected an absolute http or https URL");
|
|
314
|
+
}
|
|
315
|
+
if (url.username || url.password) {
|
|
316
|
+
throw new Error("Invalid baseUrl: URL credentials are not allowed");
|
|
317
|
+
}
|
|
318
|
+
if (url.search || url.hash) {
|
|
319
|
+
throw new Error("Invalid baseUrl: query strings and fragments are not allowed");
|
|
320
|
+
}
|
|
321
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
322
|
+
url.pathname = path.endsWith("/embeddings") ? path : `${path}/embeddings`;
|
|
323
|
+
return url.toString();
|
|
324
|
+
}
|
|
325
|
+
function createProviderMetadata(provider, dimensions, model) {
|
|
247
326
|
switch (provider) {
|
|
248
327
|
case "local":
|
|
249
328
|
return Object.freeze({
|
|
@@ -280,9 +359,25 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
280
359
|
dimensions: CLOUDFLARE_DIMENSIONS,
|
|
281
360
|
batch: Object.freeze({ mode: "native" })
|
|
282
361
|
});
|
|
362
|
+
case "openai-compatible":
|
|
363
|
+
return Object.freeze({
|
|
364
|
+
name: "openai-compatible",
|
|
365
|
+
model: getRequiredTrimmedString(model, "model"),
|
|
366
|
+
dimensions,
|
|
367
|
+
batch: Object.freeze({ mode: "native" })
|
|
368
|
+
});
|
|
283
369
|
}
|
|
284
370
|
}
|
|
285
371
|
function getEffectiveDimensions(provider, dimensions) {
|
|
372
|
+
if (provider === "local") {
|
|
373
|
+
if (dimensions !== void 0 && dimensions !== LOCAL_DIMENSIONS) {
|
|
374
|
+
throw providerError(
|
|
375
|
+
"local",
|
|
376
|
+
`${LOCAL_MODEL} returns ${LOCAL_DIMENSIONS} dimensions; received dimensions ${String(dimensions)}`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
return LOCAL_DIMENSIONS;
|
|
380
|
+
}
|
|
286
381
|
if (provider === "gemini") {
|
|
287
382
|
const effectiveDimensions = dimensions ?? GEMINI_DIMENSIONS;
|
|
288
383
|
if (!Number.isInteger(effectiveDimensions) || effectiveDimensions < GEMINI_MIN_DIMENSIONS || effectiveDimensions > GEMINI_MAX_DIMENSIONS) {
|
|
@@ -311,7 +406,13 @@ function getEffectiveDimensions(provider, dimensions) {
|
|
|
311
406
|
}
|
|
312
407
|
return CLOUDFLARE_DIMENSIONS;
|
|
313
408
|
}
|
|
314
|
-
|
|
409
|
+
if (provider === "openai-compatible") {
|
|
410
|
+
if (dimensions === void 0) {
|
|
411
|
+
throw new Error("Invalid dimensions: expected a positive integer");
|
|
412
|
+
}
|
|
413
|
+
return getPositiveInteger(dimensions, "dimensions");
|
|
414
|
+
}
|
|
415
|
+
return getPositiveInteger(dimensions ?? OPENAI_DEFAULT_DIMENSIONS, "dimensions");
|
|
315
416
|
}
|
|
316
417
|
function assertBatchSize(metadata, count) {
|
|
317
418
|
if (metadata.batch.maxSize !== void 0 && count > metadata.batch.maxSize) {
|
|
@@ -321,6 +422,13 @@ function assertBatchSize(metadata, count) {
|
|
|
321
422
|
);
|
|
322
423
|
}
|
|
323
424
|
}
|
|
425
|
+
function chunkTexts(texts, batchSize) {
|
|
426
|
+
const chunks = [];
|
|
427
|
+
for (let index = 0; index < texts.length; index += batchSize) {
|
|
428
|
+
chunks.push(texts.slice(index, index + batchSize));
|
|
429
|
+
}
|
|
430
|
+
return chunks;
|
|
431
|
+
}
|
|
324
432
|
function createEmbeddingBatchResult(metadata, intent, embeddings) {
|
|
325
433
|
return Object.freeze({
|
|
326
434
|
embeddings,
|
|
@@ -348,14 +456,13 @@ class LocalEmbeddingProvider {
|
|
|
348
456
|
"model inference",
|
|
349
457
|
this.#timeoutMs,
|
|
350
458
|
async (signal) => {
|
|
351
|
-
const model = await getLocalEmbeddingModel(this.metadata.model);
|
|
459
|
+
const model = await getLocalEmbeddingModel(this.metadata.model, signal);
|
|
352
460
|
return embedSequentially("local", "model inference", texts, signal, async (text) => {
|
|
353
461
|
const output = await model(text, {
|
|
354
462
|
pooling: "mean",
|
|
355
463
|
normalize: true
|
|
356
464
|
});
|
|
357
|
-
|
|
358
|
-
return padEmbedding(embedding, this.metadata.dimensions);
|
|
465
|
+
return Array.from(output.data);
|
|
359
466
|
});
|
|
360
467
|
},
|
|
361
468
|
options.signal
|
|
@@ -422,18 +529,23 @@ async function requestOpenAICompatibleEmbeddings(request) {
|
|
|
422
529
|
if (request.encodingFormat !== void 0) {
|
|
423
530
|
body.encoding_format = request.encodingFormat;
|
|
424
531
|
}
|
|
532
|
+
const headers = {
|
|
533
|
+
"Content-Type": "application/json"
|
|
534
|
+
};
|
|
535
|
+
if (request.apiKey !== void 0) {
|
|
536
|
+
headers.Authorization = `Bearer ${request.apiKey}`;
|
|
537
|
+
}
|
|
425
538
|
const response = await fetch(request.endpoint, {
|
|
426
539
|
method: "POST",
|
|
427
|
-
headers
|
|
428
|
-
"Content-Type": "application/json",
|
|
429
|
-
"Authorization": `Bearer ${request.apiKey}`
|
|
430
|
-
},
|
|
540
|
+
headers,
|
|
431
541
|
signal: request.signal,
|
|
432
|
-
body: JSON.stringify(body)
|
|
542
|
+
body: JSON.stringify(body),
|
|
543
|
+
...request.redirect ? { redirect: request.redirect } : {}
|
|
433
544
|
});
|
|
434
545
|
if (!response.ok) {
|
|
435
|
-
const
|
|
436
|
-
const
|
|
546
|
+
const statusHeader = request.statusHeader ?? { name: "x-request-id", label: "request" };
|
|
547
|
+
const statusHeaderValue = getSafeResponseHeader(response, statusHeader.name);
|
|
548
|
+
const context = statusHeaderValue ? ` status ${response.status}, ${statusHeader.label} ${statusHeaderValue}` : ` status ${response.status}`;
|
|
437
549
|
throw new Error(context);
|
|
438
550
|
}
|
|
439
551
|
const data = await response.json();
|
|
@@ -457,92 +569,13 @@ async function requestOpenAICompatibleEmbeddings(request) {
|
|
|
457
569
|
return parsed;
|
|
458
570
|
});
|
|
459
571
|
}
|
|
460
|
-
class
|
|
461
|
-
metadata;
|
|
462
|
-
#apiKey;
|
|
463
|
-
#timeoutMs;
|
|
464
|
-
constructor(metadata, apiKey, timeoutMs) {
|
|
465
|
-
this.metadata = metadata;
|
|
466
|
-
this.#apiKey = apiKey;
|
|
467
|
-
this.#timeoutMs = timeoutMs;
|
|
468
|
-
}
|
|
469
|
-
async embed(texts, options = {}) {
|
|
470
|
-
const intent = resolveIntent(options.intent);
|
|
471
|
-
if (texts.length === 0) {
|
|
472
|
-
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
473
|
-
}
|
|
474
|
-
assertBatchSize(this.metadata, texts.length);
|
|
475
|
-
const items = await withTimeout(
|
|
476
|
-
"openai",
|
|
477
|
-
"API request",
|
|
478
|
-
this.#timeoutMs,
|
|
479
|
-
async (signal) => requestOpenAICompatibleEmbeddings({
|
|
480
|
-
responseLabel: "OpenAI",
|
|
481
|
-
endpoint: OPENAI_EMBEDDINGS_URL,
|
|
482
|
-
apiKey: this.#apiKey,
|
|
483
|
-
model: this.metadata.model,
|
|
484
|
-
texts,
|
|
485
|
-
signal,
|
|
486
|
-
dimensions: this.metadata.dimensions
|
|
487
|
-
}),
|
|
488
|
-
options.signal,
|
|
489
|
-
[this.#apiKey]
|
|
490
|
-
);
|
|
491
|
-
return createEmbeddingBatchResult(
|
|
492
|
-
this.metadata,
|
|
493
|
-
intent,
|
|
494
|
-
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "openai")
|
|
495
|
-
);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
class MistralEmbeddingProvider {
|
|
499
|
-
metadata;
|
|
500
|
-
#apiKey;
|
|
501
|
-
#timeoutMs;
|
|
502
|
-
constructor(metadata, apiKey, timeoutMs) {
|
|
503
|
-
this.metadata = metadata;
|
|
504
|
-
this.#apiKey = apiKey;
|
|
505
|
-
this.#timeoutMs = timeoutMs;
|
|
506
|
-
}
|
|
507
|
-
async embed(texts, options = {}) {
|
|
508
|
-
const intent = resolveIntent(options.intent);
|
|
509
|
-
if (texts.length === 0) {
|
|
510
|
-
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
511
|
-
}
|
|
512
|
-
assertBatchSize(this.metadata, texts.length);
|
|
513
|
-
const items = await withTimeout(
|
|
514
|
-
"mistral",
|
|
515
|
-
"API request",
|
|
516
|
-
this.#timeoutMs,
|
|
517
|
-
async (signal) => requestOpenAICompatibleEmbeddings({
|
|
518
|
-
responseLabel: "Mistral",
|
|
519
|
-
endpoint: MISTRAL_EMBEDDINGS_URL,
|
|
520
|
-
apiKey: this.#apiKey,
|
|
521
|
-
model: this.metadata.model,
|
|
522
|
-
texts,
|
|
523
|
-
signal,
|
|
524
|
-
encodingFormat: "float",
|
|
525
|
-
requireIndex: true
|
|
526
|
-
}),
|
|
527
|
-
options.signal,
|
|
528
|
-
[this.#apiKey]
|
|
529
|
-
);
|
|
530
|
-
return createEmbeddingBatchResult(
|
|
531
|
-
this.metadata,
|
|
532
|
-
intent,
|
|
533
|
-
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "mistral")
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
class CloudflareEmbeddingProvider {
|
|
572
|
+
class OpenAICompatibleEmbeddingProvider {
|
|
538
573
|
metadata;
|
|
539
|
-
#accountId;
|
|
540
|
-
#apiToken;
|
|
541
574
|
#timeoutMs;
|
|
542
|
-
|
|
575
|
+
#profile;
|
|
576
|
+
constructor(metadata, profile, timeoutMs) {
|
|
543
577
|
this.metadata = metadata;
|
|
544
|
-
this.#
|
|
545
|
-
this.#apiToken = apiToken;
|
|
578
|
+
this.#profile = profile;
|
|
546
579
|
this.#timeoutMs = timeoutMs;
|
|
547
580
|
}
|
|
548
581
|
async embed(texts, options = {}) {
|
|
@@ -551,53 +584,45 @@ class CloudflareEmbeddingProvider {
|
|
|
551
584
|
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
552
585
|
}
|
|
553
586
|
assertBatchSize(this.metadata, texts.length);
|
|
554
|
-
const endpoint = createCloudflareEmbeddingsUrl(this.#accountId);
|
|
555
587
|
const items = await withTimeout(
|
|
556
|
-
|
|
588
|
+
this.#profile.provider,
|
|
557
589
|
"API request",
|
|
558
590
|
this.#timeoutMs,
|
|
559
591
|
async (signal) => {
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
model: this.
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
592
|
+
const batches = this.#profile.batchSize === void 0 ? [texts] : chunkTexts(texts, this.#profile.batchSize);
|
|
593
|
+
const results = [];
|
|
594
|
+
for (const batch of batches) {
|
|
595
|
+
throwIfAborted(this.#profile.provider, "API request", signal);
|
|
596
|
+
const batchItems = await requestOpenAICompatibleEmbeddings({
|
|
597
|
+
responseLabel: this.#profile.responseLabel,
|
|
598
|
+
endpoint: this.#profile.endpoint,
|
|
599
|
+
...this.#profile.apiKey !== void 0 ? { apiKey: this.#profile.apiKey } : {},
|
|
600
|
+
model: this.#profile.model,
|
|
601
|
+
texts: batch,
|
|
602
|
+
signal,
|
|
603
|
+
...this.#profile.includeDimensions ? { dimensions: this.#profile.dimensions } : {},
|
|
604
|
+
...this.#profile.encodingFormat ? { encodingFormat: this.#profile.encodingFormat } : {},
|
|
605
|
+
...this.#profile.requireIndex !== void 0 ? { requireIndex: this.#profile.requireIndex } : {},
|
|
606
|
+
...this.#profile.redirect ? { redirect: this.#profile.redirect } : {},
|
|
607
|
+
...this.#profile.statusHeader ? { statusHeader: this.#profile.statusHeader } : {}
|
|
608
|
+
});
|
|
609
|
+
results.push(...validateEmbeddingBatch(
|
|
610
|
+
batchItems,
|
|
611
|
+
batch.length,
|
|
612
|
+
this.metadata.dimensions,
|
|
613
|
+
this.#profile.provider
|
|
614
|
+
));
|
|
615
|
+
throwIfAborted(this.#profile.provider, "API request", signal);
|
|
580
616
|
}
|
|
581
|
-
return
|
|
582
|
-
const typed = item;
|
|
583
|
-
const embedding = typed.embedding;
|
|
584
|
-
if (!Array.isArray(embedding)) {
|
|
585
|
-
throw new Error("Cloudflare response item did not include an embedding array");
|
|
586
|
-
}
|
|
587
|
-
const parsed = { embedding };
|
|
588
|
-
if (hasOwnProperty(typed, "index")) {
|
|
589
|
-
parsed.index = typed.index;
|
|
590
|
-
}
|
|
591
|
-
return parsed;
|
|
592
|
-
});
|
|
617
|
+
return results;
|
|
593
618
|
},
|
|
594
619
|
options.signal,
|
|
595
|
-
|
|
620
|
+
this.#profile.exactSecrets ?? []
|
|
596
621
|
);
|
|
597
622
|
return createEmbeddingBatchResult(
|
|
598
623
|
this.metadata,
|
|
599
624
|
intent,
|
|
600
|
-
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions,
|
|
625
|
+
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, this.#profile.provider)
|
|
601
626
|
);
|
|
602
627
|
}
|
|
603
628
|
}
|
|
@@ -622,7 +647,16 @@ function createEmbeddingProvider(options = {}) {
|
|
|
622
647
|
if (!key) {
|
|
623
648
|
throw new Error("OPENAI_API_KEY is required for OpenAI embeddings");
|
|
624
649
|
}
|
|
625
|
-
return new
|
|
650
|
+
return new OpenAICompatibleEmbeddingProvider(metadata, {
|
|
651
|
+
provider: "openai",
|
|
652
|
+
responseLabel: "OpenAI",
|
|
653
|
+
endpoint: OPENAI_EMBEDDINGS_URL,
|
|
654
|
+
apiKey: key,
|
|
655
|
+
model: metadata.model,
|
|
656
|
+
dimensions: metadata.dimensions,
|
|
657
|
+
includeDimensions: true,
|
|
658
|
+
exactSecrets: [key]
|
|
659
|
+
}, timeoutMs);
|
|
626
660
|
}
|
|
627
661
|
case "mistral": {
|
|
628
662
|
const key = getOptionalTrimmedCredential(
|
|
@@ -631,7 +665,17 @@ function createEmbeddingProvider(options = {}) {
|
|
|
631
665
|
if (!key) {
|
|
632
666
|
throw new Error("MISTRAL_API_KEY is required for Mistral embeddings");
|
|
633
667
|
}
|
|
634
|
-
return new
|
|
668
|
+
return new OpenAICompatibleEmbeddingProvider(metadata, {
|
|
669
|
+
provider: "mistral",
|
|
670
|
+
responseLabel: "Mistral",
|
|
671
|
+
endpoint: MISTRAL_EMBEDDINGS_URL,
|
|
672
|
+
apiKey: key,
|
|
673
|
+
model: metadata.model,
|
|
674
|
+
dimensions: metadata.dimensions,
|
|
675
|
+
encodingFormat: "float",
|
|
676
|
+
requireIndex: true,
|
|
677
|
+
exactSecrets: [key]
|
|
678
|
+
}, timeoutMs);
|
|
635
679
|
}
|
|
636
680
|
case "cloudflare": {
|
|
637
681
|
const accountId = getOptionalTrimmedCredential(
|
|
@@ -646,13 +690,56 @@ function createEmbeddingProvider(options = {}) {
|
|
|
646
690
|
if (!apiToken) {
|
|
647
691
|
throw new Error("CLOUDFLARE_API_TOKEN is required for Cloudflare embeddings");
|
|
648
692
|
}
|
|
649
|
-
|
|
693
|
+
const endpoint = createCloudflareEmbeddingsUrl(accountId);
|
|
694
|
+
return new OpenAICompatibleEmbeddingProvider(metadata, {
|
|
695
|
+
provider: "cloudflare",
|
|
696
|
+
responseLabel: "Cloudflare",
|
|
697
|
+
endpoint,
|
|
698
|
+
apiKey: apiToken,
|
|
699
|
+
model: metadata.model,
|
|
700
|
+
dimensions: metadata.dimensions,
|
|
701
|
+
statusHeader: { name: "cf-ray", label: "cf-ray" },
|
|
702
|
+
exactSecrets: [apiToken, accountId, encodeURIComponent(accountId), endpoint]
|
|
703
|
+
}, timeoutMs);
|
|
704
|
+
}
|
|
705
|
+
case "openai-compatible": {
|
|
706
|
+
const baseUrl = getRequiredTrimmedString(options.baseUrl, "baseUrl");
|
|
707
|
+
const endpoint = normalizeOpenAICompatibleEmbeddingsUrl(baseUrl);
|
|
708
|
+
const apiKey = getOptionalTrimmedCredential(options.apiKey);
|
|
709
|
+
const batchSize = getPositiveInteger(
|
|
710
|
+
options.batchSize ?? OPENAI_COMPATIBLE_DEFAULT_BATCH_SIZE,
|
|
711
|
+
"batchSize"
|
|
712
|
+
);
|
|
713
|
+
return new OpenAICompatibleEmbeddingProvider(metadata, {
|
|
714
|
+
provider: "openai-compatible",
|
|
715
|
+
responseLabel: "OpenAI-compatible",
|
|
716
|
+
endpoint,
|
|
717
|
+
...apiKey ? { apiKey } : {},
|
|
718
|
+
model: metadata.model,
|
|
719
|
+
dimensions: metadata.dimensions,
|
|
720
|
+
includeDimensions: true,
|
|
721
|
+
encodingFormat: "float",
|
|
722
|
+
requireIndex: true,
|
|
723
|
+
redirect: "error",
|
|
724
|
+
batchSize,
|
|
725
|
+
exactSecrets: [
|
|
726
|
+
...apiKey ? [apiKey] : [],
|
|
727
|
+
baseUrl,
|
|
728
|
+
endpoint
|
|
729
|
+
]
|
|
730
|
+
}, timeoutMs);
|
|
650
731
|
}
|
|
651
732
|
}
|
|
652
733
|
}
|
|
653
734
|
function getEmbeddingProviderMetadata(options = {}) {
|
|
654
735
|
const provider = resolveProviderName(options.provider);
|
|
655
|
-
|
|
736
|
+
if (provider === "openai-compatible") {
|
|
737
|
+
normalizeOpenAICompatibleEmbeddingsUrl(getRequiredTrimmedString(options.baseUrl, "baseUrl"));
|
|
738
|
+
if (options.batchSize !== void 0) {
|
|
739
|
+
getPositiveInteger(options.batchSize, "batchSize");
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return createProviderMetadata(provider, getEffectiveDimensions(provider, options.dimensions), options.model);
|
|
656
743
|
}
|
|
657
744
|
async function generateEmbeddings(texts, options = {}) {
|
|
658
745
|
const maxLength = getPositiveInteger(options.maxLength ?? DEFAULT_MAX_LENGTH, "maxLength");
|
|
@@ -822,7 +909,7 @@ async function insertDocument(client, document, quotedTableName) {
|
|
|
822
909
|
]
|
|
823
910
|
});
|
|
824
911
|
}
|
|
825
|
-
async function createTable(client, tableName = "articles", dimensions =
|
|
912
|
+
async function createTable(client, tableName = "articles", dimensions = 384) {
|
|
826
913
|
const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
|
|
827
914
|
const vectorDimensions = normalizeVectorDimensions(dimensions);
|
|
828
915
|
const quotedEmbeddingIndexName = quoteSqlIdentifier(`${tableName}_embedding_idx`, "embedding index name");
|
package/docs/API.md
CHANGED
|
@@ -42,13 +42,13 @@ It also exports these types:
|
|
|
42
42
|
Creates the table and supporting indexes used by search.
|
|
43
43
|
|
|
44
44
|
```ts
|
|
45
|
-
await createTable(client
|
|
45
|
+
await createTable(client);
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
Defaults:
|
|
49
49
|
|
|
50
50
|
- `tableName`: `"articles"`
|
|
51
|
-
- `dimensions`: `
|
|
51
|
+
- `dimensions`: `384`
|
|
52
52
|
|
|
53
53
|
`tableName` must be an ASCII SQLite identifier matching
|
|
54
54
|
`[A-Za-z_][A-Za-z0-9_]*`. Valid identifiers are quoted internally, so reserved
|
|
@@ -216,6 +216,10 @@ credentials or configuration.
|
|
|
216
216
|
Gemini uses `gemini-embedding-2`. Its default is 3072 dimensions, and explicit
|
|
217
217
|
Gemini dimensions must be an integer from 128 through 3072.
|
|
218
218
|
|
|
219
|
+
Local embeddings use `Xenova/all-MiniLM-L6-v2` through
|
|
220
|
+
`@huggingface/transformers` and are fixed at the model's native 384 dimensions.
|
|
221
|
+
Passing any other local dimension is rejected before the runtime is loaded.
|
|
222
|
+
|
|
219
223
|
### `getEmbeddingProviderMetadata(options?)`
|
|
220
224
|
|
|
221
225
|
Returns the same metadata exposed by `createEmbeddingProvider(options).metadata`
|
|
@@ -242,7 +246,13 @@ Provider clients return:
|
|
|
242
246
|
```ts
|
|
243
247
|
interface EmbeddingBatchResult {
|
|
244
248
|
embeddings: number[][];
|
|
245
|
-
provider:
|
|
249
|
+
provider:
|
|
250
|
+
| "local"
|
|
251
|
+
| "cloudflare"
|
|
252
|
+
| "mistral"
|
|
253
|
+
| "gemini"
|
|
254
|
+
| "openai"
|
|
255
|
+
| "openai-compatible";
|
|
246
256
|
model: string;
|
|
247
257
|
dimensions: number;
|
|
248
258
|
intent: "document" | "query";
|
|
@@ -258,10 +268,19 @@ cardinality, dimensions, finite numeric values, and indexed batch ordering.
|
|
|
258
268
|
|
|
259
269
|
```ts
|
|
260
270
|
interface EmbeddingOptions {
|
|
261
|
-
provider?:
|
|
271
|
+
provider?:
|
|
272
|
+
| "local"
|
|
273
|
+
| "cloudflare"
|
|
274
|
+
| "mistral"
|
|
275
|
+
| "gemini"
|
|
276
|
+
| "openai"
|
|
277
|
+
| "openai-compatible";
|
|
262
278
|
apiKey?: string;
|
|
263
279
|
accountId?: string;
|
|
264
280
|
apiToken?: string;
|
|
281
|
+
baseUrl?: string;
|
|
282
|
+
model?: string;
|
|
283
|
+
batchSize?: number;
|
|
265
284
|
dimensions?: number;
|
|
266
285
|
maxLength?: number;
|
|
267
286
|
intent?: "document" | "query";
|
|
@@ -275,10 +294,20 @@ interface EmbeddingOptions {
|
|
|
275
294
|
Cloudflare uses `accountId` and `apiToken`, which fall back to
|
|
276
295
|
`CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`.
|
|
277
296
|
|
|
297
|
+
For `provider: "openai-compatible"`, callers must pass `baseUrl`, `model`, and
|
|
298
|
+
`dimensions`. `apiKey` is optional and is used only when explicitly provided;
|
|
299
|
+
the provider never reads `OPENAI_API_KEY`. `batchSize` controls the maximum
|
|
300
|
+
items per outbound request and defaults to `32`. The endpoint is treated as
|
|
301
|
+
trusted server-side configuration and should not be derived from untrusted
|
|
302
|
+
request input.
|
|
303
|
+
|
|
278
304
|
### `padEmbedding(embedding, targetDimensions)`
|
|
279
305
|
|
|
280
306
|
Pads or truncates an embedding array to the requested length.
|
|
281
307
|
|
|
308
|
+
This helper remains exported for callers that used it directly. The local
|
|
309
|
+
provider does not use it; local vectors are validated at native 384 dimensions.
|
|
310
|
+
|
|
282
311
|
### `prepareTextForEmbedding(fields)`
|
|
283
312
|
|
|
284
313
|
Combines title, description, tags, and content into the text sent to the
|
package/docs/INDEXING.md
CHANGED
|
@@ -25,7 +25,6 @@ await indexContent({
|
|
|
25
25
|
tableName: "articles",
|
|
26
26
|
embeddingOptions: {
|
|
27
27
|
provider: "local",
|
|
28
|
-
dimensions: 768,
|
|
29
28
|
},
|
|
30
29
|
});
|
|
31
30
|
```
|
|
@@ -34,6 +33,11 @@ That keeps the implementation simple, but it also means a failed rebuild can
|
|
|
34
33
|
leave the index partially repopulated.
|
|
35
34
|
|
|
36
35
|
Changing an embedding provider or dimension count requires a full re-embed.
|
|
36
|
+
For existing local indexes created with the older padded-local behavior, create
|
|
37
|
+
or recreate a 384-dimensional table before rebuilding. Those older local
|
|
38
|
+
768-dimensional tables stored 384 model values followed by zero padding;
|
|
39
|
+
`indexContent()` clears rows but does not change the table's `F32_BLOB` width.
|
|
40
|
+
|
|
37
41
|
For Gemini specifically, indexes created with the retired `text-embedding-004`
|
|
38
42
|
model must be rebuilt for `gemini-embedding-2` even when staying at 768
|
|
39
43
|
dimensions, because the model and query/document formatting both changed. If
|
|
@@ -72,7 +76,7 @@ database calls or embedding generation.
|
|
|
72
76
|
|
|
73
77
|
## Runtime Notes
|
|
74
78
|
|
|
75
|
-
- local embeddings may download a model on the first run
|
|
79
|
+
- local embeddings may download and cache a model on the first run
|
|
76
80
|
- Node users need `@libsql/client` installed alongside the package
|
|
77
81
|
- the repository validates both the npm package build and `deno check`, but the
|
|
78
82
|
indexing flow itself still depends on filesystem access
|
package/docs/INTEGRATIONS.md
CHANGED
|
@@ -26,7 +26,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
26
26
|
limit,
|
|
27
27
|
embeddingOptions: {
|
|
28
28
|
provider: "local",
|
|
29
|
-
dimensions: 768,
|
|
30
29
|
},
|
|
31
30
|
});
|
|
32
31
|
|
|
@@ -79,7 +78,6 @@ export async function POST(request: NextRequest) {
|
|
|
79
78
|
limit,
|
|
80
79
|
embeddingOptions: {
|
|
81
80
|
provider: "local",
|
|
82
|
-
dimensions: 768,
|
|
83
81
|
},
|
|
84
82
|
});
|
|
85
83
|
|
|
@@ -134,13 +132,21 @@ const embeddingProvider =
|
|
|
134
132
|
| "mistral"
|
|
135
133
|
| "gemini"
|
|
136
134
|
| "openai"
|
|
135
|
+
| "openai-compatible"
|
|
137
136
|
| undefined;
|
|
137
|
+
|
|
138
|
+
const dimensionsByProvider = {
|
|
139
|
+
local: 384,
|
|
140
|
+
cloudflare: 1024,
|
|
141
|
+
mistral: 1024,
|
|
142
|
+
gemini: 3072,
|
|
143
|
+
openai: 1536,
|
|
144
|
+
} as const;
|
|
145
|
+
|
|
138
146
|
const embeddingDimensions =
|
|
139
|
-
embeddingProvider === "
|
|
140
|
-
?
|
|
141
|
-
: embeddingProvider
|
|
142
|
-
? 3072
|
|
143
|
-
: 768;
|
|
147
|
+
embeddingProvider === "openai-compatible"
|
|
148
|
+
? Number(process.env.EMBEDDING_DIMENSIONS)
|
|
149
|
+
: dimensionsByProvider[embeddingProvider ?? "local"];
|
|
144
150
|
|
|
145
151
|
await createTable(client, "articles", embeddingDimensions);
|
|
146
152
|
|
|
@@ -149,6 +155,8 @@ await indexContent({
|
|
|
149
155
|
contentPath: "./content",
|
|
150
156
|
embeddingOptions: {
|
|
151
157
|
provider: embeddingProvider,
|
|
158
|
+
baseUrl: process.env.EMBEDDING_BASE_URL,
|
|
159
|
+
model: process.env.EMBEDDING_MODEL,
|
|
152
160
|
dimensions: embeddingDimensions,
|
|
153
161
|
},
|
|
154
162
|
});
|
|
@@ -156,3 +164,9 @@ await indexContent({
|
|
|
156
164
|
|
|
157
165
|
Pair this with your framework build command so indexed content and deployed code
|
|
158
166
|
stay in sync.
|
|
167
|
+
|
|
168
|
+
When you use `openai-compatible`, keep `EMBEDDING_BASE_URL` as trusted
|
|
169
|
+
server-side configuration, not user request input. Recreate the vector table or
|
|
170
|
+
rebuild into a separate table whenever the provider, endpoint, model, or
|
|
171
|
+
dimension count changes; stored vectors and query vectors must come from the
|
|
172
|
+
same embedding space.
|