libsql-search 0.2.4 → 0.4.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 +7 -4
- package/dist/index.cjs +215 -10
- package/dist/index.d.ts +3 -1
- package/dist/index.esm.js +215 -10
- package/docs/API.md +11 -3
- package/docs/INTEGRATIONS.md +16 -3
- package/docs/PROVIDERS.md +74 -9
- package/docs/README.md +2 -2
- package/docs/TROUBLESHOOTING.md +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,16 +13,17 @@ Use it when you want:
|
|
|
13
13
|
|
|
14
14
|
- a small TypeScript library instead of a hosted search product
|
|
15
15
|
- one search index shared across static-site builds and app routes
|
|
16
|
-
- local or
|
|
16
|
+
- local or hosted embeddings behind the same indexing/search API
|
|
17
17
|
- direct control over table names, dimensions, content shape, and deployment
|
|
18
18
|
|
|
19
19
|
## What It Supports
|
|
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
|
|
24
|
-
|
|
25
|
-
`text-embedding-
|
|
23
|
+
- Embedding providers: local `Xenova/all-MiniLM-L6-v2`, Cloudflare Workers AI
|
|
24
|
+
`@cf/baai/bge-m3`, Mistral `mistral-embed`, Google Gemini
|
|
25
|
+
`text-embedding-004`, and OpenAI `text-embedding-3-small` /
|
|
26
|
+
`text-embedding-3-large`
|
|
26
27
|
- npm distribution plus JSR publishing
|
|
27
28
|
|
|
28
29
|
## Install
|
|
@@ -97,6 +98,8 @@ Important behavior:
|
|
|
97
98
|
- Call `createTable()` before indexing or searching.
|
|
98
99
|
- Keep dimensions aligned across table creation, indexing, and search queries.
|
|
99
100
|
- `indexContent()` clears existing rows before rebuilding the index.
|
|
101
|
+
- `local` is the default offline provider; Cloudflare is the recommended hosted
|
|
102
|
+
option. Cloudflare and Mistral use 1024 dimensions.
|
|
100
103
|
|
|
101
104
|
## Core API
|
|
102
105
|
|
package/dist/index.cjs
CHANGED
|
@@ -12,6 +12,11 @@ const GEMINI_MODEL = "text-embedding-004";
|
|
|
12
12
|
const OPENAI_SMALL_MODEL = "text-embedding-3-small";
|
|
13
13
|
const OPENAI_LARGE_MODEL = "text-embedding-3-large";
|
|
14
14
|
const OPENAI_EMBEDDINGS_URL = "https://api.openai.com/v1/embeddings";
|
|
15
|
+
const MISTRAL_MODEL = "mistral-embed";
|
|
16
|
+
const MISTRAL_EMBEDDINGS_URL = "https://api.mistral.ai/v1/embeddings";
|
|
17
|
+
const MISTRAL_DIMENSIONS = 1024;
|
|
18
|
+
const CLOUDFLARE_MODEL = "@cf/baai/bge-m3";
|
|
19
|
+
const CLOUDFLARE_DIMENSIONS = 1024;
|
|
15
20
|
const localModelCacheByModel = /* @__PURE__ */ new Map();
|
|
16
21
|
function getEnvironmentVariable(name) {
|
|
17
22
|
const runtime = globalThis;
|
|
@@ -51,6 +56,8 @@ function resolveProviderName(provider) {
|
|
|
51
56
|
case "local":
|
|
52
57
|
case "gemini":
|
|
53
58
|
case "openai":
|
|
59
|
+
case "mistral":
|
|
60
|
+
case "cloudflare":
|
|
54
61
|
return provider ?? "local";
|
|
55
62
|
default:
|
|
56
63
|
throw new Error(`Unknown embedding provider: ${String(provider)}`);
|
|
@@ -71,9 +78,16 @@ function truncateTexts(texts, maxLength) {
|
|
|
71
78
|
function getOpenAIModel(dimensions) {
|
|
72
79
|
return dimensions <= 1536 ? OPENAI_SMALL_MODEL : OPENAI_LARGE_MODEL;
|
|
73
80
|
}
|
|
81
|
+
function getOptionalTrimmedCredential(value) {
|
|
82
|
+
const trimmed = value?.trim();
|
|
83
|
+
return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
|
|
84
|
+
}
|
|
74
85
|
function hasOwnProperty(value, property) {
|
|
75
86
|
return Object.prototype.hasOwnProperty.call(value, property);
|
|
76
87
|
}
|
|
88
|
+
function isRecord(value) {
|
|
89
|
+
return typeof value === "object" && value !== null;
|
|
90
|
+
}
|
|
77
91
|
function redactErrorText(value, exactSecrets = []) {
|
|
78
92
|
let raw = value instanceof Error ? value.message : String(value);
|
|
79
93
|
for (const secret of exactSecrets) {
|
|
@@ -97,6 +111,16 @@ function providerError(provider, message, cause, exactSecrets = []) {
|
|
|
97
111
|
function getResponseHeader(response, name) {
|
|
98
112
|
return response.headers.get(name) ?? void 0;
|
|
99
113
|
}
|
|
114
|
+
function getSafeResponseHeader(response, name) {
|
|
115
|
+
const value = getResponseHeader(response, name)?.trim();
|
|
116
|
+
if (!value) {
|
|
117
|
+
return void 0;
|
|
118
|
+
}
|
|
119
|
+
return value.replace(/[^A-Za-z0-9._:-]/g, "").slice(0, 128);
|
|
120
|
+
}
|
|
121
|
+
function createCloudflareEmbeddingsUrl(accountId) {
|
|
122
|
+
return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/v1/embeddings`;
|
|
123
|
+
}
|
|
100
124
|
async function withTimeout(provider, operation, timeoutMs, run, parentSignal, exactSecrets = []) {
|
|
101
125
|
if (parentSignal?.aborted) {
|
|
102
126
|
throw providerError(provider, `${operation} was aborted`);
|
|
@@ -225,12 +249,44 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
225
249
|
dimensions,
|
|
226
250
|
batch: Object.freeze({ mode: "native", maxSize: 2048 })
|
|
227
251
|
});
|
|
252
|
+
case "mistral":
|
|
253
|
+
return Object.freeze({
|
|
254
|
+
name: "mistral",
|
|
255
|
+
model: MISTRAL_MODEL,
|
|
256
|
+
dimensions: MISTRAL_DIMENSIONS,
|
|
257
|
+
batch: Object.freeze({ mode: "native" })
|
|
258
|
+
});
|
|
259
|
+
case "cloudflare":
|
|
260
|
+
return Object.freeze({
|
|
261
|
+
name: "cloudflare",
|
|
262
|
+
model: CLOUDFLARE_MODEL,
|
|
263
|
+
dimensions: CLOUDFLARE_DIMENSIONS,
|
|
264
|
+
batch: Object.freeze({ mode: "native" })
|
|
265
|
+
});
|
|
228
266
|
}
|
|
229
267
|
}
|
|
230
268
|
function getEffectiveDimensions(provider, dimensions) {
|
|
231
269
|
if (provider === "gemini") {
|
|
232
270
|
return DEFAULT_DIMENSIONS;
|
|
233
271
|
}
|
|
272
|
+
if (provider === "mistral") {
|
|
273
|
+
if (dimensions !== void 0 && dimensions !== MISTRAL_DIMENSIONS) {
|
|
274
|
+
throw providerError(
|
|
275
|
+
"mistral",
|
|
276
|
+
`${MISTRAL_MODEL} returns ${MISTRAL_DIMENSIONS} dimensions; received dimensions ${dimensions}`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return MISTRAL_DIMENSIONS;
|
|
280
|
+
}
|
|
281
|
+
if (provider === "cloudflare") {
|
|
282
|
+
if (dimensions !== void 0 && dimensions !== CLOUDFLARE_DIMENSIONS) {
|
|
283
|
+
throw providerError(
|
|
284
|
+
"cloudflare",
|
|
285
|
+
`@cf/baai/bge-m3 returns ${CLOUDFLARE_DIMENSIONS} dimensions; received dimensions ${dimensions}`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
return CLOUDFLARE_DIMENSIONS;
|
|
289
|
+
}
|
|
234
290
|
return getPositiveInteger(dimensions ?? DEFAULT_DIMENSIONS, "dimensions");
|
|
235
291
|
}
|
|
236
292
|
function assertBatchSize(metadata, count) {
|
|
@@ -329,6 +385,52 @@ class GeminiEmbeddingProvider {
|
|
|
329
385
|
);
|
|
330
386
|
}
|
|
331
387
|
}
|
|
388
|
+
async function requestOpenAICompatibleEmbeddings(request) {
|
|
389
|
+
const body = {
|
|
390
|
+
input: request.texts,
|
|
391
|
+
model: request.model
|
|
392
|
+
};
|
|
393
|
+
if (request.dimensions !== void 0) {
|
|
394
|
+
body.dimensions = request.dimensions;
|
|
395
|
+
}
|
|
396
|
+
if (request.encodingFormat !== void 0) {
|
|
397
|
+
body.encoding_format = request.encodingFormat;
|
|
398
|
+
}
|
|
399
|
+
const response = await fetch(request.endpoint, {
|
|
400
|
+
method: "POST",
|
|
401
|
+
headers: {
|
|
402
|
+
"Content-Type": "application/json",
|
|
403
|
+
"Authorization": `Bearer ${request.apiKey}`
|
|
404
|
+
},
|
|
405
|
+
signal: request.signal,
|
|
406
|
+
body: JSON.stringify(body)
|
|
407
|
+
});
|
|
408
|
+
if (!response.ok) {
|
|
409
|
+
const requestId = getSafeResponseHeader(response, "x-request-id");
|
|
410
|
+
const context = requestId ? ` status ${response.status}, request ${requestId}` : ` status ${response.status}`;
|
|
411
|
+
throw new Error(context);
|
|
412
|
+
}
|
|
413
|
+
const data = await response.json();
|
|
414
|
+
if (!Array.isArray(data.data)) {
|
|
415
|
+
throw new Error(`${request.responseLabel} response did not include a data array`);
|
|
416
|
+
}
|
|
417
|
+
return data.data.map((item) => {
|
|
418
|
+
if (!isRecord(item)) {
|
|
419
|
+
throw new Error(`${request.responseLabel} response item was not an object`);
|
|
420
|
+
}
|
|
421
|
+
const embedding = item.embedding;
|
|
422
|
+
if (!Array.isArray(embedding)) {
|
|
423
|
+
throw new Error(`${request.responseLabel} response item did not include an embedding array`);
|
|
424
|
+
}
|
|
425
|
+
const parsed = { embedding };
|
|
426
|
+
if (hasOwnProperty(item, "index")) {
|
|
427
|
+
parsed.index = item.index;
|
|
428
|
+
} else if (request.requireIndex) {
|
|
429
|
+
throw new Error(`${request.responseLabel} response item did not include an index`);
|
|
430
|
+
}
|
|
431
|
+
return parsed;
|
|
432
|
+
});
|
|
433
|
+
}
|
|
332
434
|
class OpenAIEmbeddingProvider {
|
|
333
435
|
metadata;
|
|
334
436
|
#apiKey;
|
|
@@ -348,34 +450,113 @@ class OpenAIEmbeddingProvider {
|
|
|
348
450
|
"openai",
|
|
349
451
|
"API request",
|
|
350
452
|
this.#timeoutMs,
|
|
453
|
+
async (signal) => requestOpenAICompatibleEmbeddings({
|
|
454
|
+
responseLabel: "OpenAI",
|
|
455
|
+
endpoint: OPENAI_EMBEDDINGS_URL,
|
|
456
|
+
apiKey: this.#apiKey,
|
|
457
|
+
model: this.metadata.model,
|
|
458
|
+
texts,
|
|
459
|
+
signal,
|
|
460
|
+
dimensions: this.metadata.dimensions
|
|
461
|
+
}),
|
|
462
|
+
options.signal,
|
|
463
|
+
[this.#apiKey]
|
|
464
|
+
);
|
|
465
|
+
return createEmbeddingBatchResult(
|
|
466
|
+
this.metadata,
|
|
467
|
+
intent,
|
|
468
|
+
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "openai")
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
class MistralEmbeddingProvider {
|
|
473
|
+
metadata;
|
|
474
|
+
#apiKey;
|
|
475
|
+
#timeoutMs;
|
|
476
|
+
constructor(metadata, apiKey, timeoutMs) {
|
|
477
|
+
this.metadata = metadata;
|
|
478
|
+
this.#apiKey = apiKey;
|
|
479
|
+
this.#timeoutMs = timeoutMs;
|
|
480
|
+
}
|
|
481
|
+
async embed(texts, options = {}) {
|
|
482
|
+
const intent = resolveIntent(options.intent);
|
|
483
|
+
if (texts.length === 0) {
|
|
484
|
+
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
485
|
+
}
|
|
486
|
+
assertBatchSize(this.metadata, texts.length);
|
|
487
|
+
const items = await withTimeout(
|
|
488
|
+
"mistral",
|
|
489
|
+
"API request",
|
|
490
|
+
this.#timeoutMs,
|
|
491
|
+
async (signal) => requestOpenAICompatibleEmbeddings({
|
|
492
|
+
responseLabel: "Mistral",
|
|
493
|
+
endpoint: MISTRAL_EMBEDDINGS_URL,
|
|
494
|
+
apiKey: this.#apiKey,
|
|
495
|
+
model: this.metadata.model,
|
|
496
|
+
texts,
|
|
497
|
+
signal,
|
|
498
|
+
encodingFormat: "float",
|
|
499
|
+
requireIndex: true
|
|
500
|
+
}),
|
|
501
|
+
options.signal,
|
|
502
|
+
[this.#apiKey]
|
|
503
|
+
);
|
|
504
|
+
return createEmbeddingBatchResult(
|
|
505
|
+
this.metadata,
|
|
506
|
+
intent,
|
|
507
|
+
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "mistral")
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
class CloudflareEmbeddingProvider {
|
|
512
|
+
metadata;
|
|
513
|
+
#accountId;
|
|
514
|
+
#apiToken;
|
|
515
|
+
#timeoutMs;
|
|
516
|
+
constructor(metadata, accountId, apiToken, timeoutMs) {
|
|
517
|
+
this.metadata = metadata;
|
|
518
|
+
this.#accountId = accountId;
|
|
519
|
+
this.#apiToken = apiToken;
|
|
520
|
+
this.#timeoutMs = timeoutMs;
|
|
521
|
+
}
|
|
522
|
+
async embed(texts, options = {}) {
|
|
523
|
+
const intent = resolveIntent(options.intent);
|
|
524
|
+
if (texts.length === 0) {
|
|
525
|
+
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
526
|
+
}
|
|
527
|
+
assertBatchSize(this.metadata, texts.length);
|
|
528
|
+
const endpoint = createCloudflareEmbeddingsUrl(this.#accountId);
|
|
529
|
+
const items = await withTimeout(
|
|
530
|
+
"cloudflare",
|
|
531
|
+
"API request",
|
|
532
|
+
this.#timeoutMs,
|
|
351
533
|
async (signal) => {
|
|
352
|
-
const response = await fetch(
|
|
534
|
+
const response = await fetch(endpoint, {
|
|
353
535
|
method: "POST",
|
|
354
536
|
headers: {
|
|
355
537
|
"Content-Type": "application/json",
|
|
356
|
-
"Authorization": `Bearer ${this.#
|
|
538
|
+
"Authorization": `Bearer ${this.#apiToken}`
|
|
357
539
|
},
|
|
358
540
|
signal,
|
|
359
541
|
body: JSON.stringify({
|
|
360
|
-
input: texts,
|
|
361
542
|
model: this.metadata.model,
|
|
362
|
-
|
|
543
|
+
input: texts
|
|
363
544
|
})
|
|
364
545
|
});
|
|
365
546
|
if (!response.ok) {
|
|
366
|
-
const
|
|
367
|
-
const context =
|
|
547
|
+
const cfRay = getSafeResponseHeader(response, "cf-ray");
|
|
548
|
+
const context = cfRay ? ` status ${response.status}, cf-ray ${cfRay}` : ` status ${response.status}`;
|
|
368
549
|
throw new Error(context);
|
|
369
550
|
}
|
|
370
551
|
const data = await response.json();
|
|
371
552
|
if (!Array.isArray(data.data)) {
|
|
372
|
-
throw new Error("
|
|
553
|
+
throw new Error("Cloudflare response did not include a data array");
|
|
373
554
|
}
|
|
374
555
|
return data.data.map((item) => {
|
|
375
556
|
const typed = item;
|
|
376
557
|
const embedding = typed.embedding;
|
|
377
558
|
if (!Array.isArray(embedding)) {
|
|
378
|
-
throw new Error("
|
|
559
|
+
throw new Error("Cloudflare response item did not include an embedding array");
|
|
379
560
|
}
|
|
380
561
|
const parsed = { embedding };
|
|
381
562
|
if (hasOwnProperty(typed, "index")) {
|
|
@@ -385,12 +566,12 @@ class OpenAIEmbeddingProvider {
|
|
|
385
566
|
});
|
|
386
567
|
},
|
|
387
568
|
options.signal,
|
|
388
|
-
[this.#
|
|
569
|
+
[this.#apiToken, this.#accountId, encodeURIComponent(this.#accountId)]
|
|
389
570
|
);
|
|
390
571
|
return createEmbeddingBatchResult(
|
|
391
572
|
this.metadata,
|
|
392
573
|
intent,
|
|
393
|
-
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "
|
|
574
|
+
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "cloudflare")
|
|
394
575
|
);
|
|
395
576
|
}
|
|
396
577
|
}
|
|
@@ -415,6 +596,30 @@ function createEmbeddingProvider(options = {}) {
|
|
|
415
596
|
}
|
|
416
597
|
return new OpenAIEmbeddingProvider(metadata, key, timeoutMs);
|
|
417
598
|
}
|
|
599
|
+
case "mistral": {
|
|
600
|
+
const key = getOptionalTrimmedCredential(
|
|
601
|
+
options.apiKey ?? getEnvironmentVariable("MISTRAL_API_KEY")
|
|
602
|
+
);
|
|
603
|
+
if (!key) {
|
|
604
|
+
throw new Error("MISTRAL_API_KEY is required for Mistral embeddings");
|
|
605
|
+
}
|
|
606
|
+
return new MistralEmbeddingProvider(metadata, key, timeoutMs);
|
|
607
|
+
}
|
|
608
|
+
case "cloudflare": {
|
|
609
|
+
const accountId = getOptionalTrimmedCredential(
|
|
610
|
+
options.accountId ?? getEnvironmentVariable("CLOUDFLARE_ACCOUNT_ID")
|
|
611
|
+
);
|
|
612
|
+
const apiToken = getOptionalTrimmedCredential(
|
|
613
|
+
options.apiToken ?? getEnvironmentVariable("CLOUDFLARE_API_TOKEN")
|
|
614
|
+
);
|
|
615
|
+
if (!accountId) {
|
|
616
|
+
throw new Error("CLOUDFLARE_ACCOUNT_ID is required for Cloudflare embeddings");
|
|
617
|
+
}
|
|
618
|
+
if (!apiToken) {
|
|
619
|
+
throw new Error("CLOUDFLARE_API_TOKEN is required for Cloudflare embeddings");
|
|
620
|
+
}
|
|
621
|
+
return new CloudflareEmbeddingProvider(metadata, accountId, apiToken, timeoutMs);
|
|
622
|
+
}
|
|
418
623
|
}
|
|
419
624
|
}
|
|
420
625
|
function getEmbeddingProviderMetadata(options = {}) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Client } from '@libsql/client';
|
|
2
2
|
|
|
3
|
-
type EmbeddingProvider = 'local' | 'gemini' | 'openai';
|
|
3
|
+
type EmbeddingProvider = 'local' | 'gemini' | 'openai' | 'mistral' | 'cloudflare';
|
|
4
4
|
type EmbeddingIntent = 'document' | 'query';
|
|
5
5
|
type EmbeddingBatchMode = 'native' | 'sequential';
|
|
6
6
|
interface EmbeddingBatchBehavior {
|
|
@@ -31,6 +31,8 @@ interface EmbeddingBatchResult {
|
|
|
31
31
|
interface EmbeddingOptions {
|
|
32
32
|
provider?: EmbeddingProvider;
|
|
33
33
|
apiKey?: string;
|
|
34
|
+
accountId?: string;
|
|
35
|
+
apiToken?: string;
|
|
34
36
|
dimensions?: number;
|
|
35
37
|
maxLength?: number;
|
|
36
38
|
intent?: EmbeddingIntent;
|
package/dist/index.esm.js
CHANGED
|
@@ -10,6 +10,11 @@ const GEMINI_MODEL = "text-embedding-004";
|
|
|
10
10
|
const OPENAI_SMALL_MODEL = "text-embedding-3-small";
|
|
11
11
|
const OPENAI_LARGE_MODEL = "text-embedding-3-large";
|
|
12
12
|
const OPENAI_EMBEDDINGS_URL = "https://api.openai.com/v1/embeddings";
|
|
13
|
+
const MISTRAL_MODEL = "mistral-embed";
|
|
14
|
+
const MISTRAL_EMBEDDINGS_URL = "https://api.mistral.ai/v1/embeddings";
|
|
15
|
+
const MISTRAL_DIMENSIONS = 1024;
|
|
16
|
+
const CLOUDFLARE_MODEL = "@cf/baai/bge-m3";
|
|
17
|
+
const CLOUDFLARE_DIMENSIONS = 1024;
|
|
13
18
|
const localModelCacheByModel = /* @__PURE__ */ new Map();
|
|
14
19
|
function getEnvironmentVariable(name) {
|
|
15
20
|
const runtime = globalThis;
|
|
@@ -49,6 +54,8 @@ function resolveProviderName(provider) {
|
|
|
49
54
|
case "local":
|
|
50
55
|
case "gemini":
|
|
51
56
|
case "openai":
|
|
57
|
+
case "mistral":
|
|
58
|
+
case "cloudflare":
|
|
52
59
|
return provider ?? "local";
|
|
53
60
|
default:
|
|
54
61
|
throw new Error(`Unknown embedding provider: ${String(provider)}`);
|
|
@@ -69,9 +76,16 @@ function truncateTexts(texts, maxLength) {
|
|
|
69
76
|
function getOpenAIModel(dimensions) {
|
|
70
77
|
return dimensions <= 1536 ? OPENAI_SMALL_MODEL : OPENAI_LARGE_MODEL;
|
|
71
78
|
}
|
|
79
|
+
function getOptionalTrimmedCredential(value) {
|
|
80
|
+
const trimmed = value?.trim();
|
|
81
|
+
return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
|
|
82
|
+
}
|
|
72
83
|
function hasOwnProperty(value, property) {
|
|
73
84
|
return Object.prototype.hasOwnProperty.call(value, property);
|
|
74
85
|
}
|
|
86
|
+
function isRecord(value) {
|
|
87
|
+
return typeof value === "object" && value !== null;
|
|
88
|
+
}
|
|
75
89
|
function redactErrorText(value, exactSecrets = []) {
|
|
76
90
|
let raw = value instanceof Error ? value.message : String(value);
|
|
77
91
|
for (const secret of exactSecrets) {
|
|
@@ -95,6 +109,16 @@ function providerError(provider, message, cause, exactSecrets = []) {
|
|
|
95
109
|
function getResponseHeader(response, name) {
|
|
96
110
|
return response.headers.get(name) ?? void 0;
|
|
97
111
|
}
|
|
112
|
+
function getSafeResponseHeader(response, name) {
|
|
113
|
+
const value = getResponseHeader(response, name)?.trim();
|
|
114
|
+
if (!value) {
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
return value.replace(/[^A-Za-z0-9._:-]/g, "").slice(0, 128);
|
|
118
|
+
}
|
|
119
|
+
function createCloudflareEmbeddingsUrl(accountId) {
|
|
120
|
+
return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/v1/embeddings`;
|
|
121
|
+
}
|
|
98
122
|
async function withTimeout(provider, operation, timeoutMs, run, parentSignal, exactSecrets = []) {
|
|
99
123
|
if (parentSignal?.aborted) {
|
|
100
124
|
throw providerError(provider, `${operation} was aborted`);
|
|
@@ -223,12 +247,44 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
223
247
|
dimensions,
|
|
224
248
|
batch: Object.freeze({ mode: "native", maxSize: 2048 })
|
|
225
249
|
});
|
|
250
|
+
case "mistral":
|
|
251
|
+
return Object.freeze({
|
|
252
|
+
name: "mistral",
|
|
253
|
+
model: MISTRAL_MODEL,
|
|
254
|
+
dimensions: MISTRAL_DIMENSIONS,
|
|
255
|
+
batch: Object.freeze({ mode: "native" })
|
|
256
|
+
});
|
|
257
|
+
case "cloudflare":
|
|
258
|
+
return Object.freeze({
|
|
259
|
+
name: "cloudflare",
|
|
260
|
+
model: CLOUDFLARE_MODEL,
|
|
261
|
+
dimensions: CLOUDFLARE_DIMENSIONS,
|
|
262
|
+
batch: Object.freeze({ mode: "native" })
|
|
263
|
+
});
|
|
226
264
|
}
|
|
227
265
|
}
|
|
228
266
|
function getEffectiveDimensions(provider, dimensions) {
|
|
229
267
|
if (provider === "gemini") {
|
|
230
268
|
return DEFAULT_DIMENSIONS;
|
|
231
269
|
}
|
|
270
|
+
if (provider === "mistral") {
|
|
271
|
+
if (dimensions !== void 0 && dimensions !== MISTRAL_DIMENSIONS) {
|
|
272
|
+
throw providerError(
|
|
273
|
+
"mistral",
|
|
274
|
+
`${MISTRAL_MODEL} returns ${MISTRAL_DIMENSIONS} dimensions; received dimensions ${dimensions}`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return MISTRAL_DIMENSIONS;
|
|
278
|
+
}
|
|
279
|
+
if (provider === "cloudflare") {
|
|
280
|
+
if (dimensions !== void 0 && dimensions !== CLOUDFLARE_DIMENSIONS) {
|
|
281
|
+
throw providerError(
|
|
282
|
+
"cloudflare",
|
|
283
|
+
`@cf/baai/bge-m3 returns ${CLOUDFLARE_DIMENSIONS} dimensions; received dimensions ${dimensions}`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
return CLOUDFLARE_DIMENSIONS;
|
|
287
|
+
}
|
|
232
288
|
return getPositiveInteger(dimensions ?? DEFAULT_DIMENSIONS, "dimensions");
|
|
233
289
|
}
|
|
234
290
|
function assertBatchSize(metadata, count) {
|
|
@@ -327,6 +383,52 @@ class GeminiEmbeddingProvider {
|
|
|
327
383
|
);
|
|
328
384
|
}
|
|
329
385
|
}
|
|
386
|
+
async function requestOpenAICompatibleEmbeddings(request) {
|
|
387
|
+
const body = {
|
|
388
|
+
input: request.texts,
|
|
389
|
+
model: request.model
|
|
390
|
+
};
|
|
391
|
+
if (request.dimensions !== void 0) {
|
|
392
|
+
body.dimensions = request.dimensions;
|
|
393
|
+
}
|
|
394
|
+
if (request.encodingFormat !== void 0) {
|
|
395
|
+
body.encoding_format = request.encodingFormat;
|
|
396
|
+
}
|
|
397
|
+
const response = await fetch(request.endpoint, {
|
|
398
|
+
method: "POST",
|
|
399
|
+
headers: {
|
|
400
|
+
"Content-Type": "application/json",
|
|
401
|
+
"Authorization": `Bearer ${request.apiKey}`
|
|
402
|
+
},
|
|
403
|
+
signal: request.signal,
|
|
404
|
+
body: JSON.stringify(body)
|
|
405
|
+
});
|
|
406
|
+
if (!response.ok) {
|
|
407
|
+
const requestId = getSafeResponseHeader(response, "x-request-id");
|
|
408
|
+
const context = requestId ? ` status ${response.status}, request ${requestId}` : ` status ${response.status}`;
|
|
409
|
+
throw new Error(context);
|
|
410
|
+
}
|
|
411
|
+
const data = await response.json();
|
|
412
|
+
if (!Array.isArray(data.data)) {
|
|
413
|
+
throw new Error(`${request.responseLabel} response did not include a data array`);
|
|
414
|
+
}
|
|
415
|
+
return data.data.map((item) => {
|
|
416
|
+
if (!isRecord(item)) {
|
|
417
|
+
throw new Error(`${request.responseLabel} response item was not an object`);
|
|
418
|
+
}
|
|
419
|
+
const embedding = item.embedding;
|
|
420
|
+
if (!Array.isArray(embedding)) {
|
|
421
|
+
throw new Error(`${request.responseLabel} response item did not include an embedding array`);
|
|
422
|
+
}
|
|
423
|
+
const parsed = { embedding };
|
|
424
|
+
if (hasOwnProperty(item, "index")) {
|
|
425
|
+
parsed.index = item.index;
|
|
426
|
+
} else if (request.requireIndex) {
|
|
427
|
+
throw new Error(`${request.responseLabel} response item did not include an index`);
|
|
428
|
+
}
|
|
429
|
+
return parsed;
|
|
430
|
+
});
|
|
431
|
+
}
|
|
330
432
|
class OpenAIEmbeddingProvider {
|
|
331
433
|
metadata;
|
|
332
434
|
#apiKey;
|
|
@@ -346,34 +448,113 @@ class OpenAIEmbeddingProvider {
|
|
|
346
448
|
"openai",
|
|
347
449
|
"API request",
|
|
348
450
|
this.#timeoutMs,
|
|
451
|
+
async (signal) => requestOpenAICompatibleEmbeddings({
|
|
452
|
+
responseLabel: "OpenAI",
|
|
453
|
+
endpoint: OPENAI_EMBEDDINGS_URL,
|
|
454
|
+
apiKey: this.#apiKey,
|
|
455
|
+
model: this.metadata.model,
|
|
456
|
+
texts,
|
|
457
|
+
signal,
|
|
458
|
+
dimensions: this.metadata.dimensions
|
|
459
|
+
}),
|
|
460
|
+
options.signal,
|
|
461
|
+
[this.#apiKey]
|
|
462
|
+
);
|
|
463
|
+
return createEmbeddingBatchResult(
|
|
464
|
+
this.metadata,
|
|
465
|
+
intent,
|
|
466
|
+
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "openai")
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
class MistralEmbeddingProvider {
|
|
471
|
+
metadata;
|
|
472
|
+
#apiKey;
|
|
473
|
+
#timeoutMs;
|
|
474
|
+
constructor(metadata, apiKey, timeoutMs) {
|
|
475
|
+
this.metadata = metadata;
|
|
476
|
+
this.#apiKey = apiKey;
|
|
477
|
+
this.#timeoutMs = timeoutMs;
|
|
478
|
+
}
|
|
479
|
+
async embed(texts, options = {}) {
|
|
480
|
+
const intent = resolveIntent(options.intent);
|
|
481
|
+
if (texts.length === 0) {
|
|
482
|
+
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
483
|
+
}
|
|
484
|
+
assertBatchSize(this.metadata, texts.length);
|
|
485
|
+
const items = await withTimeout(
|
|
486
|
+
"mistral",
|
|
487
|
+
"API request",
|
|
488
|
+
this.#timeoutMs,
|
|
489
|
+
async (signal) => requestOpenAICompatibleEmbeddings({
|
|
490
|
+
responseLabel: "Mistral",
|
|
491
|
+
endpoint: MISTRAL_EMBEDDINGS_URL,
|
|
492
|
+
apiKey: this.#apiKey,
|
|
493
|
+
model: this.metadata.model,
|
|
494
|
+
texts,
|
|
495
|
+
signal,
|
|
496
|
+
encodingFormat: "float",
|
|
497
|
+
requireIndex: true
|
|
498
|
+
}),
|
|
499
|
+
options.signal,
|
|
500
|
+
[this.#apiKey]
|
|
501
|
+
);
|
|
502
|
+
return createEmbeddingBatchResult(
|
|
503
|
+
this.metadata,
|
|
504
|
+
intent,
|
|
505
|
+
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "mistral")
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
class CloudflareEmbeddingProvider {
|
|
510
|
+
metadata;
|
|
511
|
+
#accountId;
|
|
512
|
+
#apiToken;
|
|
513
|
+
#timeoutMs;
|
|
514
|
+
constructor(metadata, accountId, apiToken, timeoutMs) {
|
|
515
|
+
this.metadata = metadata;
|
|
516
|
+
this.#accountId = accountId;
|
|
517
|
+
this.#apiToken = apiToken;
|
|
518
|
+
this.#timeoutMs = timeoutMs;
|
|
519
|
+
}
|
|
520
|
+
async embed(texts, options = {}) {
|
|
521
|
+
const intent = resolveIntent(options.intent);
|
|
522
|
+
if (texts.length === 0) {
|
|
523
|
+
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
524
|
+
}
|
|
525
|
+
assertBatchSize(this.metadata, texts.length);
|
|
526
|
+
const endpoint = createCloudflareEmbeddingsUrl(this.#accountId);
|
|
527
|
+
const items = await withTimeout(
|
|
528
|
+
"cloudflare",
|
|
529
|
+
"API request",
|
|
530
|
+
this.#timeoutMs,
|
|
349
531
|
async (signal) => {
|
|
350
|
-
const response = await fetch(
|
|
532
|
+
const response = await fetch(endpoint, {
|
|
351
533
|
method: "POST",
|
|
352
534
|
headers: {
|
|
353
535
|
"Content-Type": "application/json",
|
|
354
|
-
"Authorization": `Bearer ${this.#
|
|
536
|
+
"Authorization": `Bearer ${this.#apiToken}`
|
|
355
537
|
},
|
|
356
538
|
signal,
|
|
357
539
|
body: JSON.stringify({
|
|
358
|
-
input: texts,
|
|
359
540
|
model: this.metadata.model,
|
|
360
|
-
|
|
541
|
+
input: texts
|
|
361
542
|
})
|
|
362
543
|
});
|
|
363
544
|
if (!response.ok) {
|
|
364
|
-
const
|
|
365
|
-
const context =
|
|
545
|
+
const cfRay = getSafeResponseHeader(response, "cf-ray");
|
|
546
|
+
const context = cfRay ? ` status ${response.status}, cf-ray ${cfRay}` : ` status ${response.status}`;
|
|
366
547
|
throw new Error(context);
|
|
367
548
|
}
|
|
368
549
|
const data = await response.json();
|
|
369
550
|
if (!Array.isArray(data.data)) {
|
|
370
|
-
throw new Error("
|
|
551
|
+
throw new Error("Cloudflare response did not include a data array");
|
|
371
552
|
}
|
|
372
553
|
return data.data.map((item) => {
|
|
373
554
|
const typed = item;
|
|
374
555
|
const embedding = typed.embedding;
|
|
375
556
|
if (!Array.isArray(embedding)) {
|
|
376
|
-
throw new Error("
|
|
557
|
+
throw new Error("Cloudflare response item did not include an embedding array");
|
|
377
558
|
}
|
|
378
559
|
const parsed = { embedding };
|
|
379
560
|
if (hasOwnProperty(typed, "index")) {
|
|
@@ -383,12 +564,12 @@ class OpenAIEmbeddingProvider {
|
|
|
383
564
|
});
|
|
384
565
|
},
|
|
385
566
|
options.signal,
|
|
386
|
-
[this.#
|
|
567
|
+
[this.#apiToken, this.#accountId, encodeURIComponent(this.#accountId)]
|
|
387
568
|
);
|
|
388
569
|
return createEmbeddingBatchResult(
|
|
389
570
|
this.metadata,
|
|
390
571
|
intent,
|
|
391
|
-
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "
|
|
572
|
+
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "cloudflare")
|
|
392
573
|
);
|
|
393
574
|
}
|
|
394
575
|
}
|
|
@@ -413,6 +594,30 @@ function createEmbeddingProvider(options = {}) {
|
|
|
413
594
|
}
|
|
414
595
|
return new OpenAIEmbeddingProvider(metadata, key, timeoutMs);
|
|
415
596
|
}
|
|
597
|
+
case "mistral": {
|
|
598
|
+
const key = getOptionalTrimmedCredential(
|
|
599
|
+
options.apiKey ?? getEnvironmentVariable("MISTRAL_API_KEY")
|
|
600
|
+
);
|
|
601
|
+
if (!key) {
|
|
602
|
+
throw new Error("MISTRAL_API_KEY is required for Mistral embeddings");
|
|
603
|
+
}
|
|
604
|
+
return new MistralEmbeddingProvider(metadata, key, timeoutMs);
|
|
605
|
+
}
|
|
606
|
+
case "cloudflare": {
|
|
607
|
+
const accountId = getOptionalTrimmedCredential(
|
|
608
|
+
options.accountId ?? getEnvironmentVariable("CLOUDFLARE_ACCOUNT_ID")
|
|
609
|
+
);
|
|
610
|
+
const apiToken = getOptionalTrimmedCredential(
|
|
611
|
+
options.apiToken ?? getEnvironmentVariable("CLOUDFLARE_API_TOKEN")
|
|
612
|
+
);
|
|
613
|
+
if (!accountId) {
|
|
614
|
+
throw new Error("CLOUDFLARE_ACCOUNT_ID is required for Cloudflare embeddings");
|
|
615
|
+
}
|
|
616
|
+
if (!apiToken) {
|
|
617
|
+
throw new Error("CLOUDFLARE_API_TOKEN is required for Cloudflare embeddings");
|
|
618
|
+
}
|
|
619
|
+
return new CloudflareEmbeddingProvider(metadata, accountId, apiToken, timeoutMs);
|
|
620
|
+
}
|
|
416
621
|
}
|
|
417
622
|
}
|
|
418
623
|
function getEmbeddingProviderMetadata(options = {}) {
|
package/docs/API.md
CHANGED
|
@@ -210,7 +210,8 @@ Provider metadata includes:
|
|
|
210
210
|
- `batch.maxSize`, when the provider has a hard maximum
|
|
211
211
|
|
|
212
212
|
Hosted provider clients are scoped to their options. The library does not reuse
|
|
213
|
-
a Gemini or OpenAI client created with
|
|
213
|
+
a Cloudflare, Mistral, Gemini, or OpenAI client created with different
|
|
214
|
+
credentials or configuration.
|
|
214
215
|
|
|
215
216
|
### `getEmbeddingProviderMetadata(options?)`
|
|
216
217
|
|
|
@@ -238,7 +239,7 @@ Provider clients return:
|
|
|
238
239
|
```ts
|
|
239
240
|
interface EmbeddingBatchResult {
|
|
240
241
|
embeddings: number[][];
|
|
241
|
-
provider: "local" | "gemini" | "openai";
|
|
242
|
+
provider: "local" | "cloudflare" | "mistral" | "gemini" | "openai";
|
|
242
243
|
model: string;
|
|
243
244
|
dimensions: number;
|
|
244
245
|
intent: "document" | "query";
|
|
@@ -254,8 +255,10 @@ cardinality, dimensions, finite numeric values, and indexed batch ordering.
|
|
|
254
255
|
|
|
255
256
|
```ts
|
|
256
257
|
interface EmbeddingOptions {
|
|
257
|
-
provider?: "local" | "gemini" | "openai";
|
|
258
|
+
provider?: "local" | "cloudflare" | "mistral" | "gemini" | "openai";
|
|
258
259
|
apiKey?: string;
|
|
260
|
+
accountId?: string;
|
|
261
|
+
apiToken?: string;
|
|
259
262
|
dimensions?: number;
|
|
260
263
|
maxLength?: number;
|
|
261
264
|
intent?: "document" | "query";
|
|
@@ -264,6 +267,11 @@ interface EmbeddingOptions {
|
|
|
264
267
|
}
|
|
265
268
|
```
|
|
266
269
|
|
|
270
|
+
`apiKey` is used by Mistral, Gemini, and OpenAI. It falls back to
|
|
271
|
+
`MISTRAL_API_KEY`, `GEMINI_API_KEY`, or `OPENAI_API_KEY` for those providers.
|
|
272
|
+
Cloudflare uses `accountId` and `apiToken`, which fall back to
|
|
273
|
+
`CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`.
|
|
274
|
+
|
|
267
275
|
### `padEmbedding(embedding, targetDimensions)`
|
|
268
276
|
|
|
269
277
|
Pads or truncates an embedding array to the requested length.
|
package/docs/INTEGRATIONS.md
CHANGED
|
@@ -127,14 +127,27 @@ const client = createClient({
|
|
|
127
127
|
authToken: process.env.TURSO_AUTH_TOKEN!,
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
const embeddingProvider =
|
|
131
|
+
process.env.EMBEDDING_PROVIDER as
|
|
132
|
+
| "local"
|
|
133
|
+
| "cloudflare"
|
|
134
|
+
| "mistral"
|
|
135
|
+
| "gemini"
|
|
136
|
+
| "openai"
|
|
137
|
+
| undefined;
|
|
138
|
+
const embeddingDimensions =
|
|
139
|
+
embeddingProvider === "cloudflare" || embeddingProvider === "mistral"
|
|
140
|
+
? 1024
|
|
141
|
+
: 768;
|
|
142
|
+
|
|
143
|
+
await createTable(client, "articles", embeddingDimensions);
|
|
131
144
|
|
|
132
145
|
await indexContent({
|
|
133
146
|
client,
|
|
134
147
|
contentPath: "./content",
|
|
135
148
|
embeddingOptions: {
|
|
136
|
-
provider:
|
|
137
|
-
dimensions:
|
|
149
|
+
provider: embeddingProvider,
|
|
150
|
+
dimensions: embeddingDimensions,
|
|
138
151
|
},
|
|
139
152
|
});
|
|
140
153
|
```
|
package/docs/PROVIDERS.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Embedding Providers
|
|
2
2
|
|
|
3
|
-
`libsql-search` currently supports
|
|
3
|
+
`libsql-search` currently supports five embedding providers:
|
|
4
4
|
|
|
5
5
|
- `local`
|
|
6
|
+
- `cloudflare`
|
|
7
|
+
- `mistral`
|
|
6
8
|
- `gemini`
|
|
7
9
|
- `openai`
|
|
8
10
|
|
|
@@ -14,8 +16,10 @@ query time.
|
|
|
14
16
|
|
|
15
17
|
```ts
|
|
16
18
|
interface EmbeddingOptions {
|
|
17
|
-
provider?: "local" | "gemini" | "openai";
|
|
19
|
+
provider?: "local" | "cloudflare" | "mistral" | "gemini" | "openai";
|
|
18
20
|
apiKey?: string;
|
|
21
|
+
accountId?: string;
|
|
22
|
+
apiToken?: string;
|
|
19
23
|
dimensions?: number;
|
|
20
24
|
maxLength?: number;
|
|
21
25
|
intent?: "document" | "query";
|
|
@@ -30,8 +34,10 @@ interface EmbeddingOptions {
|
|
|
30
34
|
- `intent` can be `"document"` or `"query"`; indexing defaults to
|
|
31
35
|
`"document"` and search defaults to `"query"` unless explicitly set
|
|
32
36
|
- `timeoutMs` defaults to `30000`
|
|
33
|
-
- `apiKey` is
|
|
34
|
-
|
|
37
|
+
- `apiKey` is used by Mistral, Gemini, and OpenAI and falls back to
|
|
38
|
+
`MISTRAL_API_KEY`, `GEMINI_API_KEY`, or `OPENAI_API_KEY`
|
|
39
|
+
- `accountId` and `apiToken` are used by Cloudflare and fall back to
|
|
40
|
+
`CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`
|
|
35
41
|
|
|
36
42
|
## Provider Contract
|
|
37
43
|
|
|
@@ -39,7 +45,7 @@ Each provider exposes immutable metadata:
|
|
|
39
45
|
|
|
40
46
|
```ts
|
|
41
47
|
interface EmbeddingProviderMetadata {
|
|
42
|
-
name: "local" | "gemini" | "openai";
|
|
48
|
+
name: "local" | "cloudflare" | "mistral" | "gemini" | "openai";
|
|
43
49
|
model: string;
|
|
44
50
|
dimensions: number;
|
|
45
51
|
batch: {
|
|
@@ -78,9 +84,9 @@ Lower-level provider clients return an `EmbeddingBatchResult` with the validated
|
|
|
78
84
|
vectors plus provider, model, dimensions, and intent. The compatibility helpers
|
|
79
85
|
`generateEmbedding()` and `generateEmbeddings()` return only arrays.
|
|
80
86
|
|
|
81
|
-
Gemini and OpenAI clients are scoped to their current
|
|
82
|
-
cached globally across different credentials or
|
|
83
|
-
model can be cached by model name.
|
|
87
|
+
Cloudflare, Mistral, Gemini, and OpenAI clients are scoped to their current
|
|
88
|
+
options. They are not cached globally across different credentials or
|
|
89
|
+
configurations. The local Xenova model can be cached by model name.
|
|
84
90
|
|
|
85
91
|
Hosted provider failures are reported with bounded provider/status/request-id
|
|
86
92
|
context and without raw upstream bodies, credentials, Authorization headers, or
|
|
@@ -108,6 +114,63 @@ Notes:
|
|
|
108
114
|
- batch metadata is `{ mode: "sequential" }`
|
|
109
115
|
- the first run downloads the model and can take longer on a fresh machine
|
|
110
116
|
- no API key is required
|
|
117
|
+
- this remains the default provider for backward compatibility and offline use
|
|
118
|
+
|
|
119
|
+
## Cloudflare Workers AI
|
|
120
|
+
|
|
121
|
+
Provider value: `cloudflare`
|
|
122
|
+
|
|
123
|
+
Cloudflare is the recommended hosted provider for low-cost Markdown search.
|
|
124
|
+
It uses Workers AI `@cf/baai/bge-m3` through Cloudflare's OpenAI-compatible
|
|
125
|
+
embeddings endpoint.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
embeddingOptions: {
|
|
129
|
+
provider: "cloudflare",
|
|
130
|
+
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
|
131
|
+
apiToken: process.env.CLOUDFLARE_API_TOKEN,
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Behavior:
|
|
136
|
+
|
|
137
|
+
- if `accountId` is omitted, the library reads `CLOUDFLARE_ACCOUNT_ID`
|
|
138
|
+
- if `apiToken` is omitted, the library reads `CLOUDFLARE_API_TOKEN`
|
|
139
|
+
- blank Cloudflare credentials are treated as missing
|
|
140
|
+
- `@cf/baai/bge-m3` returns 1024 dimensions
|
|
141
|
+
- metadata reports `@cf/baai/bge-m3` and 1024 dimensions without requiring
|
|
142
|
+
credentials
|
|
143
|
+
- batch metadata is `{ mode: "native" }`
|
|
144
|
+
- response items are reordered by provider-supplied index before being returned
|
|
145
|
+
- Cloudflare does not accept custom dimensions in this provider; use
|
|
146
|
+
`createTable(client, "articles", 1024)` for Cloudflare-backed indexes
|
|
147
|
+
|
|
148
|
+
## Mistral
|
|
149
|
+
|
|
150
|
+
Provider value: `mistral`
|
|
151
|
+
|
|
152
|
+
Mistral uses the hosted `mistral-embed` model through
|
|
153
|
+
`https://api.mistral.ai/v1/embeddings`.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
embeddingOptions: {
|
|
157
|
+
provider: "mistral",
|
|
158
|
+
apiKey: process.env.MISTRAL_API_KEY,
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Behavior:
|
|
163
|
+
|
|
164
|
+
- if `apiKey` is omitted, the library reads `MISTRAL_API_KEY`
|
|
165
|
+
- blank Mistral credentials are treated as missing
|
|
166
|
+
- `mistral-embed` returns 1024 dimensions
|
|
167
|
+
- metadata reports `mistral-embed` and 1024 dimensions without requiring
|
|
168
|
+
credentials
|
|
169
|
+
- batch metadata is `{ mode: "native" }`
|
|
170
|
+
- request bodies send `encoding_format: "float"`
|
|
171
|
+
- response items are reordered by provider-supplied index before being returned
|
|
172
|
+
- Mistral does not accept custom dimensions in this provider; use
|
|
173
|
+
`createTable(client, "articles", 1024)` for Mistral-backed indexes
|
|
111
174
|
|
|
112
175
|
## Gemini
|
|
113
176
|
|
|
@@ -156,7 +219,9 @@ Behavior:
|
|
|
156
219
|
|
|
157
220
|
## Dimension Guidelines
|
|
158
221
|
|
|
159
|
-
- `
|
|
222
|
+
- `local` defaults to `768`
|
|
223
|
+
- `cloudflare` is fixed at `1024`
|
|
224
|
+
- `mistral` is fixed at `1024`
|
|
160
225
|
- local embeddings are padded from 384 to your target size
|
|
161
226
|
- Gemini stays at 768
|
|
162
227
|
- OpenAI can be used at 1536 or 3072, or another supported OpenAI dimension
|
package/docs/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
This directory holds the longer-form reference material for `libsql-search`.
|
|
4
4
|
Start with the page that matches the job you are doing:
|
|
5
5
|
|
|
6
|
-
- [Provider guide](./PROVIDERS.md): local, Gemini, and
|
|
7
|
-
dimensions, and API key behavior
|
|
6
|
+
- [Provider guide](./PROVIDERS.md): local, Cloudflare, Mistral, Gemini, and
|
|
7
|
+
OpenAI embedding options, dimensions, and API key behavior
|
|
8
8
|
- [API reference](./API.md): exported functions, option shapes, and result data
|
|
9
9
|
- [Integration examples](./INTEGRATIONS.md): Astro and Next.js server-side usage
|
|
10
10
|
- [Indexing and operations](./INDEXING.md): content layout, rebuild scripts,
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -9,4 +9,6 @@ Common operational checks:
|
|
|
9
9
|
- verify you called `createTable()` before indexing or searching
|
|
10
10
|
- verify the table dimension matches the embedding dimension in your code
|
|
11
11
|
- verify the same provider is used for indexing and querying
|
|
12
|
-
- verify hosted providers have `
|
|
12
|
+
- verify hosted providers have `CLOUDFLARE_ACCOUNT_ID` and
|
|
13
|
+
`CLOUDFLARE_API_TOKEN`, `MISTRAL_API_KEY`, `GEMINI_API_KEY`, or
|
|
14
|
+
`OPENAI_API_KEY` available as required by the selected provider
|