libsql-search 0.6.0 → 0.7.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
@@ -23,7 +23,8 @@ Use it when you want:
23
23
  - Embedding providers: local Hugging Face `Xenova/all-MiniLM-L6-v2`,
24
24
  Cloudflare Workers AI `@cf/baai/bge-m3`, Mistral `mistral-embed`, Google Gemini
25
25
  `gemini-embedding-2`, and OpenAI `text-embedding-3-small` /
26
- `text-embedding-3-large`
26
+ `text-embedding-3-large`; self-hosted OpenAI-compatible endpoints are also
27
+ available for TEI and similar trusted deployments
27
28
  - npm distribution plus JSR publishing
28
29
 
29
30
  ## Install
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;
package/dist/index.esm.js CHANGED
@@ -3,6 +3,7 @@ import { join, extname, relative, dirname } from 'path';
3
3
  import matter from 'gray-matter';
4
4
 
5
5
  const OPENAI_DEFAULT_DIMENSIONS = 768;
6
+ const OPENAI_COMPATIBLE_DEFAULT_BATCH_SIZE = 32;
6
7
  const LOCAL_DIMENSIONS = 384;
7
8
  const DEFAULT_MAX_LENGTH = 8e3;
8
9
  const DEFAULT_TIMEOUT_MS = 3e4;
@@ -109,6 +110,7 @@ function resolveProviderName(provider) {
109
110
  case "openai":
110
111
  case "mistral":
111
112
  case "cloudflare":
113
+ case "openai-compatible":
112
114
  return provider ?? "local";
113
115
  default:
114
116
  throw new Error(`Unknown embedding provider: ${String(provider)}`);
@@ -133,6 +135,13 @@ function getOptionalTrimmedCredential(value) {
133
135
  const trimmed = value?.trim();
134
136
  return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
135
137
  }
138
+ function getRequiredTrimmedString(value, optionName) {
139
+ const trimmed = getOptionalTrimmedCredential(value);
140
+ if (!trimmed) {
141
+ throw new Error(`Invalid ${optionName}: expected a non-empty string`);
142
+ }
143
+ return trimmed;
144
+ }
136
145
  function hasOwnProperty(value, property) {
137
146
  return Object.prototype.hasOwnProperty.call(value, property);
138
147
  }
@@ -293,7 +302,27 @@ async function embedSequentially(provider, operation, texts, signal, embedOne) {
293
302
  }
294
303
  return results;
295
304
  }
296
- function createProviderMetadata(provider, dimensions) {
305
+ function normalizeOpenAICompatibleEmbeddingsUrl(baseUrl) {
306
+ let url;
307
+ try {
308
+ url = new URL(baseUrl);
309
+ } catch {
310
+ throw new Error("Invalid baseUrl: expected an absolute http or https URL");
311
+ }
312
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
313
+ throw new Error("Invalid baseUrl: expected an absolute http or https URL");
314
+ }
315
+ if (url.username || url.password) {
316
+ throw new Error("Invalid baseUrl: URL credentials are not allowed");
317
+ }
318
+ if (url.search || url.hash) {
319
+ throw new Error("Invalid baseUrl: query strings and fragments are not allowed");
320
+ }
321
+ const path = url.pathname.replace(/\/+$/, "");
322
+ url.pathname = path.endsWith("/embeddings") ? path : `${path}/embeddings`;
323
+ return url.toString();
324
+ }
325
+ function createProviderMetadata(provider, dimensions, model) {
297
326
  switch (provider) {
298
327
  case "local":
299
328
  return Object.freeze({
@@ -330,6 +359,13 @@ function createProviderMetadata(provider, dimensions) {
330
359
  dimensions: CLOUDFLARE_DIMENSIONS,
331
360
  batch: Object.freeze({ mode: "native" })
332
361
  });
362
+ case "openai-compatible":
363
+ return Object.freeze({
364
+ name: "openai-compatible",
365
+ model: getRequiredTrimmedString(model, "model"),
366
+ dimensions,
367
+ batch: Object.freeze({ mode: "native" })
368
+ });
333
369
  }
334
370
  }
335
371
  function getEffectiveDimensions(provider, dimensions) {
@@ -370,6 +406,12 @@ function getEffectiveDimensions(provider, dimensions) {
370
406
  }
371
407
  return CLOUDFLARE_DIMENSIONS;
372
408
  }
409
+ if (provider === "openai-compatible") {
410
+ if (dimensions === void 0) {
411
+ throw new Error("Invalid dimensions: expected a positive integer");
412
+ }
413
+ return getPositiveInteger(dimensions, "dimensions");
414
+ }
373
415
  return getPositiveInteger(dimensions ?? OPENAI_DEFAULT_DIMENSIONS, "dimensions");
374
416
  }
375
417
  function assertBatchSize(metadata, count) {
@@ -380,6 +422,13 @@ function assertBatchSize(metadata, count) {
380
422
  );
381
423
  }
382
424
  }
425
+ function chunkTexts(texts, batchSize) {
426
+ const chunks = [];
427
+ for (let index = 0; index < texts.length; index += batchSize) {
428
+ chunks.push(texts.slice(index, index + batchSize));
429
+ }
430
+ return chunks;
431
+ }
383
432
  function createEmbeddingBatchResult(metadata, intent, embeddings) {
384
433
  return Object.freeze({
385
434
  embeddings,
@@ -480,18 +529,23 @@ async function requestOpenAICompatibleEmbeddings(request) {
480
529
  if (request.encodingFormat !== void 0) {
481
530
  body.encoding_format = request.encodingFormat;
482
531
  }
532
+ const headers = {
533
+ "Content-Type": "application/json"
534
+ };
535
+ if (request.apiKey !== void 0) {
536
+ headers.Authorization = `Bearer ${request.apiKey}`;
537
+ }
483
538
  const response = await fetch(request.endpoint, {
484
539
  method: "POST",
485
- headers: {
486
- "Content-Type": "application/json",
487
- "Authorization": `Bearer ${request.apiKey}`
488
- },
540
+ headers,
489
541
  signal: request.signal,
490
- body: JSON.stringify(body)
542
+ body: JSON.stringify(body),
543
+ ...request.redirect ? { redirect: request.redirect } : {}
491
544
  });
492
545
  if (!response.ok) {
493
- const requestId = getSafeResponseHeader(response, "x-request-id");
494
- const context = requestId ? ` status ${response.status}, request ${requestId}` : ` status ${response.status}`;
546
+ const statusHeader = request.statusHeader ?? { name: "x-request-id", label: "request" };
547
+ const statusHeaderValue = getSafeResponseHeader(response, statusHeader.name);
548
+ const context = statusHeaderValue ? ` status ${response.status}, ${statusHeader.label} ${statusHeaderValue}` : ` status ${response.status}`;
495
549
  throw new Error(context);
496
550
  }
497
551
  const data = await response.json();
@@ -515,13 +569,13 @@ async function requestOpenAICompatibleEmbeddings(request) {
515
569
  return parsed;
516
570
  });
517
571
  }
518
- class OpenAIEmbeddingProvider {
572
+ class OpenAICompatibleEmbeddingProvider {
519
573
  metadata;
520
- #apiKey;
521
574
  #timeoutMs;
522
- constructor(metadata, apiKey, timeoutMs) {
575
+ #profile;
576
+ constructor(metadata, profile, timeoutMs) {
523
577
  this.metadata = metadata;
524
- this.#apiKey = apiKey;
578
+ this.#profile = profile;
525
579
  this.#timeoutMs = timeoutMs;
526
580
  }
527
581
  async embed(texts, options = {}) {
@@ -531,131 +585,44 @@ class OpenAIEmbeddingProvider {
531
585
  }
532
586
  assertBatchSize(this.metadata, texts.length);
533
587
  const items = await withTimeout(
534
- "openai",
535
- "API request",
536
- this.#timeoutMs,
537
- async (signal) => requestOpenAICompatibleEmbeddings({
538
- responseLabel: "OpenAI",
539
- endpoint: OPENAI_EMBEDDINGS_URL,
540
- apiKey: this.#apiKey,
541
- model: this.metadata.model,
542
- texts,
543
- signal,
544
- dimensions: this.metadata.dimensions
545
- }),
546
- options.signal,
547
- [this.#apiKey]
548
- );
549
- return createEmbeddingBatchResult(
550
- this.metadata,
551
- intent,
552
- validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "openai")
553
- );
554
- }
555
- }
556
- class MistralEmbeddingProvider {
557
- metadata;
558
- #apiKey;
559
- #timeoutMs;
560
- constructor(metadata, apiKey, timeoutMs) {
561
- this.metadata = metadata;
562
- this.#apiKey = apiKey;
563
- this.#timeoutMs = timeoutMs;
564
- }
565
- async embed(texts, options = {}) {
566
- const intent = resolveIntent(options.intent);
567
- if (texts.length === 0) {
568
- return createEmbeddingBatchResult(this.metadata, intent, []);
569
- }
570
- assertBatchSize(this.metadata, texts.length);
571
- const items = await withTimeout(
572
- "mistral",
573
- "API request",
574
- this.#timeoutMs,
575
- async (signal) => requestOpenAICompatibleEmbeddings({
576
- responseLabel: "Mistral",
577
- endpoint: MISTRAL_EMBEDDINGS_URL,
578
- apiKey: this.#apiKey,
579
- model: this.metadata.model,
580
- texts,
581
- signal,
582
- encodingFormat: "float",
583
- requireIndex: true
584
- }),
585
- options.signal,
586
- [this.#apiKey]
587
- );
588
- return createEmbeddingBatchResult(
589
- this.metadata,
590
- intent,
591
- validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "mistral")
592
- );
593
- }
594
- }
595
- class CloudflareEmbeddingProvider {
596
- metadata;
597
- #accountId;
598
- #apiToken;
599
- #timeoutMs;
600
- constructor(metadata, accountId, apiToken, timeoutMs) {
601
- this.metadata = metadata;
602
- this.#accountId = accountId;
603
- this.#apiToken = apiToken;
604
- this.#timeoutMs = timeoutMs;
605
- }
606
- async embed(texts, options = {}) {
607
- const intent = resolveIntent(options.intent);
608
- if (texts.length === 0) {
609
- return createEmbeddingBatchResult(this.metadata, intent, []);
610
- }
611
- assertBatchSize(this.metadata, texts.length);
612
- const endpoint = createCloudflareEmbeddingsUrl(this.#accountId);
613
- const items = await withTimeout(
614
- "cloudflare",
588
+ this.#profile.provider,
615
589
  "API request",
616
590
  this.#timeoutMs,
617
591
  async (signal) => {
618
- const response = await fetch(endpoint, {
619
- method: "POST",
620
- headers: {
621
- "Content-Type": "application/json",
622
- "Authorization": `Bearer ${this.#apiToken}`
623
- },
624
- signal,
625
- body: JSON.stringify({
626
- model: this.metadata.model,
627
- input: texts
628
- })
629
- });
630
- if (!response.ok) {
631
- const cfRay = getSafeResponseHeader(response, "cf-ray");
632
- const context = cfRay ? ` status ${response.status}, cf-ray ${cfRay}` : ` status ${response.status}`;
633
- throw new Error(context);
634
- }
635
- const data = await response.json();
636
- if (!Array.isArray(data.data)) {
637
- throw new Error("Cloudflare response did not include a data array");
592
+ const batches = this.#profile.batchSize === void 0 ? [texts] : chunkTexts(texts, this.#profile.batchSize);
593
+ const results = [];
594
+ for (const batch of batches) {
595
+ throwIfAborted(this.#profile.provider, "API request", signal);
596
+ const batchItems = await requestOpenAICompatibleEmbeddings({
597
+ responseLabel: this.#profile.responseLabel,
598
+ endpoint: this.#profile.endpoint,
599
+ ...this.#profile.apiKey !== void 0 ? { apiKey: this.#profile.apiKey } : {},
600
+ model: this.#profile.model,
601
+ texts: batch,
602
+ signal,
603
+ ...this.#profile.includeDimensions ? { dimensions: this.#profile.dimensions } : {},
604
+ ...this.#profile.encodingFormat ? { encodingFormat: this.#profile.encodingFormat } : {},
605
+ ...this.#profile.requireIndex !== void 0 ? { requireIndex: this.#profile.requireIndex } : {},
606
+ ...this.#profile.redirect ? { redirect: this.#profile.redirect } : {},
607
+ ...this.#profile.statusHeader ? { statusHeader: this.#profile.statusHeader } : {}
608
+ });
609
+ results.push(...validateEmbeddingBatch(
610
+ batchItems,
611
+ batch.length,
612
+ this.metadata.dimensions,
613
+ this.#profile.provider
614
+ ));
615
+ throwIfAborted(this.#profile.provider, "API request", signal);
638
616
  }
639
- return data.data.map((item) => {
640
- const typed = item;
641
- const embedding = typed.embedding;
642
- if (!Array.isArray(embedding)) {
643
- throw new Error("Cloudflare response item did not include an embedding array");
644
- }
645
- const parsed = { embedding };
646
- if (hasOwnProperty(typed, "index")) {
647
- parsed.index = typed.index;
648
- }
649
- return parsed;
650
- });
617
+ return results;
651
618
  },
652
619
  options.signal,
653
- [this.#apiToken, this.#accountId, encodeURIComponent(this.#accountId)]
620
+ this.#profile.exactSecrets ?? []
654
621
  );
655
622
  return createEmbeddingBatchResult(
656
623
  this.metadata,
657
624
  intent,
658
- validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "cloudflare")
625
+ validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, this.#profile.provider)
659
626
  );
660
627
  }
661
628
  }
@@ -680,7 +647,16 @@ function createEmbeddingProvider(options = {}) {
680
647
  if (!key) {
681
648
  throw new Error("OPENAI_API_KEY is required for OpenAI embeddings");
682
649
  }
683
- return new OpenAIEmbeddingProvider(metadata, key, timeoutMs);
650
+ return new OpenAICompatibleEmbeddingProvider(metadata, {
651
+ provider: "openai",
652
+ responseLabel: "OpenAI",
653
+ endpoint: OPENAI_EMBEDDINGS_URL,
654
+ apiKey: key,
655
+ model: metadata.model,
656
+ dimensions: metadata.dimensions,
657
+ includeDimensions: true,
658
+ exactSecrets: [key]
659
+ }, timeoutMs);
684
660
  }
685
661
  case "mistral": {
686
662
  const key = getOptionalTrimmedCredential(
@@ -689,7 +665,17 @@ function createEmbeddingProvider(options = {}) {
689
665
  if (!key) {
690
666
  throw new Error("MISTRAL_API_KEY is required for Mistral embeddings");
691
667
  }
692
- return new MistralEmbeddingProvider(metadata, key, timeoutMs);
668
+ return new OpenAICompatibleEmbeddingProvider(metadata, {
669
+ provider: "mistral",
670
+ responseLabel: "Mistral",
671
+ endpoint: MISTRAL_EMBEDDINGS_URL,
672
+ apiKey: key,
673
+ model: metadata.model,
674
+ dimensions: metadata.dimensions,
675
+ encodingFormat: "float",
676
+ requireIndex: true,
677
+ exactSecrets: [key]
678
+ }, timeoutMs);
693
679
  }
694
680
  case "cloudflare": {
695
681
  const accountId = getOptionalTrimmedCredential(
@@ -704,13 +690,56 @@ function createEmbeddingProvider(options = {}) {
704
690
  if (!apiToken) {
705
691
  throw new Error("CLOUDFLARE_API_TOKEN is required for Cloudflare embeddings");
706
692
  }
707
- return new CloudflareEmbeddingProvider(metadata, accountId, apiToken, timeoutMs);
693
+ const endpoint = createCloudflareEmbeddingsUrl(accountId);
694
+ return new OpenAICompatibleEmbeddingProvider(metadata, {
695
+ provider: "cloudflare",
696
+ responseLabel: "Cloudflare",
697
+ endpoint,
698
+ apiKey: apiToken,
699
+ model: metadata.model,
700
+ dimensions: metadata.dimensions,
701
+ statusHeader: { name: "cf-ray", label: "cf-ray" },
702
+ exactSecrets: [apiToken, accountId, encodeURIComponent(accountId), endpoint]
703
+ }, timeoutMs);
704
+ }
705
+ case "openai-compatible": {
706
+ const baseUrl = getRequiredTrimmedString(options.baseUrl, "baseUrl");
707
+ const endpoint = normalizeOpenAICompatibleEmbeddingsUrl(baseUrl);
708
+ const apiKey = getOptionalTrimmedCredential(options.apiKey);
709
+ const batchSize = getPositiveInteger(
710
+ options.batchSize ?? OPENAI_COMPATIBLE_DEFAULT_BATCH_SIZE,
711
+ "batchSize"
712
+ );
713
+ return new OpenAICompatibleEmbeddingProvider(metadata, {
714
+ provider: "openai-compatible",
715
+ responseLabel: "OpenAI-compatible",
716
+ endpoint,
717
+ ...apiKey ? { apiKey } : {},
718
+ model: metadata.model,
719
+ dimensions: metadata.dimensions,
720
+ includeDimensions: true,
721
+ encodingFormat: "float",
722
+ requireIndex: true,
723
+ redirect: "error",
724
+ batchSize,
725
+ exactSecrets: [
726
+ ...apiKey ? [apiKey] : [],
727
+ baseUrl,
728
+ endpoint
729
+ ]
730
+ }, timeoutMs);
708
731
  }
709
732
  }
710
733
  }
711
734
  function getEmbeddingProviderMetadata(options = {}) {
712
735
  const provider = resolveProviderName(options.provider);
713
- return createProviderMetadata(provider, getEffectiveDimensions(provider, options.dimensions));
736
+ if (provider === "openai-compatible") {
737
+ normalizeOpenAICompatibleEmbeddingsUrl(getRequiredTrimmedString(options.baseUrl, "baseUrl"));
738
+ if (options.batchSize !== void 0) {
739
+ getPositiveInteger(options.batchSize, "batchSize");
740
+ }
741
+ }
742
+ return createProviderMetadata(provider, getEffectiveDimensions(provider, options.dimensions), options.model);
714
743
  }
715
744
  async function generateEmbeddings(texts, options = {}) {
716
745
  const maxLength = getPositiveInteger(options.maxLength ?? DEFAULT_MAX_LENGTH, "maxLength");
package/docs/API.md CHANGED
@@ -246,7 +246,13 @@ Provider clients return:
246
246
  ```ts
247
247
  interface EmbeddingBatchResult {
248
248
  embeddings: number[][];
249
- provider: "local" | "cloudflare" | "mistral" | "gemini" | "openai";
249
+ provider:
250
+ | "local"
251
+ | "cloudflare"
252
+ | "mistral"
253
+ | "gemini"
254
+ | "openai"
255
+ | "openai-compatible";
250
256
  model: string;
251
257
  dimensions: number;
252
258
  intent: "document" | "query";
@@ -262,10 +268,19 @@ cardinality, dimensions, finite numeric values, and indexed batch ordering.
262
268
 
263
269
  ```ts
264
270
  interface EmbeddingOptions {
265
- provider?: "local" | "cloudflare" | "mistral" | "gemini" | "openai";
271
+ provider?:
272
+ | "local"
273
+ | "cloudflare"
274
+ | "mistral"
275
+ | "gemini"
276
+ | "openai"
277
+ | "openai-compatible";
266
278
  apiKey?: string;
267
279
  accountId?: string;
268
280
  apiToken?: string;
281
+ baseUrl?: string;
282
+ model?: string;
283
+ batchSize?: number;
269
284
  dimensions?: number;
270
285
  maxLength?: number;
271
286
  intent?: "document" | "query";
@@ -279,6 +294,13 @@ interface EmbeddingOptions {
279
294
  Cloudflare uses `accountId` and `apiToken`, which fall back to
280
295
  `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`.
281
296
 
297
+ For `provider: "openai-compatible"`, callers must pass `baseUrl`, `model`, and
298
+ `dimensions`. `apiKey` is optional and is used only when explicitly provided;
299
+ the provider never reads `OPENAI_API_KEY`. `batchSize` controls the maximum
300
+ items per outbound request and defaults to `32`. The endpoint is treated as
301
+ trusted server-side configuration and should not be derived from untrusted
302
+ request input.
303
+
282
304
  ### `padEmbedding(embedding, targetDimensions)`
283
305
 
284
306
  Pads or truncates an embedding array to the requested length.
@@ -132,6 +132,7 @@ const embeddingProvider =
132
132
  | "mistral"
133
133
  | "gemini"
134
134
  | "openai"
135
+ | "openai-compatible"
135
136
  | undefined;
136
137
 
137
138
  const dimensionsByProvider = {
@@ -142,7 +143,10 @@ const dimensionsByProvider = {
142
143
  openai: 1536,
143
144
  } as const;
144
145
 
145
- const embeddingDimensions = dimensionsByProvider[embeddingProvider ?? "local"];
146
+ const embeddingDimensions =
147
+ embeddingProvider === "openai-compatible"
148
+ ? Number(process.env.EMBEDDING_DIMENSIONS)
149
+ : dimensionsByProvider[embeddingProvider ?? "local"];
146
150
 
147
151
  await createTable(client, "articles", embeddingDimensions);
148
152
 
@@ -151,6 +155,8 @@ await indexContent({
151
155
  contentPath: "./content",
152
156
  embeddingOptions: {
153
157
  provider: embeddingProvider,
158
+ baseUrl: process.env.EMBEDDING_BASE_URL,
159
+ model: process.env.EMBEDDING_MODEL,
154
160
  dimensions: embeddingDimensions,
155
161
  },
156
162
  });
@@ -158,3 +164,9 @@ await indexContent({
158
164
 
159
165
  Pair this with your framework build command so indexed content and deployed code
160
166
  stay in sync.
167
+
168
+ When you use `openai-compatible`, keep `EMBEDDING_BASE_URL` as trusted
169
+ server-side configuration, not user request input. Recreate the vector table or
170
+ rebuild into a separate table whenever the provider, endpoint, model, or
171
+ dimension count changes; stored vectors and query vectors must come from the
172
+ same embedding space.
package/docs/PROVIDERS.md CHANGED
@@ -1,12 +1,13 @@
1
1
  # Embedding Providers
2
2
 
3
- `libsql-search` currently supports five embedding providers:
3
+ `libsql-search` currently supports six embedding providers:
4
4
 
5
5
  - `local`
6
6
  - `cloudflare`
7
7
  - `mistral`
8
8
  - `gemini`
9
9
  - `openai`
10
+ - `openai-compatible`
10
11
 
11
12
  Use the same provider and dimensions for both indexing and querying. A mismatch
12
13
  between stored vectors and query vectors will break search quality or fail at
@@ -16,10 +17,19 @@ query time.
16
17
 
17
18
  ```ts
18
19
  interface EmbeddingOptions {
19
- provider?: "local" | "cloudflare" | "mistral" | "gemini" | "openai";
20
+ provider?:
21
+ | "local"
22
+ | "cloudflare"
23
+ | "mistral"
24
+ | "gemini"
25
+ | "openai"
26
+ | "openai-compatible";
20
27
  apiKey?: string;
21
28
  accountId?: string;
22
29
  apiToken?: string;
30
+ baseUrl?: string;
31
+ model?: string;
32
+ batchSize?: number;
23
33
  dimensions?: number;
24
34
  maxLength?: number;
25
35
  intent?: "document" | "query";
@@ -37,9 +47,12 @@ interface EmbeddingOptions {
37
47
  `"document"` and search defaults to `"query"` unless explicitly set
38
48
  - `timeoutMs` defaults to `30000`
39
49
  - `apiKey` is used by Mistral, Gemini, and OpenAI and falls back to
40
- `MISTRAL_API_KEY`, `GEMINI_API_KEY`, or `OPENAI_API_KEY`
50
+ `MISTRAL_API_KEY`, `GEMINI_API_KEY`, or `OPENAI_API_KEY`. For
51
+ `openai-compatible`, `apiKey` is optional and never falls back to
52
+ `OPENAI_API_KEY`.
41
53
  - `accountId` and `apiToken` are used by Cloudflare and fall back to
42
54
  `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`
55
+ - `baseUrl`, `model`, and `batchSize` are used by `openai-compatible`
43
56
 
44
57
  ## Provider Contract
45
58
 
@@ -47,7 +60,13 @@ Each provider exposes immutable metadata:
47
60
 
48
61
  ```ts
49
62
  interface EmbeddingProviderMetadata {
50
- name: "local" | "cloudflare" | "mistral" | "gemini" | "openai";
63
+ name:
64
+ | "local"
65
+ | "cloudflare"
66
+ | "mistral"
67
+ | "gemini"
68
+ | "openai"
69
+ | "openai-compatible";
51
70
  model: string;
52
71
  dimensions: number;
53
72
  batch: {
@@ -229,6 +248,80 @@ Behavior:
229
248
  - batch metadata is `{ mode: "native", maxSize: 2048 }`
230
249
  - use the same dimension count in `createTable()`
231
250
 
251
+ ## OpenAI-Compatible Endpoints
252
+
253
+ Provider value: `openai-compatible`
254
+
255
+ Use this provider for trusted OpenAI-compatible embedding services such as
256
+ Hugging Face Text Embeddings Inference (TEI) or an internal gateway. This is an
257
+ optional escape hatch; `local` remains the default offline provider, and the
258
+ named hosted providers above are still preferred when their fixed adapters fit.
259
+
260
+ ```ts
261
+ embeddingOptions: {
262
+ provider: "openai-compatible",
263
+ baseUrl: "http://localhost:8080/v1",
264
+ model: "BAAI/bge-large-en-v1.5",
265
+ dimensions: 1024,
266
+ batchSize: 32,
267
+ }
268
+ ```
269
+
270
+ If your endpoint requires bearer auth, pass `apiKey` explicitly:
271
+
272
+ ```ts
273
+ embeddingOptions: {
274
+ provider: "openai-compatible",
275
+ baseUrl: "https://embeddings.example.com/v1",
276
+ apiKey: process.env.EMBEDDINGS_API_KEY,
277
+ model: "BAAI/bge-large-en-v1.5",
278
+ dimensions: 1024,
279
+ }
280
+ ```
281
+
282
+ Behavior:
283
+
284
+ - `baseUrl`, `model`, and `dimensions` are required
285
+ - `baseUrl` is the API base, such as `http://localhost:8080/v1`; the library
286
+ sends requests to `/embeddings` below that base
287
+ - a base URL that already ends in `/embeddings` is used as-is
288
+ - only absolute `http` and `https` URLs are accepted
289
+ - URL usernames, passwords, query strings, and fragments are rejected
290
+ - `apiKey` is optional; blank keys are ignored and no `Authorization` header is
291
+ sent
292
+ - `OPENAI_API_KEY` is never read for this provider
293
+ - `batchSize` defaults to `32`, matching TEI's conservative client batch size;
294
+ larger input arrays are split into sequential outbound requests and returned
295
+ in the original input order
296
+ - requests send `{ input, model, dimensions, encoding_format: "float" }`
297
+ - responses must use the standard OpenAI embeddings shape with indexed
298
+ `data[]` items; each response chunk must include unique contiguous indices
299
+ - batch metadata is `{ mode: "native" }`; the internal outbound chunk size is
300
+ not reported as `batch.maxSize`
301
+
302
+ Security boundary:
303
+
304
+ - treat `baseUrl` as trusted server-side configuration only
305
+ - never pass user-supplied request values directly into `baseUrl`
306
+ - use HTTPS for remote endpoints
307
+ - credentials are not sent across redirects
308
+ - non-2xx response bodies are not read, and errors avoid echoing API keys or
309
+ configured endpoint URLs
310
+
311
+ TEI exposes an OpenAI-compatible base at `/v1`, so a local TEI server usually
312
+ uses:
313
+
314
+ ```ts
315
+ embeddingOptions: {
316
+ provider: "openai-compatible",
317
+ baseUrl: "http://localhost:8080/v1",
318
+ model: "BAAI/bge-large-en-v1.5",
319
+ dimensions: 1024,
320
+ }
321
+ ```
322
+
323
+ This library does not call TEI's native `/embed` endpoint.
324
+
232
325
  ## Dimension Guidelines
233
326
 
234
327
  - `local` is fixed at `384`
@@ -238,9 +331,12 @@ Behavior:
238
331
  `3072`; use `768`, `1536`, or `3072` unless you have a specific reason
239
332
  - OpenAI can be used at 1536 or 3072, or another supported OpenAI dimension
240
333
  value you explicitly set
334
+ - `openai-compatible` requires you to set the dimension count that your
335
+ endpoint/model actually returns
241
336
 
242
- If you switch provider or dimensions for an existing table, recreate the table
243
- or rebuild the index into a separate table so stored vectors stay consistent.
337
+ If you switch provider, endpoint, model, or dimensions for an existing table,
338
+ recreate the table or rebuild the index into a separate table so stored vectors
339
+ stay consistent.
244
340
 
245
341
  Existing Gemini indexes created with `text-embedding-004` must be fully
246
342
  re-embedded for `gemini-embedding-2`, even if you keep `dimensions: 768`,
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, Cloudflare, Mistral, Gemini, and
7
- OpenAI embedding options, dimensions, and API key behavior
6
+ - [Provider guide](./PROVIDERS.md): local, hosted, and OpenAI-compatible
7
+ 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libsql-search",
3
- "version": "0.6.0",
3
+ "version": "0.7.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",