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 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`, Google Gemini `text-embedding-004`, and OpenAI
25
- `text-embedding-3-small` / `text-embedding-3-large`
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 uses 1024 dimensions.
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
- const response = await fetch(OPENAI_EMBEDDINGS_URL, {
386
- method: "POST",
387
- headers: {
388
- "Content-Type": "application/json",
389
- "Authorization": `Bearer ${this.#apiKey}`
390
- },
391
- signal,
392
- body: JSON.stringify({
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
- const response = await fetch(OPENAI_EMBEDDINGS_URL, {
384
- method: "POST",
385
- headers: {
386
- "Content-Type": "application/json",
387
- "Authorization": `Bearer ${this.#apiKey}`
388
- },
389
- signal,
390
- body: JSON.stringify({
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 credentials or
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. Cloudflare uses `accountId` and
271
- `apiToken`, which fall back to `CLOUDFLARE_ACCOUNT_ID` and
272
- `CLOUDFLARE_API_TOKEN`.
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
 
@@ -128,8 +128,17 @@ const client = createClient({
128
128
  });
129
129
 
130
130
  const embeddingProvider =
131
- process.env.EMBEDDING_PROVIDER as "local" | "cloudflare" | "gemini" | "openai" | undefined;
132
- const embeddingDimensions = embeddingProvider === "cloudflare" ? 1024 : 768;
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 four embedding providers:
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 `GEMINI_API_KEY` or
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 options. They
87
- are not cached globally across different credentials or configurations. The
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 OpenAI embedding options,
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,
@@ -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 `GEMINI_API_KEY` or `OPENAI_API_KEY` available
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libsql-search",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Semantic search for static sites using libSQL/Turso with multi-provider embeddings",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.34.5",