libsql-search 0.3.0 → 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 +4 -3
- package/dist/index.cjs +126 -36
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +126 -36
- package/docs/API.md +8 -7
- package/docs/INTEGRATIONS.md +11 -2
- package/docs/PROVIDERS.md +37 -8
- package/docs/README.md +2 -2
- package/docs/TROUBLESHOOTING.md +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,8 +21,9 @@ Use it when you want:
|
|
|
21
21
|
- Markdown indexing from local directories with frontmatter via `gray-matter`
|
|
22
22
|
- libSQL/Turso storage and vector search
|
|
23
23
|
- Embedding providers: local `Xenova/all-MiniLM-L6-v2`, Cloudflare Workers AI
|
|
24
|
-
`@cf/baai/bge-m3`,
|
|
25
|
-
`text-embedding-
|
|
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
|
|
@@ -98,7 +99,7 @@ Important behavior:
|
|
|
98
99
|
- Keep dimensions aligned across table creation, indexing, and search queries.
|
|
99
100
|
- `indexContent()` clears existing rows before rebuilding the index.
|
|
100
101
|
- `local` is the default offline provider; Cloudflare is the recommended hosted
|
|
101
|
-
option and
|
|
102
|
+
option. Cloudflare and Mistral use 1024 dimensions.
|
|
102
103
|
|
|
103
104
|
## Core API
|
|
104
105
|
|
package/dist/index.cjs
CHANGED
|
@@ -12,6 +12,9 @@ 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;
|
|
15
18
|
const CLOUDFLARE_MODEL = "@cf/baai/bge-m3";
|
|
16
19
|
const CLOUDFLARE_DIMENSIONS = 1024;
|
|
17
20
|
const localModelCacheByModel = /* @__PURE__ */ new Map();
|
|
@@ -53,6 +56,7 @@ function resolveProviderName(provider) {
|
|
|
53
56
|
case "local":
|
|
54
57
|
case "gemini":
|
|
55
58
|
case "openai":
|
|
59
|
+
case "mistral":
|
|
56
60
|
case "cloudflare":
|
|
57
61
|
return provider ?? "local";
|
|
58
62
|
default:
|
|
@@ -81,6 +85,9 @@ function getOptionalTrimmedCredential(value) {
|
|
|
81
85
|
function hasOwnProperty(value, property) {
|
|
82
86
|
return Object.prototype.hasOwnProperty.call(value, property);
|
|
83
87
|
}
|
|
88
|
+
function isRecord(value) {
|
|
89
|
+
return typeof value === "object" && value !== null;
|
|
90
|
+
}
|
|
84
91
|
function redactErrorText(value, exactSecrets = []) {
|
|
85
92
|
let raw = value instanceof Error ? value.message : String(value);
|
|
86
93
|
for (const secret of exactSecrets) {
|
|
@@ -242,6 +249,13 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
242
249
|
dimensions,
|
|
243
250
|
batch: Object.freeze({ mode: "native", maxSize: 2048 })
|
|
244
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
|
+
});
|
|
245
259
|
case "cloudflare":
|
|
246
260
|
return Object.freeze({
|
|
247
261
|
name: "cloudflare",
|
|
@@ -255,6 +269,15 @@ function getEffectiveDimensions(provider, dimensions) {
|
|
|
255
269
|
if (provider === "gemini") {
|
|
256
270
|
return DEFAULT_DIMENSIONS;
|
|
257
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
|
+
}
|
|
258
281
|
if (provider === "cloudflare") {
|
|
259
282
|
if (dimensions !== void 0 && dimensions !== CLOUDFLARE_DIMENSIONS) {
|
|
260
283
|
throw providerError(
|
|
@@ -362,6 +385,52 @@ class GeminiEmbeddingProvider {
|
|
|
362
385
|
);
|
|
363
386
|
}
|
|
364
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
|
+
}
|
|
365
434
|
class OpenAIEmbeddingProvider {
|
|
366
435
|
metadata;
|
|
367
436
|
#apiKey;
|
|
@@ -381,42 +450,15 @@ class OpenAIEmbeddingProvider {
|
|
|
381
450
|
"openai",
|
|
382
451
|
"API request",
|
|
383
452
|
this.#timeoutMs,
|
|
384
|
-
async (signal) => {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
input: texts,
|
|
394
|
-
model: this.metadata.model,
|
|
395
|
-
dimensions: this.metadata.dimensions
|
|
396
|
-
})
|
|
397
|
-
});
|
|
398
|
-
if (!response.ok) {
|
|
399
|
-
const requestId = getResponseHeader(response, "x-request-id");
|
|
400
|
-
const context = requestId ? ` status ${response.status}, request ${requestId}` : ` status ${response.status}`;
|
|
401
|
-
throw new Error(context);
|
|
402
|
-
}
|
|
403
|
-
const data = await response.json();
|
|
404
|
-
if (!Array.isArray(data.data)) {
|
|
405
|
-
throw new Error("OpenAI response did not include a data array");
|
|
406
|
-
}
|
|
407
|
-
return data.data.map((item) => {
|
|
408
|
-
const typed = item;
|
|
409
|
-
const embedding = typed.embedding;
|
|
410
|
-
if (!Array.isArray(embedding)) {
|
|
411
|
-
throw new Error("OpenAI response item did not include an embedding array");
|
|
412
|
-
}
|
|
413
|
-
const parsed = { embedding };
|
|
414
|
-
if (hasOwnProperty(typed, "index")) {
|
|
415
|
-
parsed.index = typed.index;
|
|
416
|
-
}
|
|
417
|
-
return parsed;
|
|
418
|
-
});
|
|
419
|
-
},
|
|
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
|
+
}),
|
|
420
462
|
options.signal,
|
|
421
463
|
[this.#apiKey]
|
|
422
464
|
);
|
|
@@ -427,6 +469,45 @@ class OpenAIEmbeddingProvider {
|
|
|
427
469
|
);
|
|
428
470
|
}
|
|
429
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
|
+
}
|
|
430
511
|
class CloudflareEmbeddingProvider {
|
|
431
512
|
metadata;
|
|
432
513
|
#accountId;
|
|
@@ -515,6 +596,15 @@ function createEmbeddingProvider(options = {}) {
|
|
|
515
596
|
}
|
|
516
597
|
return new OpenAIEmbeddingProvider(metadata, key, timeoutMs);
|
|
517
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
|
+
}
|
|
518
608
|
case "cloudflare": {
|
|
519
609
|
const accountId = getOptionalTrimmedCredential(
|
|
520
610
|
options.accountId ?? getEnvironmentVariable("CLOUDFLARE_ACCOUNT_ID")
|
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' | 'cloudflare';
|
|
3
|
+
type EmbeddingProvider = 'local' | 'gemini' | 'openai' | 'mistral' | 'cloudflare';
|
|
4
4
|
type EmbeddingIntent = 'document' | 'query';
|
|
5
5
|
type EmbeddingBatchMode = 'native' | 'sequential';
|
|
6
6
|
interface EmbeddingBatchBehavior {
|
package/dist/index.esm.js
CHANGED
|
@@ -10,6 +10,9 @@ 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;
|
|
13
16
|
const CLOUDFLARE_MODEL = "@cf/baai/bge-m3";
|
|
14
17
|
const CLOUDFLARE_DIMENSIONS = 1024;
|
|
15
18
|
const localModelCacheByModel = /* @__PURE__ */ new Map();
|
|
@@ -51,6 +54,7 @@ function resolveProviderName(provider) {
|
|
|
51
54
|
case "local":
|
|
52
55
|
case "gemini":
|
|
53
56
|
case "openai":
|
|
57
|
+
case "mistral":
|
|
54
58
|
case "cloudflare":
|
|
55
59
|
return provider ?? "local";
|
|
56
60
|
default:
|
|
@@ -79,6 +83,9 @@ function getOptionalTrimmedCredential(value) {
|
|
|
79
83
|
function hasOwnProperty(value, property) {
|
|
80
84
|
return Object.prototype.hasOwnProperty.call(value, property);
|
|
81
85
|
}
|
|
86
|
+
function isRecord(value) {
|
|
87
|
+
return typeof value === "object" && value !== null;
|
|
88
|
+
}
|
|
82
89
|
function redactErrorText(value, exactSecrets = []) {
|
|
83
90
|
let raw = value instanceof Error ? value.message : String(value);
|
|
84
91
|
for (const secret of exactSecrets) {
|
|
@@ -240,6 +247,13 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
240
247
|
dimensions,
|
|
241
248
|
batch: Object.freeze({ mode: "native", maxSize: 2048 })
|
|
242
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
|
+
});
|
|
243
257
|
case "cloudflare":
|
|
244
258
|
return Object.freeze({
|
|
245
259
|
name: "cloudflare",
|
|
@@ -253,6 +267,15 @@ function getEffectiveDimensions(provider, dimensions) {
|
|
|
253
267
|
if (provider === "gemini") {
|
|
254
268
|
return DEFAULT_DIMENSIONS;
|
|
255
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
|
+
}
|
|
256
279
|
if (provider === "cloudflare") {
|
|
257
280
|
if (dimensions !== void 0 && dimensions !== CLOUDFLARE_DIMENSIONS) {
|
|
258
281
|
throw providerError(
|
|
@@ -360,6 +383,52 @@ class GeminiEmbeddingProvider {
|
|
|
360
383
|
);
|
|
361
384
|
}
|
|
362
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
|
+
}
|
|
363
432
|
class OpenAIEmbeddingProvider {
|
|
364
433
|
metadata;
|
|
365
434
|
#apiKey;
|
|
@@ -379,42 +448,15 @@ class OpenAIEmbeddingProvider {
|
|
|
379
448
|
"openai",
|
|
380
449
|
"API request",
|
|
381
450
|
this.#timeoutMs,
|
|
382
|
-
async (signal) => {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
input: texts,
|
|
392
|
-
model: this.metadata.model,
|
|
393
|
-
dimensions: this.metadata.dimensions
|
|
394
|
-
})
|
|
395
|
-
});
|
|
396
|
-
if (!response.ok) {
|
|
397
|
-
const requestId = getResponseHeader(response, "x-request-id");
|
|
398
|
-
const context = requestId ? ` status ${response.status}, request ${requestId}` : ` status ${response.status}`;
|
|
399
|
-
throw new Error(context);
|
|
400
|
-
}
|
|
401
|
-
const data = await response.json();
|
|
402
|
-
if (!Array.isArray(data.data)) {
|
|
403
|
-
throw new Error("OpenAI response did not include a data array");
|
|
404
|
-
}
|
|
405
|
-
return data.data.map((item) => {
|
|
406
|
-
const typed = item;
|
|
407
|
-
const embedding = typed.embedding;
|
|
408
|
-
if (!Array.isArray(embedding)) {
|
|
409
|
-
throw new Error("OpenAI response item did not include an embedding array");
|
|
410
|
-
}
|
|
411
|
-
const parsed = { embedding };
|
|
412
|
-
if (hasOwnProperty(typed, "index")) {
|
|
413
|
-
parsed.index = typed.index;
|
|
414
|
-
}
|
|
415
|
-
return parsed;
|
|
416
|
-
});
|
|
417
|
-
},
|
|
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
|
+
}),
|
|
418
460
|
options.signal,
|
|
419
461
|
[this.#apiKey]
|
|
420
462
|
);
|
|
@@ -425,6 +467,45 @@ class OpenAIEmbeddingProvider {
|
|
|
425
467
|
);
|
|
426
468
|
}
|
|
427
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
|
+
}
|
|
428
509
|
class CloudflareEmbeddingProvider {
|
|
429
510
|
metadata;
|
|
430
511
|
#accountId;
|
|
@@ -513,6 +594,15 @@ function createEmbeddingProvider(options = {}) {
|
|
|
513
594
|
}
|
|
514
595
|
return new OpenAIEmbeddingProvider(metadata, key, timeoutMs);
|
|
515
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
|
+
}
|
|
516
606
|
case "cloudflare": {
|
|
517
607
|
const accountId = getOptionalTrimmedCredential(
|
|
518
608
|
options.accountId ?? getEnvironmentVariable("CLOUDFLARE_ACCOUNT_ID")
|
package/docs/API.md
CHANGED
|
@@ -210,8 +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 Cloudflare, Gemini, or OpenAI client created with different
|
|
214
|
-
configuration.
|
|
213
|
+
a Cloudflare, Mistral, Gemini, or OpenAI client created with different
|
|
214
|
+
credentials or configuration.
|
|
215
215
|
|
|
216
216
|
### `getEmbeddingProviderMetadata(options?)`
|
|
217
217
|
|
|
@@ -239,7 +239,7 @@ Provider clients return:
|
|
|
239
239
|
```ts
|
|
240
240
|
interface EmbeddingBatchResult {
|
|
241
241
|
embeddings: number[][];
|
|
242
|
-
provider: "local" | "cloudflare" | "gemini" | "openai";
|
|
242
|
+
provider: "local" | "cloudflare" | "mistral" | "gemini" | "openai";
|
|
243
243
|
model: string;
|
|
244
244
|
dimensions: number;
|
|
245
245
|
intent: "document" | "query";
|
|
@@ -255,7 +255,7 @@ cardinality, dimensions, finite numeric values, and indexed batch ordering.
|
|
|
255
255
|
|
|
256
256
|
```ts
|
|
257
257
|
interface EmbeddingOptions {
|
|
258
|
-
provider?: "local" | "cloudflare" | "gemini" | "openai";
|
|
258
|
+
provider?: "local" | "cloudflare" | "mistral" | "gemini" | "openai";
|
|
259
259
|
apiKey?: string;
|
|
260
260
|
accountId?: string;
|
|
261
261
|
apiToken?: string;
|
|
@@ -267,9 +267,10 @@ interface EmbeddingOptions {
|
|
|
267
267
|
}
|
|
268
268
|
```
|
|
269
269
|
|
|
270
|
-
`apiKey` is used by Gemini and OpenAI.
|
|
271
|
-
`
|
|
272
|
-
`
|
|
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`.
|
|
273
274
|
|
|
274
275
|
### `padEmbedding(embedding, targetDimensions)`
|
|
275
276
|
|
package/docs/INTEGRATIONS.md
CHANGED
|
@@ -128,8 +128,17 @@ const client = createClient({
|
|
|
128
128
|
});
|
|
129
129
|
|
|
130
130
|
const embeddingProvider =
|
|
131
|
-
process.env.EMBEDDING_PROVIDER as
|
|
132
|
-
|
|
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;
|
|
133
142
|
|
|
134
143
|
await createTable(client, "articles", embeddingDimensions);
|
|
135
144
|
|
package/docs/PROVIDERS.md
CHANGED
|
@@ -1,9 +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
6
|
- `cloudflare`
|
|
7
|
+
- `mistral`
|
|
7
8
|
- `gemini`
|
|
8
9
|
- `openai`
|
|
9
10
|
|
|
@@ -15,7 +16,7 @@ query time.
|
|
|
15
16
|
|
|
16
17
|
```ts
|
|
17
18
|
interface EmbeddingOptions {
|
|
18
|
-
provider?: "local" | "cloudflare" | "gemini" | "openai";
|
|
19
|
+
provider?: "local" | "cloudflare" | "mistral" | "gemini" | "openai";
|
|
19
20
|
apiKey?: string;
|
|
20
21
|
accountId?: string;
|
|
21
22
|
apiToken?: string;
|
|
@@ -33,8 +34,8 @@ interface EmbeddingOptions {
|
|
|
33
34
|
- `intent` can be `"document"` or `"query"`; indexing defaults to
|
|
34
35
|
`"document"` and search defaults to `"query"` unless explicitly set
|
|
35
36
|
- `timeoutMs` defaults to `30000`
|
|
36
|
-
- `apiKey` is used by Gemini and OpenAI and falls back to
|
|
37
|
-
`OPENAI_API_KEY`
|
|
37
|
+
- `apiKey` is used by Mistral, Gemini, and OpenAI and falls back to
|
|
38
|
+
`MISTRAL_API_KEY`, `GEMINI_API_KEY`, or `OPENAI_API_KEY`
|
|
38
39
|
- `accountId` and `apiToken` are used by Cloudflare and fall back to
|
|
39
40
|
`CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`
|
|
40
41
|
|
|
@@ -44,7 +45,7 @@ Each provider exposes immutable metadata:
|
|
|
44
45
|
|
|
45
46
|
```ts
|
|
46
47
|
interface EmbeddingProviderMetadata {
|
|
47
|
-
name: "local" | "cloudflare" | "gemini" | "openai";
|
|
48
|
+
name: "local" | "cloudflare" | "mistral" | "gemini" | "openai";
|
|
48
49
|
model: string;
|
|
49
50
|
dimensions: number;
|
|
50
51
|
batch: {
|
|
@@ -83,9 +84,9 @@ Lower-level provider clients return an `EmbeddingBatchResult` with the validated
|
|
|
83
84
|
vectors plus provider, model, dimensions, and intent. The compatibility helpers
|
|
84
85
|
`generateEmbedding()` and `generateEmbeddings()` return only arrays.
|
|
85
86
|
|
|
86
|
-
Cloudflare, Gemini, and OpenAI clients are scoped to their current
|
|
87
|
-
are not cached globally across different credentials or
|
|
88
|
-
local Xenova 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.
|
|
89
90
|
|
|
90
91
|
Hosted provider failures are reported with bounded provider/status/request-id
|
|
91
92
|
context and without raw upstream bodies, credentials, Authorization headers, or
|
|
@@ -144,6 +145,33 @@ Behavior:
|
|
|
144
145
|
- Cloudflare does not accept custom dimensions in this provider; use
|
|
145
146
|
`createTable(client, "articles", 1024)` for Cloudflare-backed indexes
|
|
146
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
|
|
174
|
+
|
|
147
175
|
## Gemini
|
|
148
176
|
|
|
149
177
|
Provider value: `gemini`
|
|
@@ -193,6 +221,7 @@ Behavior:
|
|
|
193
221
|
|
|
194
222
|
- `local` defaults to `768`
|
|
195
223
|
- `cloudflare` is fixed at `1024`
|
|
224
|
+
- `mistral` is fixed at `1024`
|
|
196
225
|
- local embeddings are padded from 384 to your target size
|
|
197
226
|
- Gemini stays at 768
|
|
198
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
|