libsql-search 0.6.0 → 0.7.1
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 +26 -47
- package/dist/index.cjs +160 -131
- package/dist/index.d.ts +6 -2
- package/dist/index.esm.js +160 -131
- package/docs/API.md +125 -96
- package/docs/INDEXING.md +12 -25
- package/docs/INTEGRATIONS.md +221 -29
- package/docs/MIGRATIONS.md +174 -0
- package/docs/PROVIDERS.md +112 -175
- package/docs/README.md +7 -9
- package/docs/TESTING.md +138 -0
- package/package.json +1 -1
package/dist/index.esm.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join, extname, relative, dirname } from 'path';
|
|
|
3
3
|
import matter from 'gray-matter';
|
|
4
4
|
|
|
5
5
|
const OPENAI_DEFAULT_DIMENSIONS = 768;
|
|
6
|
+
const OPENAI_COMPATIBLE_DEFAULT_BATCH_SIZE = 32;
|
|
6
7
|
const LOCAL_DIMENSIONS = 384;
|
|
7
8
|
const DEFAULT_MAX_LENGTH = 8e3;
|
|
8
9
|
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
@@ -109,6 +110,7 @@ function resolveProviderName(provider) {
|
|
|
109
110
|
case "openai":
|
|
110
111
|
case "mistral":
|
|
111
112
|
case "cloudflare":
|
|
113
|
+
case "openai-compatible":
|
|
112
114
|
return provider ?? "local";
|
|
113
115
|
default:
|
|
114
116
|
throw new Error(`Unknown embedding provider: ${String(provider)}`);
|
|
@@ -133,6 +135,13 @@ function getOptionalTrimmedCredential(value) {
|
|
|
133
135
|
const trimmed = value?.trim();
|
|
134
136
|
return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
|
|
135
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
|
+
}
|
|
136
145
|
function hasOwnProperty(value, property) {
|
|
137
146
|
return Object.prototype.hasOwnProperty.call(value, property);
|
|
138
147
|
}
|
|
@@ -293,7 +302,27 @@ async function embedSequentially(provider, operation, texts, signal, embedOne) {
|
|
|
293
302
|
}
|
|
294
303
|
return results;
|
|
295
304
|
}
|
|
296
|
-
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) {
|
|
297
326
|
switch (provider) {
|
|
298
327
|
case "local":
|
|
299
328
|
return Object.freeze({
|
|
@@ -330,6 +359,13 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
330
359
|
dimensions: CLOUDFLARE_DIMENSIONS,
|
|
331
360
|
batch: Object.freeze({ mode: "native" })
|
|
332
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
|
+
});
|
|
333
369
|
}
|
|
334
370
|
}
|
|
335
371
|
function getEffectiveDimensions(provider, dimensions) {
|
|
@@ -370,6 +406,12 @@ function getEffectiveDimensions(provider, dimensions) {
|
|
|
370
406
|
}
|
|
371
407
|
return CLOUDFLARE_DIMENSIONS;
|
|
372
408
|
}
|
|
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
|
+
}
|
|
373
415
|
return getPositiveInteger(dimensions ?? OPENAI_DEFAULT_DIMENSIONS, "dimensions");
|
|
374
416
|
}
|
|
375
417
|
function assertBatchSize(metadata, count) {
|
|
@@ -380,6 +422,13 @@ function assertBatchSize(metadata, count) {
|
|
|
380
422
|
);
|
|
381
423
|
}
|
|
382
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
|
+
}
|
|
383
432
|
function createEmbeddingBatchResult(metadata, intent, embeddings) {
|
|
384
433
|
return Object.freeze({
|
|
385
434
|
embeddings,
|
|
@@ -480,18 +529,23 @@ async function requestOpenAICompatibleEmbeddings(request) {
|
|
|
480
529
|
if (request.encodingFormat !== void 0) {
|
|
481
530
|
body.encoding_format = request.encodingFormat;
|
|
482
531
|
}
|
|
532
|
+
const headers = {
|
|
533
|
+
"Content-Type": "application/json"
|
|
534
|
+
};
|
|
535
|
+
if (request.apiKey !== void 0) {
|
|
536
|
+
headers.Authorization = `Bearer ${request.apiKey}`;
|
|
537
|
+
}
|
|
483
538
|
const response = await fetch(request.endpoint, {
|
|
484
539
|
method: "POST",
|
|
485
|
-
headers
|
|
486
|
-
"Content-Type": "application/json",
|
|
487
|
-
"Authorization": `Bearer ${request.apiKey}`
|
|
488
|
-
},
|
|
540
|
+
headers,
|
|
489
541
|
signal: request.signal,
|
|
490
|
-
body: JSON.stringify(body)
|
|
542
|
+
body: JSON.stringify(body),
|
|
543
|
+
...request.redirect ? { redirect: request.redirect } : {}
|
|
491
544
|
});
|
|
492
545
|
if (!response.ok) {
|
|
493
|
-
const
|
|
494
|
-
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}`;
|
|
495
549
|
throw new Error(context);
|
|
496
550
|
}
|
|
497
551
|
const data = await response.json();
|
|
@@ -515,13 +569,13 @@ async function requestOpenAICompatibleEmbeddings(request) {
|
|
|
515
569
|
return parsed;
|
|
516
570
|
});
|
|
517
571
|
}
|
|
518
|
-
class
|
|
572
|
+
class OpenAICompatibleEmbeddingProvider {
|
|
519
573
|
metadata;
|
|
520
|
-
#apiKey;
|
|
521
574
|
#timeoutMs;
|
|
522
|
-
|
|
575
|
+
#profile;
|
|
576
|
+
constructor(metadata, profile, timeoutMs) {
|
|
523
577
|
this.metadata = metadata;
|
|
524
|
-
this.#
|
|
578
|
+
this.#profile = profile;
|
|
525
579
|
this.#timeoutMs = timeoutMs;
|
|
526
580
|
}
|
|
527
581
|
async embed(texts, options = {}) {
|
|
@@ -531,131 +585,44 @@ class OpenAIEmbeddingProvider {
|
|
|
531
585
|
}
|
|
532
586
|
assertBatchSize(this.metadata, texts.length);
|
|
533
587
|
const items = await withTimeout(
|
|
534
|
-
|
|
535
|
-
"API request",
|
|
536
|
-
this.#timeoutMs,
|
|
537
|
-
async (signal) => requestOpenAICompatibleEmbeddings({
|
|
538
|
-
responseLabel: "OpenAI",
|
|
539
|
-
endpoint: OPENAI_EMBEDDINGS_URL,
|
|
540
|
-
apiKey: this.#apiKey,
|
|
541
|
-
model: this.metadata.model,
|
|
542
|
-
texts,
|
|
543
|
-
signal,
|
|
544
|
-
dimensions: this.metadata.dimensions
|
|
545
|
-
}),
|
|
546
|
-
options.signal,
|
|
547
|
-
[this.#apiKey]
|
|
548
|
-
);
|
|
549
|
-
return createEmbeddingBatchResult(
|
|
550
|
-
this.metadata,
|
|
551
|
-
intent,
|
|
552
|
-
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "openai")
|
|
553
|
-
);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
class MistralEmbeddingProvider {
|
|
557
|
-
metadata;
|
|
558
|
-
#apiKey;
|
|
559
|
-
#timeoutMs;
|
|
560
|
-
constructor(metadata, apiKey, timeoutMs) {
|
|
561
|
-
this.metadata = metadata;
|
|
562
|
-
this.#apiKey = apiKey;
|
|
563
|
-
this.#timeoutMs = timeoutMs;
|
|
564
|
-
}
|
|
565
|
-
async embed(texts, options = {}) {
|
|
566
|
-
const intent = resolveIntent(options.intent);
|
|
567
|
-
if (texts.length === 0) {
|
|
568
|
-
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
569
|
-
}
|
|
570
|
-
assertBatchSize(this.metadata, texts.length);
|
|
571
|
-
const items = await withTimeout(
|
|
572
|
-
"mistral",
|
|
573
|
-
"API request",
|
|
574
|
-
this.#timeoutMs,
|
|
575
|
-
async (signal) => requestOpenAICompatibleEmbeddings({
|
|
576
|
-
responseLabel: "Mistral",
|
|
577
|
-
endpoint: MISTRAL_EMBEDDINGS_URL,
|
|
578
|
-
apiKey: this.#apiKey,
|
|
579
|
-
model: this.metadata.model,
|
|
580
|
-
texts,
|
|
581
|
-
signal,
|
|
582
|
-
encodingFormat: "float",
|
|
583
|
-
requireIndex: true
|
|
584
|
-
}),
|
|
585
|
-
options.signal,
|
|
586
|
-
[this.#apiKey]
|
|
587
|
-
);
|
|
588
|
-
return createEmbeddingBatchResult(
|
|
589
|
-
this.metadata,
|
|
590
|
-
intent,
|
|
591
|
-
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "mistral")
|
|
592
|
-
);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
class CloudflareEmbeddingProvider {
|
|
596
|
-
metadata;
|
|
597
|
-
#accountId;
|
|
598
|
-
#apiToken;
|
|
599
|
-
#timeoutMs;
|
|
600
|
-
constructor(metadata, accountId, apiToken, timeoutMs) {
|
|
601
|
-
this.metadata = metadata;
|
|
602
|
-
this.#accountId = accountId;
|
|
603
|
-
this.#apiToken = apiToken;
|
|
604
|
-
this.#timeoutMs = timeoutMs;
|
|
605
|
-
}
|
|
606
|
-
async embed(texts, options = {}) {
|
|
607
|
-
const intent = resolveIntent(options.intent);
|
|
608
|
-
if (texts.length === 0) {
|
|
609
|
-
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
610
|
-
}
|
|
611
|
-
assertBatchSize(this.metadata, texts.length);
|
|
612
|
-
const endpoint = createCloudflareEmbeddingsUrl(this.#accountId);
|
|
613
|
-
const items = await withTimeout(
|
|
614
|
-
"cloudflare",
|
|
588
|
+
this.#profile.provider,
|
|
615
589
|
"API request",
|
|
616
590
|
this.#timeoutMs,
|
|
617
591
|
async (signal) => {
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
model: this.
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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);
|
|
638
616
|
}
|
|
639
|
-
return
|
|
640
|
-
const typed = item;
|
|
641
|
-
const embedding = typed.embedding;
|
|
642
|
-
if (!Array.isArray(embedding)) {
|
|
643
|
-
throw new Error("Cloudflare response item did not include an embedding array");
|
|
644
|
-
}
|
|
645
|
-
const parsed = { embedding };
|
|
646
|
-
if (hasOwnProperty(typed, "index")) {
|
|
647
|
-
parsed.index = typed.index;
|
|
648
|
-
}
|
|
649
|
-
return parsed;
|
|
650
|
-
});
|
|
617
|
+
return results;
|
|
651
618
|
},
|
|
652
619
|
options.signal,
|
|
653
|
-
|
|
620
|
+
this.#profile.exactSecrets ?? []
|
|
654
621
|
);
|
|
655
622
|
return createEmbeddingBatchResult(
|
|
656
623
|
this.metadata,
|
|
657
624
|
intent,
|
|
658
|
-
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions,
|
|
625
|
+
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, this.#profile.provider)
|
|
659
626
|
);
|
|
660
627
|
}
|
|
661
628
|
}
|
|
@@ -680,7 +647,16 @@ function createEmbeddingProvider(options = {}) {
|
|
|
680
647
|
if (!key) {
|
|
681
648
|
throw new Error("OPENAI_API_KEY is required for OpenAI embeddings");
|
|
682
649
|
}
|
|
683
|
-
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);
|
|
684
660
|
}
|
|
685
661
|
case "mistral": {
|
|
686
662
|
const key = getOptionalTrimmedCredential(
|
|
@@ -689,7 +665,17 @@ function createEmbeddingProvider(options = {}) {
|
|
|
689
665
|
if (!key) {
|
|
690
666
|
throw new Error("MISTRAL_API_KEY is required for Mistral embeddings");
|
|
691
667
|
}
|
|
692
|
-
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);
|
|
693
679
|
}
|
|
694
680
|
case "cloudflare": {
|
|
695
681
|
const accountId = getOptionalTrimmedCredential(
|
|
@@ -704,13 +690,56 @@ function createEmbeddingProvider(options = {}) {
|
|
|
704
690
|
if (!apiToken) {
|
|
705
691
|
throw new Error("CLOUDFLARE_API_TOKEN is required for Cloudflare embeddings");
|
|
706
692
|
}
|
|
707
|
-
|
|
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);
|
|
708
731
|
}
|
|
709
732
|
}
|
|
710
733
|
}
|
|
711
734
|
function getEmbeddingProviderMetadata(options = {}) {
|
|
712
735
|
const provider = resolveProviderName(options.provider);
|
|
713
|
-
|
|
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);
|
|
714
743
|
}
|
|
715
744
|
async function generateEmbeddings(texts, options = {}) {
|
|
716
745
|
const maxLength = getPositiveInteger(options.maxLength ?? DEFAULT_MAX_LENGTH, "maxLength");
|