libsql-search 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,26 +5,13 @@
5
5
  [![CI](https://github.com/llbbl/libsql-search/actions/workflows/ci.yml/badge.svg)](https://github.com/llbbl/libsql-search/actions/workflows/ci.yml)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
- `libsql-search` adds semantic search to Markdown-backed sites using libSQL/Turso.
9
- It indexes frontmatter and content from files on disk, stores vectors in your
10
- database, and lets you query by meaning instead of exact keywords.
8
+ `libsql-search` adds semantic search to Markdown-backed sites with a small TypeScript API. It indexes frontmatter and content from files on disk, stores vectors in libSQL/Turso, and lets you query by meaning instead of exact keywords.
11
9
 
12
10
  Use it when you want:
13
11
 
14
- - a small TypeScript library instead of a hosted search product
15
- - one search index shared across static-site builds and app routes
16
- - local or hosted embeddings behind the same indexing/search API
17
- - direct control over table names, dimensions, content shape, and deployment
18
-
19
- ## What It Supports
20
-
21
- - Markdown indexing from local directories with frontmatter via `gray-matter`
22
- - libSQL/Turso storage and vector search
23
- - Embedding providers: local Hugging Face `Xenova/all-MiniLM-L6-v2`,
24
- Cloudflare Workers AI `@cf/baai/bge-m3`, Mistral `mistral-embed`, Google Gemini
25
- `gemini-embedding-2`, and OpenAI `text-embedding-3-small` /
26
- `text-embedding-3-large`
27
- - npm distribution plus JSR publishing
12
+ - one indexing/search API across local and hosted embedding providers
13
+ - direct control over vector dimensions, table names, and deployment shape
14
+ - a lightweight library instead of a hosted search product
28
15
 
29
16
  ## Install
30
17
 
@@ -43,18 +30,10 @@ deno add jsr:@logan/libsql-search npm:@libsql/client
43
30
  ```
44
31
 
45
32
  For npm usage, the package requires Node `>=22.12.0`.
46
- Node examples in this README import from `libsql-search` and `@libsql/client`.
47
- In Deno, after `deno add`, import from `@logan/libsql-search` and
48
- `@libsql/client`.
49
33
 
50
34
  ## Quick Start
51
35
 
52
- The shortest working flow is:
53
-
54
- 1. create a libSQL client
55
- 2. create the search table
56
- 3. index a Markdown directory
57
- 4. query it with the same embedding provider and dimensions
36
+ This example uses the default local provider. Local embeddings run in-process after the initial model download and cache warmup; they are not automatically air-gapped.
58
37
 
59
38
  ```ts
60
39
  import { createClient } from "@libsql/client";
@@ -65,11 +44,12 @@ const client = createClient({
65
44
  authToken: "your-auth-token",
66
45
  });
67
46
 
68
- await createTable(client);
47
+ await createTable(client, "articles_local_384", 384);
69
48
 
70
49
  await indexContent({
71
50
  client,
72
51
  contentPath: "./content",
52
+ tableName: "articles_local_384",
73
53
  embeddingOptions: {
74
54
  provider: "local",
75
55
  },
@@ -78,6 +58,7 @@ await indexContent({
78
58
  const results = await search({
79
59
  client,
80
60
  query: "how do I deploy my docs site",
61
+ tableName: "articles_local_384",
81
62
  limit: 5,
82
63
  embeddingOptions: {
83
64
  provider: "local",
@@ -94,33 +75,31 @@ console.log(results.map((result) => ({
94
75
  Important behavior:
95
76
 
96
77
  - Call `createTable()` before indexing or searching.
97
- - Keep dimensions aligned across table creation, indexing, and search queries.
98
- - `indexContent()` clears existing rows before rebuilding the index.
99
- - `local` is the default offline provider and uses 384 dimensions. Cloudflare is
100
- the recommended hosted option. Cloudflare and Mistral use 1024 dimensions.
101
- Gemini defaults to 3072 dimensions and supports 128-3072.
102
-
103
- ## Core API
104
-
105
- - `createTable(client, tableName?, dimensions?)`
106
- - `indexContent(options)`
107
- - `search(options)`
108
- - `getAllArticles(client, tableName?)`
109
- - `getArticleBySlug(client, slug, tableName?)`
110
- - `getArticlesByFolder(client, folder, tableName?)`
111
- - `getFolders(client, tableName?)`
112
- - `generateEmbedding(text, options?)`
113
- - `prepareTextForEmbedding(fields)`
78
+ - Keep table width, provider, and dimensions aligned across create/index/query.
79
+ - `indexContent()` clears existing rows before rebuilding and is not transactional.
80
+ - Hosted providers send indexed and queried text to external services and may incur provider charges.
81
+
82
+ ## Providers
83
+
84
+ Built-in providers:
85
+
86
+ - `local` with `Xenova/all-MiniLM-L6-v2` at 384 dimensions
87
+ - `cloudflare` with `@cf/baai/bge-m3` at 1024 dimensions
88
+ - `mistral` with `mistral-embed` at 1024 dimensions
89
+ - `gemini` with `gemini-embedding-2` at 128-3072 dimensions, default 3072
90
+ - `openai` with `text-embedding-3-small` or `text-embedding-3-large`, default 768
91
+ - `openai-compatible` for trusted OpenAI-compatible endpoints such as TEI
114
92
 
115
93
  ## Docs
116
94
 
117
- - [Docs index](./docs/README.md)
118
- - [Provider guide](./docs/PROVIDERS.md)
95
+ - [Documentation index](./docs/README.md)
96
+ - [Provider selection and configuration](./docs/PROVIDERS.md)
119
97
  - [API reference](./docs/API.md)
120
98
  - [Integration examples](./docs/INTEGRATIONS.md)
99
+ - [Migration and reindexing guide](./docs/MIGRATIONS.md)
100
+ - [Testing guidance](./docs/TESTING.md)
121
101
  - [Indexing and operations](./docs/INDEXING.md)
122
102
  - [Troubleshooting](./docs/TROUBLESHOOTING.md)
123
- - [Release workflow](./docs/RELEASING.md)
124
103
 
125
104
  ## License
126
105
 
package/dist/index.cjs CHANGED
@@ -5,6 +5,7 @@ var path = require('path');
5
5
  var matter = require('gray-matter');
6
6
 
7
7
  const OPENAI_DEFAULT_DIMENSIONS = 768;
8
+ const OPENAI_COMPATIBLE_DEFAULT_BATCH_SIZE = 32;
8
9
  const LOCAL_DIMENSIONS = 384;
9
10
  const DEFAULT_MAX_LENGTH = 8e3;
10
11
  const DEFAULT_TIMEOUT_MS = 3e4;
@@ -111,6 +112,7 @@ function resolveProviderName(provider) {
111
112
  case "openai":
112
113
  case "mistral":
113
114
  case "cloudflare":
115
+ case "openai-compatible":
114
116
  return provider ?? "local";
115
117
  default:
116
118
  throw new Error(`Unknown embedding provider: ${String(provider)}`);
@@ -135,6 +137,13 @@ function getOptionalTrimmedCredential(value) {
135
137
  const trimmed = value?.trim();
136
138
  return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
137
139
  }
140
+ function getRequiredTrimmedString(value, optionName) {
141
+ const trimmed = getOptionalTrimmedCredential(value);
142
+ if (!trimmed) {
143
+ throw new Error(`Invalid ${optionName}: expected a non-empty string`);
144
+ }
145
+ return trimmed;
146
+ }
138
147
  function hasOwnProperty(value, property) {
139
148
  return Object.prototype.hasOwnProperty.call(value, property);
140
149
  }
@@ -295,7 +304,27 @@ async function embedSequentially(provider, operation, texts, signal, embedOne) {
295
304
  }
296
305
  return results;
297
306
  }
298
- function createProviderMetadata(provider, dimensions) {
307
+ function normalizeOpenAICompatibleEmbeddingsUrl(baseUrl) {
308
+ let url;
309
+ try {
310
+ url = new URL(baseUrl);
311
+ } catch {
312
+ throw new Error("Invalid baseUrl: expected an absolute http or https URL");
313
+ }
314
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
315
+ throw new Error("Invalid baseUrl: expected an absolute http or https URL");
316
+ }
317
+ if (url.username || url.password) {
318
+ throw new Error("Invalid baseUrl: URL credentials are not allowed");
319
+ }
320
+ if (url.search || url.hash) {
321
+ throw new Error("Invalid baseUrl: query strings and fragments are not allowed");
322
+ }
323
+ const path = url.pathname.replace(/\/+$/, "");
324
+ url.pathname = path.endsWith("/embeddings") ? path : `${path}/embeddings`;
325
+ return url.toString();
326
+ }
327
+ function createProviderMetadata(provider, dimensions, model) {
299
328
  switch (provider) {
300
329
  case "local":
301
330
  return Object.freeze({
@@ -332,6 +361,13 @@ function createProviderMetadata(provider, dimensions) {
332
361
  dimensions: CLOUDFLARE_DIMENSIONS,
333
362
  batch: Object.freeze({ mode: "native" })
334
363
  });
364
+ case "openai-compatible":
365
+ return Object.freeze({
366
+ name: "openai-compatible",
367
+ model: getRequiredTrimmedString(model, "model"),
368
+ dimensions,
369
+ batch: Object.freeze({ mode: "native" })
370
+ });
335
371
  }
336
372
  }
337
373
  function getEffectiveDimensions(provider, dimensions) {
@@ -372,6 +408,12 @@ function getEffectiveDimensions(provider, dimensions) {
372
408
  }
373
409
  return CLOUDFLARE_DIMENSIONS;
374
410
  }
411
+ if (provider === "openai-compatible") {
412
+ if (dimensions === void 0) {
413
+ throw new Error("Invalid dimensions: expected a positive integer");
414
+ }
415
+ return getPositiveInteger(dimensions, "dimensions");
416
+ }
375
417
  return getPositiveInteger(dimensions ?? OPENAI_DEFAULT_DIMENSIONS, "dimensions");
376
418
  }
377
419
  function assertBatchSize(metadata, count) {
@@ -382,6 +424,13 @@ function assertBatchSize(metadata, count) {
382
424
  );
383
425
  }
384
426
  }
427
+ function chunkTexts(texts, batchSize) {
428
+ const chunks = [];
429
+ for (let index = 0; index < texts.length; index += batchSize) {
430
+ chunks.push(texts.slice(index, index + batchSize));
431
+ }
432
+ return chunks;
433
+ }
385
434
  function createEmbeddingBatchResult(metadata, intent, embeddings) {
386
435
  return Object.freeze({
387
436
  embeddings,
@@ -482,18 +531,23 @@ async function requestOpenAICompatibleEmbeddings(request) {
482
531
  if (request.encodingFormat !== void 0) {
483
532
  body.encoding_format = request.encodingFormat;
484
533
  }
534
+ const headers = {
535
+ "Content-Type": "application/json"
536
+ };
537
+ if (request.apiKey !== void 0) {
538
+ headers.Authorization = `Bearer ${request.apiKey}`;
539
+ }
485
540
  const response = await fetch(request.endpoint, {
486
541
  method: "POST",
487
- headers: {
488
- "Content-Type": "application/json",
489
- "Authorization": `Bearer ${request.apiKey}`
490
- },
542
+ headers,
491
543
  signal: request.signal,
492
- body: JSON.stringify(body)
544
+ body: JSON.stringify(body),
545
+ ...request.redirect ? { redirect: request.redirect } : {}
493
546
  });
494
547
  if (!response.ok) {
495
- const requestId = getSafeResponseHeader(response, "x-request-id");
496
- const context = requestId ? ` status ${response.status}, request ${requestId}` : ` status ${response.status}`;
548
+ const statusHeader = request.statusHeader ?? { name: "x-request-id", label: "request" };
549
+ const statusHeaderValue = getSafeResponseHeader(response, statusHeader.name);
550
+ const context = statusHeaderValue ? ` status ${response.status}, ${statusHeader.label} ${statusHeaderValue}` : ` status ${response.status}`;
497
551
  throw new Error(context);
498
552
  }
499
553
  const data = await response.json();
@@ -517,13 +571,13 @@ async function requestOpenAICompatibleEmbeddings(request) {
517
571
  return parsed;
518
572
  });
519
573
  }
520
- class OpenAIEmbeddingProvider {
574
+ class OpenAICompatibleEmbeddingProvider {
521
575
  metadata;
522
- #apiKey;
523
576
  #timeoutMs;
524
- constructor(metadata, apiKey, timeoutMs) {
577
+ #profile;
578
+ constructor(metadata, profile, timeoutMs) {
525
579
  this.metadata = metadata;
526
- this.#apiKey = apiKey;
580
+ this.#profile = profile;
527
581
  this.#timeoutMs = timeoutMs;
528
582
  }
529
583
  async embed(texts, options = {}) {
@@ -533,131 +587,44 @@ class OpenAIEmbeddingProvider {
533
587
  }
534
588
  assertBatchSize(this.metadata, texts.length);
535
589
  const items = await withTimeout(
536
- "openai",
537
- "API request",
538
- this.#timeoutMs,
539
- async (signal) => requestOpenAICompatibleEmbeddings({
540
- responseLabel: "OpenAI",
541
- endpoint: OPENAI_EMBEDDINGS_URL,
542
- apiKey: this.#apiKey,
543
- model: this.metadata.model,
544
- texts,
545
- signal,
546
- dimensions: this.metadata.dimensions
547
- }),
548
- options.signal,
549
- [this.#apiKey]
550
- );
551
- return createEmbeddingBatchResult(
552
- this.metadata,
553
- intent,
554
- validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "openai")
555
- );
556
- }
557
- }
558
- class MistralEmbeddingProvider {
559
- metadata;
560
- #apiKey;
561
- #timeoutMs;
562
- constructor(metadata, apiKey, timeoutMs) {
563
- this.metadata = metadata;
564
- this.#apiKey = apiKey;
565
- this.#timeoutMs = timeoutMs;
566
- }
567
- async embed(texts, options = {}) {
568
- const intent = resolveIntent(options.intent);
569
- if (texts.length === 0) {
570
- return createEmbeddingBatchResult(this.metadata, intent, []);
571
- }
572
- assertBatchSize(this.metadata, texts.length);
573
- const items = await withTimeout(
574
- "mistral",
575
- "API request",
576
- this.#timeoutMs,
577
- async (signal) => requestOpenAICompatibleEmbeddings({
578
- responseLabel: "Mistral",
579
- endpoint: MISTRAL_EMBEDDINGS_URL,
580
- apiKey: this.#apiKey,
581
- model: this.metadata.model,
582
- texts,
583
- signal,
584
- encodingFormat: "float",
585
- requireIndex: true
586
- }),
587
- options.signal,
588
- [this.#apiKey]
589
- );
590
- return createEmbeddingBatchResult(
591
- this.metadata,
592
- intent,
593
- validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "mistral")
594
- );
595
- }
596
- }
597
- class CloudflareEmbeddingProvider {
598
- metadata;
599
- #accountId;
600
- #apiToken;
601
- #timeoutMs;
602
- constructor(metadata, accountId, apiToken, timeoutMs) {
603
- this.metadata = metadata;
604
- this.#accountId = accountId;
605
- this.#apiToken = apiToken;
606
- this.#timeoutMs = timeoutMs;
607
- }
608
- async embed(texts, options = {}) {
609
- const intent = resolveIntent(options.intent);
610
- if (texts.length === 0) {
611
- return createEmbeddingBatchResult(this.metadata, intent, []);
612
- }
613
- assertBatchSize(this.metadata, texts.length);
614
- const endpoint = createCloudflareEmbeddingsUrl(this.#accountId);
615
- const items = await withTimeout(
616
- "cloudflare",
590
+ this.#profile.provider,
617
591
  "API request",
618
592
  this.#timeoutMs,
619
593
  async (signal) => {
620
- const response = await fetch(endpoint, {
621
- method: "POST",
622
- headers: {
623
- "Content-Type": "application/json",
624
- "Authorization": `Bearer ${this.#apiToken}`
625
- },
626
- signal,
627
- body: JSON.stringify({
628
- model: this.metadata.model,
629
- input: texts
630
- })
631
- });
632
- if (!response.ok) {
633
- const cfRay = getSafeResponseHeader(response, "cf-ray");
634
- const context = cfRay ? ` status ${response.status}, cf-ray ${cfRay}` : ` status ${response.status}`;
635
- throw new Error(context);
636
- }
637
- const data = await response.json();
638
- if (!Array.isArray(data.data)) {
639
- throw new Error("Cloudflare response did not include a data array");
594
+ const batches = this.#profile.batchSize === void 0 ? [texts] : chunkTexts(texts, this.#profile.batchSize);
595
+ const results = [];
596
+ for (const batch of batches) {
597
+ throwIfAborted(this.#profile.provider, "API request", signal);
598
+ const batchItems = await requestOpenAICompatibleEmbeddings({
599
+ responseLabel: this.#profile.responseLabel,
600
+ endpoint: this.#profile.endpoint,
601
+ ...this.#profile.apiKey !== void 0 ? { apiKey: this.#profile.apiKey } : {},
602
+ model: this.#profile.model,
603
+ texts: batch,
604
+ signal,
605
+ ...this.#profile.includeDimensions ? { dimensions: this.#profile.dimensions } : {},
606
+ ...this.#profile.encodingFormat ? { encodingFormat: this.#profile.encodingFormat } : {},
607
+ ...this.#profile.requireIndex !== void 0 ? { requireIndex: this.#profile.requireIndex } : {},
608
+ ...this.#profile.redirect ? { redirect: this.#profile.redirect } : {},
609
+ ...this.#profile.statusHeader ? { statusHeader: this.#profile.statusHeader } : {}
610
+ });
611
+ results.push(...validateEmbeddingBatch(
612
+ batchItems,
613
+ batch.length,
614
+ this.metadata.dimensions,
615
+ this.#profile.provider
616
+ ));
617
+ throwIfAborted(this.#profile.provider, "API request", signal);
640
618
  }
641
- return data.data.map((item) => {
642
- const typed = item;
643
- const embedding = typed.embedding;
644
- if (!Array.isArray(embedding)) {
645
- throw new Error("Cloudflare response item did not include an embedding array");
646
- }
647
- const parsed = { embedding };
648
- if (hasOwnProperty(typed, "index")) {
649
- parsed.index = typed.index;
650
- }
651
- return parsed;
652
- });
619
+ return results;
653
620
  },
654
621
  options.signal,
655
- [this.#apiToken, this.#accountId, encodeURIComponent(this.#accountId)]
622
+ this.#profile.exactSecrets ?? []
656
623
  );
657
624
  return createEmbeddingBatchResult(
658
625
  this.metadata,
659
626
  intent,
660
- validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "cloudflare")
627
+ validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, this.#profile.provider)
661
628
  );
662
629
  }
663
630
  }
@@ -682,7 +649,16 @@ function createEmbeddingProvider(options = {}) {
682
649
  if (!key) {
683
650
  throw new Error("OPENAI_API_KEY is required for OpenAI embeddings");
684
651
  }
685
- return new OpenAIEmbeddingProvider(metadata, key, timeoutMs);
652
+ return new OpenAICompatibleEmbeddingProvider(metadata, {
653
+ provider: "openai",
654
+ responseLabel: "OpenAI",
655
+ endpoint: OPENAI_EMBEDDINGS_URL,
656
+ apiKey: key,
657
+ model: metadata.model,
658
+ dimensions: metadata.dimensions,
659
+ includeDimensions: true,
660
+ exactSecrets: [key]
661
+ }, timeoutMs);
686
662
  }
687
663
  case "mistral": {
688
664
  const key = getOptionalTrimmedCredential(
@@ -691,7 +667,17 @@ function createEmbeddingProvider(options = {}) {
691
667
  if (!key) {
692
668
  throw new Error("MISTRAL_API_KEY is required for Mistral embeddings");
693
669
  }
694
- return new MistralEmbeddingProvider(metadata, key, timeoutMs);
670
+ return new OpenAICompatibleEmbeddingProvider(metadata, {
671
+ provider: "mistral",
672
+ responseLabel: "Mistral",
673
+ endpoint: MISTRAL_EMBEDDINGS_URL,
674
+ apiKey: key,
675
+ model: metadata.model,
676
+ dimensions: metadata.dimensions,
677
+ encodingFormat: "float",
678
+ requireIndex: true,
679
+ exactSecrets: [key]
680
+ }, timeoutMs);
695
681
  }
696
682
  case "cloudflare": {
697
683
  const accountId = getOptionalTrimmedCredential(
@@ -706,13 +692,56 @@ function createEmbeddingProvider(options = {}) {
706
692
  if (!apiToken) {
707
693
  throw new Error("CLOUDFLARE_API_TOKEN is required for Cloudflare embeddings");
708
694
  }
709
- return new CloudflareEmbeddingProvider(metadata, accountId, apiToken, timeoutMs);
695
+ const endpoint = createCloudflareEmbeddingsUrl(accountId);
696
+ return new OpenAICompatibleEmbeddingProvider(metadata, {
697
+ provider: "cloudflare",
698
+ responseLabel: "Cloudflare",
699
+ endpoint,
700
+ apiKey: apiToken,
701
+ model: metadata.model,
702
+ dimensions: metadata.dimensions,
703
+ statusHeader: { name: "cf-ray", label: "cf-ray" },
704
+ exactSecrets: [apiToken, accountId, encodeURIComponent(accountId), endpoint]
705
+ }, timeoutMs);
706
+ }
707
+ case "openai-compatible": {
708
+ const baseUrl = getRequiredTrimmedString(options.baseUrl, "baseUrl");
709
+ const endpoint = normalizeOpenAICompatibleEmbeddingsUrl(baseUrl);
710
+ const apiKey = getOptionalTrimmedCredential(options.apiKey);
711
+ const batchSize = getPositiveInteger(
712
+ options.batchSize ?? OPENAI_COMPATIBLE_DEFAULT_BATCH_SIZE,
713
+ "batchSize"
714
+ );
715
+ return new OpenAICompatibleEmbeddingProvider(metadata, {
716
+ provider: "openai-compatible",
717
+ responseLabel: "OpenAI-compatible",
718
+ endpoint,
719
+ ...apiKey ? { apiKey } : {},
720
+ model: metadata.model,
721
+ dimensions: metadata.dimensions,
722
+ includeDimensions: true,
723
+ encodingFormat: "float",
724
+ requireIndex: true,
725
+ redirect: "error",
726
+ batchSize,
727
+ exactSecrets: [
728
+ ...apiKey ? [apiKey] : [],
729
+ baseUrl,
730
+ endpoint
731
+ ]
732
+ }, timeoutMs);
710
733
  }
711
734
  }
712
735
  }
713
736
  function getEmbeddingProviderMetadata(options = {}) {
714
737
  const provider = resolveProviderName(options.provider);
715
- return createProviderMetadata(provider, getEffectiveDimensions(provider, options.dimensions));
738
+ if (provider === "openai-compatible") {
739
+ normalizeOpenAICompatibleEmbeddingsUrl(getRequiredTrimmedString(options.baseUrl, "baseUrl"));
740
+ if (options.batchSize !== void 0) {
741
+ getPositiveInteger(options.batchSize, "batchSize");
742
+ }
743
+ }
744
+ return createProviderMetadata(provider, getEffectiveDimensions(provider, options.dimensions), options.model);
716
745
  }
717
746
  async function generateEmbeddings(texts, options = {}) {
718
747
  const maxLength = getPositiveInteger(options.maxLength ?? DEFAULT_MAX_LENGTH, "maxLength");
package/dist/index.d.ts CHANGED
@@ -2,9 +2,10 @@ import { Client } from '@libsql/client';
2
2
 
3
3
  /**
4
4
  * Multi-provider embedding generation
5
- * Supports local Hugging Face Transformers, Gemini, OpenAI, Mistral, and Cloudflare Workers AI
5
+ * Supports local Hugging Face Transformers, Gemini, OpenAI, Mistral,
6
+ * Cloudflare Workers AI, and custom OpenAI-compatible endpoints
6
7
  */
7
- type EmbeddingProvider = 'local' | 'gemini' | 'openai' | 'mistral' | 'cloudflare';
8
+ type EmbeddingProvider = 'local' | 'gemini' | 'openai' | 'mistral' | 'cloudflare' | 'openai-compatible';
8
9
  type EmbeddingIntent = 'document' | 'query';
9
10
  type EmbeddingBatchMode = 'native' | 'sequential';
10
11
  interface EmbeddingBatchBehavior {
@@ -37,6 +38,9 @@ interface EmbeddingOptions {
37
38
  apiKey?: string;
38
39
  accountId?: string;
39
40
  apiToken?: string;
41
+ baseUrl?: string;
42
+ model?: string;
43
+ batchSize?: number;
40
44
  dimensions?: number;
41
45
  maxLength?: number;
42
46
  intent?: EmbeddingIntent;