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/README.md
CHANGED
|
@@ -5,26 +5,13 @@
|
|
|
5
5
|
[](https://github.com/llbbl/libsql-search/actions/workflows/ci.yml)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
`libsql-search` adds semantic search to Markdown-backed sites
|
|
9
|
-
It indexes frontmatter and content from files on disk, stores vectors in your
|
|
10
|
-
database, and lets you query by meaning instead of exact keywords.
|
|
8
|
+
`libsql-search` adds semantic search to Markdown-backed sites with a small TypeScript API. It indexes frontmatter and content from files on disk, stores vectors in libSQL/Turso, and lets you query by meaning instead of exact keywords.
|
|
11
9
|
|
|
12
10
|
Use it when you want:
|
|
13
11
|
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
- direct control over table names, dimensions, content shape, and deployment
|
|
18
|
-
|
|
19
|
-
## What It Supports
|
|
20
|
-
|
|
21
|
-
- Markdown indexing from local directories with frontmatter via `gray-matter`
|
|
22
|
-
- libSQL/Turso storage and vector search
|
|
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
|
-
`gemini-embedding-2`, and OpenAI `text-embedding-3-small` /
|
|
26
|
-
`text-embedding-3-large`
|
|
27
|
-
- npm distribution plus JSR publishing
|
|
12
|
+
- one indexing/search API across local and hosted embedding providers
|
|
13
|
+
- direct control over vector dimensions, table names, and deployment shape
|
|
14
|
+
- a lightweight library instead of a hosted search product
|
|
28
15
|
|
|
29
16
|
## Install
|
|
30
17
|
|
|
@@ -43,18 +30,10 @@ deno add jsr:@logan/libsql-search npm:@libsql/client
|
|
|
43
30
|
```
|
|
44
31
|
|
|
45
32
|
For npm usage, the package requires Node `>=22.12.0`.
|
|
46
|
-
Node examples in this README import from `libsql-search` and `@libsql/client`.
|
|
47
|
-
In Deno, after `deno add`, import from `@logan/libsql-search` and
|
|
48
|
-
`@libsql/client`.
|
|
49
33
|
|
|
50
34
|
## Quick Start
|
|
51
35
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
1. create a libSQL client
|
|
55
|
-
2. create the search table
|
|
56
|
-
3. index a Markdown directory
|
|
57
|
-
4. query it with the same embedding provider and dimensions
|
|
36
|
+
This example uses the default local provider. Local embeddings run in-process after the initial model download and cache warmup; they are not automatically air-gapped.
|
|
58
37
|
|
|
59
38
|
```ts
|
|
60
39
|
import { createClient } from "@libsql/client";
|
|
@@ -65,11 +44,12 @@ const client = createClient({
|
|
|
65
44
|
authToken: "your-auth-token",
|
|
66
45
|
});
|
|
67
46
|
|
|
68
|
-
await createTable(client);
|
|
47
|
+
await createTable(client, "articles_local_384", 384);
|
|
69
48
|
|
|
70
49
|
await indexContent({
|
|
71
50
|
client,
|
|
72
51
|
contentPath: "./content",
|
|
52
|
+
tableName: "articles_local_384",
|
|
73
53
|
embeddingOptions: {
|
|
74
54
|
provider: "local",
|
|
75
55
|
},
|
|
@@ -78,6 +58,7 @@ await indexContent({
|
|
|
78
58
|
const results = await search({
|
|
79
59
|
client,
|
|
80
60
|
query: "how do I deploy my docs site",
|
|
61
|
+
tableName: "articles_local_384",
|
|
81
62
|
limit: 5,
|
|
82
63
|
embeddingOptions: {
|
|
83
64
|
provider: "local",
|
|
@@ -94,33 +75,31 @@ console.log(results.map((result) => ({
|
|
|
94
75
|
Important behavior:
|
|
95
76
|
|
|
96
77
|
- Call `createTable()` before indexing or searching.
|
|
97
|
-
- Keep
|
|
98
|
-
- `indexContent()` clears existing rows before rebuilding
|
|
99
|
-
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
- `
|
|
106
|
-
- `
|
|
107
|
-
- `
|
|
108
|
-
- `
|
|
109
|
-
- `
|
|
110
|
-
- `
|
|
111
|
-
- `getFolders(client, tableName?)`
|
|
112
|
-
- `generateEmbedding(text, options?)`
|
|
113
|
-
- `prepareTextForEmbedding(fields)`
|
|
78
|
+
- Keep table width, provider, and dimensions aligned across create/index/query.
|
|
79
|
+
- `indexContent()` clears existing rows before rebuilding and is not transactional.
|
|
80
|
+
- Hosted providers send indexed and queried text to external services and may incur provider charges.
|
|
81
|
+
|
|
82
|
+
## Providers
|
|
83
|
+
|
|
84
|
+
Built-in providers:
|
|
85
|
+
|
|
86
|
+
- `local` with `Xenova/all-MiniLM-L6-v2` at 384 dimensions
|
|
87
|
+
- `cloudflare` with `@cf/baai/bge-m3` at 1024 dimensions
|
|
88
|
+
- `mistral` with `mistral-embed` at 1024 dimensions
|
|
89
|
+
- `gemini` with `gemini-embedding-2` at 128-3072 dimensions, default 3072
|
|
90
|
+
- `openai` with `text-embedding-3-small` or `text-embedding-3-large`, default 768
|
|
91
|
+
- `openai-compatible` for trusted OpenAI-compatible endpoints such as TEI
|
|
114
92
|
|
|
115
93
|
## Docs
|
|
116
94
|
|
|
117
|
-
- [
|
|
118
|
-
- [Provider
|
|
95
|
+
- [Documentation index](./docs/README.md)
|
|
96
|
+
- [Provider selection and configuration](./docs/PROVIDERS.md)
|
|
119
97
|
- [API reference](./docs/API.md)
|
|
120
98
|
- [Integration examples](./docs/INTEGRATIONS.md)
|
|
99
|
+
- [Migration and reindexing guide](./docs/MIGRATIONS.md)
|
|
100
|
+
- [Testing guidance](./docs/TESTING.md)
|
|
121
101
|
- [Indexing and operations](./docs/INDEXING.md)
|
|
122
102
|
- [Troubleshooting](./docs/TROUBLESHOOTING.md)
|
|
123
|
-
- [Release workflow](./docs/RELEASING.md)
|
|
124
103
|
|
|
125
104
|
## License
|
|
126
105
|
|
package/dist/index.cjs
CHANGED
|
@@ -5,6 +5,7 @@ var path = require('path');
|
|
|
5
5
|
var matter = require('gray-matter');
|
|
6
6
|
|
|
7
7
|
const OPENAI_DEFAULT_DIMENSIONS = 768;
|
|
8
|
+
const OPENAI_COMPATIBLE_DEFAULT_BATCH_SIZE = 32;
|
|
8
9
|
const LOCAL_DIMENSIONS = 384;
|
|
9
10
|
const DEFAULT_MAX_LENGTH = 8e3;
|
|
10
11
|
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
@@ -111,6 +112,7 @@ function resolveProviderName(provider) {
|
|
|
111
112
|
case "openai":
|
|
112
113
|
case "mistral":
|
|
113
114
|
case "cloudflare":
|
|
115
|
+
case "openai-compatible":
|
|
114
116
|
return provider ?? "local";
|
|
115
117
|
default:
|
|
116
118
|
throw new Error(`Unknown embedding provider: ${String(provider)}`);
|
|
@@ -135,6 +137,13 @@ function getOptionalTrimmedCredential(value) {
|
|
|
135
137
|
const trimmed = value?.trim();
|
|
136
138
|
return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
|
|
137
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
|
+
}
|
|
138
147
|
function hasOwnProperty(value, property) {
|
|
139
148
|
return Object.prototype.hasOwnProperty.call(value, property);
|
|
140
149
|
}
|
|
@@ -295,7 +304,27 @@ async function embedSequentially(provider, operation, texts, signal, embedOne) {
|
|
|
295
304
|
}
|
|
296
305
|
return results;
|
|
297
306
|
}
|
|
298
|
-
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) {
|
|
299
328
|
switch (provider) {
|
|
300
329
|
case "local":
|
|
301
330
|
return Object.freeze({
|
|
@@ -332,6 +361,13 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
332
361
|
dimensions: CLOUDFLARE_DIMENSIONS,
|
|
333
362
|
batch: Object.freeze({ mode: "native" })
|
|
334
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
|
+
});
|
|
335
371
|
}
|
|
336
372
|
}
|
|
337
373
|
function getEffectiveDimensions(provider, dimensions) {
|
|
@@ -372,6 +408,12 @@ function getEffectiveDimensions(provider, dimensions) {
|
|
|
372
408
|
}
|
|
373
409
|
return CLOUDFLARE_DIMENSIONS;
|
|
374
410
|
}
|
|
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
|
+
}
|
|
375
417
|
return getPositiveInteger(dimensions ?? OPENAI_DEFAULT_DIMENSIONS, "dimensions");
|
|
376
418
|
}
|
|
377
419
|
function assertBatchSize(metadata, count) {
|
|
@@ -382,6 +424,13 @@ function assertBatchSize(metadata, count) {
|
|
|
382
424
|
);
|
|
383
425
|
}
|
|
384
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
|
+
}
|
|
385
434
|
function createEmbeddingBatchResult(metadata, intent, embeddings) {
|
|
386
435
|
return Object.freeze({
|
|
387
436
|
embeddings,
|
|
@@ -482,18 +531,23 @@ async function requestOpenAICompatibleEmbeddings(request) {
|
|
|
482
531
|
if (request.encodingFormat !== void 0) {
|
|
483
532
|
body.encoding_format = request.encodingFormat;
|
|
484
533
|
}
|
|
534
|
+
const headers = {
|
|
535
|
+
"Content-Type": "application/json"
|
|
536
|
+
};
|
|
537
|
+
if (request.apiKey !== void 0) {
|
|
538
|
+
headers.Authorization = `Bearer ${request.apiKey}`;
|
|
539
|
+
}
|
|
485
540
|
const response = await fetch(request.endpoint, {
|
|
486
541
|
method: "POST",
|
|
487
|
-
headers
|
|
488
|
-
"Content-Type": "application/json",
|
|
489
|
-
"Authorization": `Bearer ${request.apiKey}`
|
|
490
|
-
},
|
|
542
|
+
headers,
|
|
491
543
|
signal: request.signal,
|
|
492
|
-
body: JSON.stringify(body)
|
|
544
|
+
body: JSON.stringify(body),
|
|
545
|
+
...request.redirect ? { redirect: request.redirect } : {}
|
|
493
546
|
});
|
|
494
547
|
if (!response.ok) {
|
|
495
|
-
const
|
|
496
|
-
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}`;
|
|
497
551
|
throw new Error(context);
|
|
498
552
|
}
|
|
499
553
|
const data = await response.json();
|
|
@@ -517,13 +571,13 @@ async function requestOpenAICompatibleEmbeddings(request) {
|
|
|
517
571
|
return parsed;
|
|
518
572
|
});
|
|
519
573
|
}
|
|
520
|
-
class
|
|
574
|
+
class OpenAICompatibleEmbeddingProvider {
|
|
521
575
|
metadata;
|
|
522
|
-
#apiKey;
|
|
523
576
|
#timeoutMs;
|
|
524
|
-
|
|
577
|
+
#profile;
|
|
578
|
+
constructor(metadata, profile, timeoutMs) {
|
|
525
579
|
this.metadata = metadata;
|
|
526
|
-
this.#
|
|
580
|
+
this.#profile = profile;
|
|
527
581
|
this.#timeoutMs = timeoutMs;
|
|
528
582
|
}
|
|
529
583
|
async embed(texts, options = {}) {
|
|
@@ -533,131 +587,44 @@ class OpenAIEmbeddingProvider {
|
|
|
533
587
|
}
|
|
534
588
|
assertBatchSize(this.metadata, texts.length);
|
|
535
589
|
const items = await withTimeout(
|
|
536
|
-
|
|
537
|
-
"API request",
|
|
538
|
-
this.#timeoutMs,
|
|
539
|
-
async (signal) => requestOpenAICompatibleEmbeddings({
|
|
540
|
-
responseLabel: "OpenAI",
|
|
541
|
-
endpoint: OPENAI_EMBEDDINGS_URL,
|
|
542
|
-
apiKey: this.#apiKey,
|
|
543
|
-
model: this.metadata.model,
|
|
544
|
-
texts,
|
|
545
|
-
signal,
|
|
546
|
-
dimensions: this.metadata.dimensions
|
|
547
|
-
}),
|
|
548
|
-
options.signal,
|
|
549
|
-
[this.#apiKey]
|
|
550
|
-
);
|
|
551
|
-
return createEmbeddingBatchResult(
|
|
552
|
-
this.metadata,
|
|
553
|
-
intent,
|
|
554
|
-
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "openai")
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
class MistralEmbeddingProvider {
|
|
559
|
-
metadata;
|
|
560
|
-
#apiKey;
|
|
561
|
-
#timeoutMs;
|
|
562
|
-
constructor(metadata, apiKey, timeoutMs) {
|
|
563
|
-
this.metadata = metadata;
|
|
564
|
-
this.#apiKey = apiKey;
|
|
565
|
-
this.#timeoutMs = timeoutMs;
|
|
566
|
-
}
|
|
567
|
-
async embed(texts, options = {}) {
|
|
568
|
-
const intent = resolveIntent(options.intent);
|
|
569
|
-
if (texts.length === 0) {
|
|
570
|
-
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
571
|
-
}
|
|
572
|
-
assertBatchSize(this.metadata, texts.length);
|
|
573
|
-
const items = await withTimeout(
|
|
574
|
-
"mistral",
|
|
575
|
-
"API request",
|
|
576
|
-
this.#timeoutMs,
|
|
577
|
-
async (signal) => requestOpenAICompatibleEmbeddings({
|
|
578
|
-
responseLabel: "Mistral",
|
|
579
|
-
endpoint: MISTRAL_EMBEDDINGS_URL,
|
|
580
|
-
apiKey: this.#apiKey,
|
|
581
|
-
model: this.metadata.model,
|
|
582
|
-
texts,
|
|
583
|
-
signal,
|
|
584
|
-
encodingFormat: "float",
|
|
585
|
-
requireIndex: true
|
|
586
|
-
}),
|
|
587
|
-
options.signal,
|
|
588
|
-
[this.#apiKey]
|
|
589
|
-
);
|
|
590
|
-
return createEmbeddingBatchResult(
|
|
591
|
-
this.metadata,
|
|
592
|
-
intent,
|
|
593
|
-
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "mistral")
|
|
594
|
-
);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
class CloudflareEmbeddingProvider {
|
|
598
|
-
metadata;
|
|
599
|
-
#accountId;
|
|
600
|
-
#apiToken;
|
|
601
|
-
#timeoutMs;
|
|
602
|
-
constructor(metadata, accountId, apiToken, timeoutMs) {
|
|
603
|
-
this.metadata = metadata;
|
|
604
|
-
this.#accountId = accountId;
|
|
605
|
-
this.#apiToken = apiToken;
|
|
606
|
-
this.#timeoutMs = timeoutMs;
|
|
607
|
-
}
|
|
608
|
-
async embed(texts, options = {}) {
|
|
609
|
-
const intent = resolveIntent(options.intent);
|
|
610
|
-
if (texts.length === 0) {
|
|
611
|
-
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
612
|
-
}
|
|
613
|
-
assertBatchSize(this.metadata, texts.length);
|
|
614
|
-
const endpoint = createCloudflareEmbeddingsUrl(this.#accountId);
|
|
615
|
-
const items = await withTimeout(
|
|
616
|
-
"cloudflare",
|
|
590
|
+
this.#profile.provider,
|
|
617
591
|
"API request",
|
|
618
592
|
this.#timeoutMs,
|
|
619
593
|
async (signal) => {
|
|
620
|
-
const
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
model: this.
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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);
|
|
640
618
|
}
|
|
641
|
-
return
|
|
642
|
-
const typed = item;
|
|
643
|
-
const embedding = typed.embedding;
|
|
644
|
-
if (!Array.isArray(embedding)) {
|
|
645
|
-
throw new Error("Cloudflare response item did not include an embedding array");
|
|
646
|
-
}
|
|
647
|
-
const parsed = { embedding };
|
|
648
|
-
if (hasOwnProperty(typed, "index")) {
|
|
649
|
-
parsed.index = typed.index;
|
|
650
|
-
}
|
|
651
|
-
return parsed;
|
|
652
|
-
});
|
|
619
|
+
return results;
|
|
653
620
|
},
|
|
654
621
|
options.signal,
|
|
655
|
-
|
|
622
|
+
this.#profile.exactSecrets ?? []
|
|
656
623
|
);
|
|
657
624
|
return createEmbeddingBatchResult(
|
|
658
625
|
this.metadata,
|
|
659
626
|
intent,
|
|
660
|
-
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions,
|
|
627
|
+
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, this.#profile.provider)
|
|
661
628
|
);
|
|
662
629
|
}
|
|
663
630
|
}
|
|
@@ -682,7 +649,16 @@ function createEmbeddingProvider(options = {}) {
|
|
|
682
649
|
if (!key) {
|
|
683
650
|
throw new Error("OPENAI_API_KEY is required for OpenAI embeddings");
|
|
684
651
|
}
|
|
685
|
-
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);
|
|
686
662
|
}
|
|
687
663
|
case "mistral": {
|
|
688
664
|
const key = getOptionalTrimmedCredential(
|
|
@@ -691,7 +667,17 @@ function createEmbeddingProvider(options = {}) {
|
|
|
691
667
|
if (!key) {
|
|
692
668
|
throw new Error("MISTRAL_API_KEY is required for Mistral embeddings");
|
|
693
669
|
}
|
|
694
|
-
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);
|
|
695
681
|
}
|
|
696
682
|
case "cloudflare": {
|
|
697
683
|
const accountId = getOptionalTrimmedCredential(
|
|
@@ -706,13 +692,56 @@ function createEmbeddingProvider(options = {}) {
|
|
|
706
692
|
if (!apiToken) {
|
|
707
693
|
throw new Error("CLOUDFLARE_API_TOKEN is required for Cloudflare embeddings");
|
|
708
694
|
}
|
|
709
|
-
|
|
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);
|
|
710
733
|
}
|
|
711
734
|
}
|
|
712
735
|
}
|
|
713
736
|
function getEmbeddingProviderMetadata(options = {}) {
|
|
714
737
|
const provider = resolveProviderName(options.provider);
|
|
715
|
-
|
|
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);
|
|
716
745
|
}
|
|
717
746
|
async function generateEmbeddings(texts, options = {}) {
|
|
718
747
|
const maxLength = getPositiveInteger(options.maxLength ?? DEFAULT_MAX_LENGTH, "maxLength");
|
package/dist/index.d.ts
CHANGED
|
@@ -2,9 +2,10 @@ import { Client } from '@libsql/client';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Multi-provider embedding generation
|
|
5
|
-
* Supports local Hugging Face Transformers, Gemini, OpenAI, Mistral,
|
|
5
|
+
* Supports local Hugging Face Transformers, Gemini, OpenAI, Mistral,
|
|
6
|
+
* Cloudflare Workers AI, and custom OpenAI-compatible endpoints
|
|
6
7
|
*/
|
|
7
|
-
type EmbeddingProvider = 'local' | 'gemini' | 'openai' | 'mistral' | 'cloudflare';
|
|
8
|
+
type EmbeddingProvider = 'local' | 'gemini' | 'openai' | 'mistral' | 'cloudflare' | 'openai-compatible';
|
|
8
9
|
type EmbeddingIntent = 'document' | 'query';
|
|
9
10
|
type EmbeddingBatchMode = 'native' | 'sequential';
|
|
10
11
|
interface EmbeddingBatchBehavior {
|
|
@@ -37,6 +38,9 @@ interface EmbeddingOptions {
|
|
|
37
38
|
apiKey?: string;
|
|
38
39
|
accountId?: string;
|
|
39
40
|
apiToken?: string;
|
|
41
|
+
baseUrl?: string;
|
|
42
|
+
model?: string;
|
|
43
|
+
batchSize?: number;
|
|
40
44
|
dimensions?: number;
|
|
41
45
|
maxLength?: number;
|
|
42
46
|
intent?: EmbeddingIntent;
|