libsql-search 0.2.4 → 0.3.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
@@ -13,16 +13,16 @@ 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 API-based embeddings behind the same indexing/search API
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 that exist in the code today: local
24
- `Xenova/all-MiniLM-L6-v2`, Google Gemini `text-embedding-004`, and OpenAI
25
- `text-embedding-3-small` and `text-embedding-3-large`
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`
26
26
  - npm distribution plus JSR publishing
27
27
 
28
28
  ## Install
@@ -97,6 +97,8 @@ Important behavior:
97
97
  - Call `createTable()` before indexing or searching.
98
98
  - Keep dimensions aligned across table creation, indexing, and search queries.
99
99
  - `indexContent()` clears existing rows before rebuilding the index.
100
+ - `local` is the default offline provider; Cloudflare is the recommended hosted
101
+ option and uses 1024 dimensions.
100
102
 
101
103
  ## Core API
102
104
 
package/dist/index.cjs CHANGED
@@ -12,6 +12,8 @@ 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 CLOUDFLARE_MODEL = "@cf/baai/bge-m3";
16
+ const CLOUDFLARE_DIMENSIONS = 1024;
15
17
  const localModelCacheByModel = /* @__PURE__ */ new Map();
16
18
  function getEnvironmentVariable(name) {
17
19
  const runtime = globalThis;
@@ -51,6 +53,7 @@ function resolveProviderName(provider) {
51
53
  case "local":
52
54
  case "gemini":
53
55
  case "openai":
56
+ case "cloudflare":
54
57
  return provider ?? "local";
55
58
  default:
56
59
  throw new Error(`Unknown embedding provider: ${String(provider)}`);
@@ -71,6 +74,10 @@ function truncateTexts(texts, maxLength) {
71
74
  function getOpenAIModel(dimensions) {
72
75
  return dimensions <= 1536 ? OPENAI_SMALL_MODEL : OPENAI_LARGE_MODEL;
73
76
  }
77
+ function getOptionalTrimmedCredential(value) {
78
+ const trimmed = value?.trim();
79
+ return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
80
+ }
74
81
  function hasOwnProperty(value, property) {
75
82
  return Object.prototype.hasOwnProperty.call(value, property);
76
83
  }
@@ -97,6 +104,16 @@ function providerError(provider, message, cause, exactSecrets = []) {
97
104
  function getResponseHeader(response, name) {
98
105
  return response.headers.get(name) ?? void 0;
99
106
  }
107
+ function getSafeResponseHeader(response, name) {
108
+ const value = getResponseHeader(response, name)?.trim();
109
+ if (!value) {
110
+ return void 0;
111
+ }
112
+ return value.replace(/[^A-Za-z0-9._:-]/g, "").slice(0, 128);
113
+ }
114
+ function createCloudflareEmbeddingsUrl(accountId) {
115
+ return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/v1/embeddings`;
116
+ }
100
117
  async function withTimeout(provider, operation, timeoutMs, run, parentSignal, exactSecrets = []) {
101
118
  if (parentSignal?.aborted) {
102
119
  throw providerError(provider, `${operation} was aborted`);
@@ -225,12 +242,28 @@ function createProviderMetadata(provider, dimensions) {
225
242
  dimensions,
226
243
  batch: Object.freeze({ mode: "native", maxSize: 2048 })
227
244
  });
245
+ case "cloudflare":
246
+ return Object.freeze({
247
+ name: "cloudflare",
248
+ model: CLOUDFLARE_MODEL,
249
+ dimensions: CLOUDFLARE_DIMENSIONS,
250
+ batch: Object.freeze({ mode: "native" })
251
+ });
228
252
  }
229
253
  }
230
254
  function getEffectiveDimensions(provider, dimensions) {
231
255
  if (provider === "gemini") {
232
256
  return DEFAULT_DIMENSIONS;
233
257
  }
258
+ if (provider === "cloudflare") {
259
+ if (dimensions !== void 0 && dimensions !== CLOUDFLARE_DIMENSIONS) {
260
+ throw providerError(
261
+ "cloudflare",
262
+ `@cf/baai/bge-m3 returns ${CLOUDFLARE_DIMENSIONS} dimensions; received dimensions ${dimensions}`
263
+ );
264
+ }
265
+ return CLOUDFLARE_DIMENSIONS;
266
+ }
234
267
  return getPositiveInteger(dimensions ?? DEFAULT_DIMENSIONS, "dimensions");
235
268
  }
236
269
  function assertBatchSize(metadata, count) {
@@ -394,6 +427,73 @@ class OpenAIEmbeddingProvider {
394
427
  );
395
428
  }
396
429
  }
430
+ class CloudflareEmbeddingProvider {
431
+ metadata;
432
+ #accountId;
433
+ #apiToken;
434
+ #timeoutMs;
435
+ constructor(metadata, accountId, apiToken, timeoutMs) {
436
+ this.metadata = metadata;
437
+ this.#accountId = accountId;
438
+ this.#apiToken = apiToken;
439
+ this.#timeoutMs = timeoutMs;
440
+ }
441
+ async embed(texts, options = {}) {
442
+ const intent = resolveIntent(options.intent);
443
+ if (texts.length === 0) {
444
+ return createEmbeddingBatchResult(this.metadata, intent, []);
445
+ }
446
+ assertBatchSize(this.metadata, texts.length);
447
+ const endpoint = createCloudflareEmbeddingsUrl(this.#accountId);
448
+ const items = await withTimeout(
449
+ "cloudflare",
450
+ "API request",
451
+ this.#timeoutMs,
452
+ async (signal) => {
453
+ const response = await fetch(endpoint, {
454
+ method: "POST",
455
+ headers: {
456
+ "Content-Type": "application/json",
457
+ "Authorization": `Bearer ${this.#apiToken}`
458
+ },
459
+ signal,
460
+ body: JSON.stringify({
461
+ model: this.metadata.model,
462
+ input: texts
463
+ })
464
+ });
465
+ if (!response.ok) {
466
+ const cfRay = getSafeResponseHeader(response, "cf-ray");
467
+ const context = cfRay ? ` status ${response.status}, cf-ray ${cfRay}` : ` status ${response.status}`;
468
+ throw new Error(context);
469
+ }
470
+ const data = await response.json();
471
+ if (!Array.isArray(data.data)) {
472
+ throw new Error("Cloudflare response did not include a data array");
473
+ }
474
+ return data.data.map((item) => {
475
+ const typed = item;
476
+ const embedding = typed.embedding;
477
+ if (!Array.isArray(embedding)) {
478
+ throw new Error("Cloudflare response item did not include an embedding array");
479
+ }
480
+ const parsed = { embedding };
481
+ if (hasOwnProperty(typed, "index")) {
482
+ parsed.index = typed.index;
483
+ }
484
+ return parsed;
485
+ });
486
+ },
487
+ options.signal,
488
+ [this.#apiToken, this.#accountId, encodeURIComponent(this.#accountId)]
489
+ );
490
+ return createEmbeddingBatchResult(
491
+ this.metadata,
492
+ intent,
493
+ validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "cloudflare")
494
+ );
495
+ }
496
+ }
397
497
  function createEmbeddingProvider(options = {}) {
398
498
  const provider = resolveProviderName(options.provider);
399
499
  const metadata = getEmbeddingProviderMetadata(options);
@@ -415,6 +515,21 @@ function createEmbeddingProvider(options = {}) {
415
515
  }
416
516
  return new OpenAIEmbeddingProvider(metadata, key, timeoutMs);
417
517
  }
518
+ case "cloudflare": {
519
+ const accountId = getOptionalTrimmedCredential(
520
+ options.accountId ?? getEnvironmentVariable("CLOUDFLARE_ACCOUNT_ID")
521
+ );
522
+ const apiToken = getOptionalTrimmedCredential(
523
+ options.apiToken ?? getEnvironmentVariable("CLOUDFLARE_API_TOKEN")
524
+ );
525
+ if (!accountId) {
526
+ throw new Error("CLOUDFLARE_ACCOUNT_ID is required for Cloudflare embeddings");
527
+ }
528
+ if (!apiToken) {
529
+ throw new Error("CLOUDFLARE_API_TOKEN is required for Cloudflare embeddings");
530
+ }
531
+ return new CloudflareEmbeddingProvider(metadata, accountId, apiToken, timeoutMs);
532
+ }
418
533
  }
419
534
  }
420
535
  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' | '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,8 @@ 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 CLOUDFLARE_MODEL = "@cf/baai/bge-m3";
14
+ const CLOUDFLARE_DIMENSIONS = 1024;
13
15
  const localModelCacheByModel = /* @__PURE__ */ new Map();
14
16
  function getEnvironmentVariable(name) {
15
17
  const runtime = globalThis;
@@ -49,6 +51,7 @@ function resolveProviderName(provider) {
49
51
  case "local":
50
52
  case "gemini":
51
53
  case "openai":
54
+ case "cloudflare":
52
55
  return provider ?? "local";
53
56
  default:
54
57
  throw new Error(`Unknown embedding provider: ${String(provider)}`);
@@ -69,6 +72,10 @@ function truncateTexts(texts, maxLength) {
69
72
  function getOpenAIModel(dimensions) {
70
73
  return dimensions <= 1536 ? OPENAI_SMALL_MODEL : OPENAI_LARGE_MODEL;
71
74
  }
75
+ function getOptionalTrimmedCredential(value) {
76
+ const trimmed = value?.trim();
77
+ return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
78
+ }
72
79
  function hasOwnProperty(value, property) {
73
80
  return Object.prototype.hasOwnProperty.call(value, property);
74
81
  }
@@ -95,6 +102,16 @@ function providerError(provider, message, cause, exactSecrets = []) {
95
102
  function getResponseHeader(response, name) {
96
103
  return response.headers.get(name) ?? void 0;
97
104
  }
105
+ function getSafeResponseHeader(response, name) {
106
+ const value = getResponseHeader(response, name)?.trim();
107
+ if (!value) {
108
+ return void 0;
109
+ }
110
+ return value.replace(/[^A-Za-z0-9._:-]/g, "").slice(0, 128);
111
+ }
112
+ function createCloudflareEmbeddingsUrl(accountId) {
113
+ return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/v1/embeddings`;
114
+ }
98
115
  async function withTimeout(provider, operation, timeoutMs, run, parentSignal, exactSecrets = []) {
99
116
  if (parentSignal?.aborted) {
100
117
  throw providerError(provider, `${operation} was aborted`);
@@ -223,12 +240,28 @@ function createProviderMetadata(provider, dimensions) {
223
240
  dimensions,
224
241
  batch: Object.freeze({ mode: "native", maxSize: 2048 })
225
242
  });
243
+ case "cloudflare":
244
+ return Object.freeze({
245
+ name: "cloudflare",
246
+ model: CLOUDFLARE_MODEL,
247
+ dimensions: CLOUDFLARE_DIMENSIONS,
248
+ batch: Object.freeze({ mode: "native" })
249
+ });
226
250
  }
227
251
  }
228
252
  function getEffectiveDimensions(provider, dimensions) {
229
253
  if (provider === "gemini") {
230
254
  return DEFAULT_DIMENSIONS;
231
255
  }
256
+ if (provider === "cloudflare") {
257
+ if (dimensions !== void 0 && dimensions !== CLOUDFLARE_DIMENSIONS) {
258
+ throw providerError(
259
+ "cloudflare",
260
+ `@cf/baai/bge-m3 returns ${CLOUDFLARE_DIMENSIONS} dimensions; received dimensions ${dimensions}`
261
+ );
262
+ }
263
+ return CLOUDFLARE_DIMENSIONS;
264
+ }
232
265
  return getPositiveInteger(dimensions ?? DEFAULT_DIMENSIONS, "dimensions");
233
266
  }
234
267
  function assertBatchSize(metadata, count) {
@@ -392,6 +425,73 @@ class OpenAIEmbeddingProvider {
392
425
  );
393
426
  }
394
427
  }
428
+ class CloudflareEmbeddingProvider {
429
+ metadata;
430
+ #accountId;
431
+ #apiToken;
432
+ #timeoutMs;
433
+ constructor(metadata, accountId, apiToken, timeoutMs) {
434
+ this.metadata = metadata;
435
+ this.#accountId = accountId;
436
+ this.#apiToken = apiToken;
437
+ this.#timeoutMs = timeoutMs;
438
+ }
439
+ async embed(texts, options = {}) {
440
+ const intent = resolveIntent(options.intent);
441
+ if (texts.length === 0) {
442
+ return createEmbeddingBatchResult(this.metadata, intent, []);
443
+ }
444
+ assertBatchSize(this.metadata, texts.length);
445
+ const endpoint = createCloudflareEmbeddingsUrl(this.#accountId);
446
+ const items = await withTimeout(
447
+ "cloudflare",
448
+ "API request",
449
+ this.#timeoutMs,
450
+ async (signal) => {
451
+ const response = await fetch(endpoint, {
452
+ method: "POST",
453
+ headers: {
454
+ "Content-Type": "application/json",
455
+ "Authorization": `Bearer ${this.#apiToken}`
456
+ },
457
+ signal,
458
+ body: JSON.stringify({
459
+ model: this.metadata.model,
460
+ input: texts
461
+ })
462
+ });
463
+ if (!response.ok) {
464
+ const cfRay = getSafeResponseHeader(response, "cf-ray");
465
+ const context = cfRay ? ` status ${response.status}, cf-ray ${cfRay}` : ` status ${response.status}`;
466
+ throw new Error(context);
467
+ }
468
+ const data = await response.json();
469
+ if (!Array.isArray(data.data)) {
470
+ throw new Error("Cloudflare response did not include a data array");
471
+ }
472
+ return data.data.map((item) => {
473
+ const typed = item;
474
+ const embedding = typed.embedding;
475
+ if (!Array.isArray(embedding)) {
476
+ throw new Error("Cloudflare response item did not include an embedding array");
477
+ }
478
+ const parsed = { embedding };
479
+ if (hasOwnProperty(typed, "index")) {
480
+ parsed.index = typed.index;
481
+ }
482
+ return parsed;
483
+ });
484
+ },
485
+ options.signal,
486
+ [this.#apiToken, this.#accountId, encodeURIComponent(this.#accountId)]
487
+ );
488
+ return createEmbeddingBatchResult(
489
+ this.metadata,
490
+ intent,
491
+ validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "cloudflare")
492
+ );
493
+ }
494
+ }
395
495
  function createEmbeddingProvider(options = {}) {
396
496
  const provider = resolveProviderName(options.provider);
397
497
  const metadata = getEmbeddingProviderMetadata(options);
@@ -413,6 +513,21 @@ function createEmbeddingProvider(options = {}) {
413
513
  }
414
514
  return new OpenAIEmbeddingProvider(metadata, key, timeoutMs);
415
515
  }
516
+ case "cloudflare": {
517
+ const accountId = getOptionalTrimmedCredential(
518
+ options.accountId ?? getEnvironmentVariable("CLOUDFLARE_ACCOUNT_ID")
519
+ );
520
+ const apiToken = getOptionalTrimmedCredential(
521
+ options.apiToken ?? getEnvironmentVariable("CLOUDFLARE_API_TOKEN")
522
+ );
523
+ if (!accountId) {
524
+ throw new Error("CLOUDFLARE_ACCOUNT_ID is required for Cloudflare embeddings");
525
+ }
526
+ if (!apiToken) {
527
+ throw new Error("CLOUDFLARE_API_TOKEN is required for Cloudflare embeddings");
528
+ }
529
+ return new CloudflareEmbeddingProvider(metadata, accountId, apiToken, timeoutMs);
530
+ }
416
531
  }
417
532
  }
418
533
  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 a different API key or configuration.
213
+ a Cloudflare, Gemini, or OpenAI client created with different credentials or
214
+ 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" | "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" | "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,10 @@ interface EmbeddingOptions {
264
267
  }
265
268
  ```
266
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`.
273
+
267
274
  ### `padEmbedding(embedding, targetDimensions)`
268
275
 
269
276
  Pads or truncates an embedding array to the requested length.
@@ -127,14 +127,18 @@ const client = createClient({
127
127
  authToken: process.env.TURSO_AUTH_TOKEN!,
128
128
  });
129
129
 
130
- await createTable(client, "articles", 768);
130
+ const embeddingProvider =
131
+ process.env.EMBEDDING_PROVIDER as "local" | "cloudflare" | "gemini" | "openai" | undefined;
132
+ const embeddingDimensions = embeddingProvider === "cloudflare" ? 1024 : 768;
133
+
134
+ await createTable(client, "articles", embeddingDimensions);
131
135
 
132
136
  await indexContent({
133
137
  client,
134
138
  contentPath: "./content",
135
139
  embeddingOptions: {
136
- provider: process.env.EMBEDDING_PROVIDER as "local" | "gemini" | "openai" | undefined,
137
- dimensions: 768,
140
+ provider: embeddingProvider,
141
+ dimensions: embeddingDimensions,
138
142
  },
139
143
  });
140
144
  ```
package/docs/PROVIDERS.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # Embedding Providers
2
2
 
3
- `libsql-search` currently supports three embedding providers:
3
+ `libsql-search` currently supports four embedding providers:
4
4
 
5
5
  - `local`
6
+ - `cloudflare`
6
7
  - `gemini`
7
8
  - `openai`
8
9
 
@@ -14,8 +15,10 @@ query time.
14
15
 
15
16
  ```ts
16
17
  interface EmbeddingOptions {
17
- provider?: "local" | "gemini" | "openai";
18
+ provider?: "local" | "cloudflare" | "gemini" | "openai";
18
19
  apiKey?: string;
20
+ accountId?: string;
21
+ apiToken?: string;
19
22
  dimensions?: number;
20
23
  maxLength?: number;
21
24
  intent?: "document" | "query";
@@ -30,8 +33,10 @@ interface EmbeddingOptions {
30
33
  - `intent` can be `"document"` or `"query"`; indexing defaults to
31
34
  `"document"` and search defaults to `"query"` unless explicitly set
32
35
  - `timeoutMs` defaults to `30000`
33
- - `apiKey` is optional in code, but required for hosted providers unless the
34
- matching environment variable is available
36
+ - `apiKey` is used by Gemini and OpenAI and falls back to `GEMINI_API_KEY` or
37
+ `OPENAI_API_KEY`
38
+ - `accountId` and `apiToken` are used by Cloudflare and fall back to
39
+ `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`
35
40
 
36
41
  ## Provider Contract
37
42
 
@@ -39,7 +44,7 @@ Each provider exposes immutable metadata:
39
44
 
40
45
  ```ts
41
46
  interface EmbeddingProviderMetadata {
42
- name: "local" | "gemini" | "openai";
47
+ name: "local" | "cloudflare" | "gemini" | "openai";
43
48
  model: string;
44
49
  dimensions: number;
45
50
  batch: {
@@ -78,9 +83,9 @@ Lower-level provider clients return an `EmbeddingBatchResult` with the validated
78
83
  vectors plus provider, model, dimensions, and intent. The compatibility helpers
79
84
  `generateEmbedding()` and `generateEmbeddings()` return only arrays.
80
85
 
81
- Gemini and OpenAI clients are scoped to their current options. They are not
82
- cached globally across different credentials or configurations. The local Xenova
83
- model can be cached by model name.
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.
84
89
 
85
90
  Hosted provider failures are reported with bounded provider/status/request-id
86
91
  context and without raw upstream bodies, credentials, Authorization headers, or
@@ -108,6 +113,36 @@ Notes:
108
113
  - batch metadata is `{ mode: "sequential" }`
109
114
  - the first run downloads the model and can take longer on a fresh machine
110
115
  - no API key is required
116
+ - this remains the default provider for backward compatibility and offline use
117
+
118
+ ## Cloudflare Workers AI
119
+
120
+ Provider value: `cloudflare`
121
+
122
+ Cloudflare is the recommended hosted provider for low-cost Markdown search.
123
+ It uses Workers AI `@cf/baai/bge-m3` through Cloudflare's OpenAI-compatible
124
+ embeddings endpoint.
125
+
126
+ ```ts
127
+ embeddingOptions: {
128
+ provider: "cloudflare",
129
+ accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
130
+ apiToken: process.env.CLOUDFLARE_API_TOKEN,
131
+ }
132
+ ```
133
+
134
+ Behavior:
135
+
136
+ - if `accountId` is omitted, the library reads `CLOUDFLARE_ACCOUNT_ID`
137
+ - if `apiToken` is omitted, the library reads `CLOUDFLARE_API_TOKEN`
138
+ - blank Cloudflare credentials are treated as missing
139
+ - `@cf/baai/bge-m3` returns 1024 dimensions
140
+ - metadata reports `@cf/baai/bge-m3` and 1024 dimensions without requiring
141
+ credentials
142
+ - batch metadata is `{ mode: "native" }`
143
+ - response items are reordered by provider-supplied index before being returned
144
+ - Cloudflare does not accept custom dimensions in this provider; use
145
+ `createTable(client, "articles", 1024)` for Cloudflare-backed indexes
111
146
 
112
147
  ## Gemini
113
148
 
@@ -156,7 +191,8 @@ Behavior:
156
191
 
157
192
  ## Dimension Guidelines
158
193
 
159
- - `768` is the easiest cross-provider target in the current implementation
194
+ - `local` defaults to `768`
195
+ - `cloudflare` is fixed at `1024`
160
196
  - local embeddings are padded from 384 to your target size
161
197
  - Gemini stays at 768
162
198
  - OpenAI can be used at 1536 or 3072, or another supported OpenAI dimension
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libsql-search",
3
- "version": "0.2.4",
3
+ "version": "0.3.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",