libsql-search 0.5.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
@@ -20,10 +20,11 @@ Use it when you want:
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: local `Xenova/all-MiniLM-L6-v2`, Cloudflare Workers AI
24
- `@cf/baai/bge-m3`, Mistral `mistral-embed`, Google Gemini
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
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
@@ -65,14 +66,13 @@ const client = createClient({
65
66
  authToken: "your-auth-token",
66
67
  });
67
68
 
68
- await createTable(client, "articles", 768);
69
+ await createTable(client);
69
70
 
70
71
  await indexContent({
71
72
  client,
72
73
  contentPath: "./content",
73
74
  embeddingOptions: {
74
75
  provider: "local",
75
- dimensions: 768,
76
76
  },
77
77
  });
78
78
 
@@ -82,7 +82,6 @@ const results = await search({
82
82
  limit: 5,
83
83
  embeddingOptions: {
84
84
  provider: "local",
85
- dimensions: 768,
86
85
  },
87
86
  });
88
87
 
@@ -98,9 +97,9 @@ Important behavior:
98
97
  - Call `createTable()` before indexing or searching.
99
98
  - Keep dimensions aligned across table creation, indexing, and search queries.
100
99
  - `indexContent()` clears existing rows before rebuilding the index.
101
- - `local` is the default offline provider; Cloudflare is the recommended hosted
102
- option. Cloudflare and Mistral use 1024 dimensions. Gemini defaults to 3072
103
- dimensions and supports 128-3072.
100
+ - `local` is the default offline provider and uses 384 dimensions. Cloudflare is
101
+ the recommended hosted option. Cloudflare and Mistral use 1024 dimensions.
102
+ Gemini defaults to 3072 dimensions and supports 128-3072.
104
103
 
105
104
  ## Core API
106
105
 
package/dist/index.cjs CHANGED
@@ -4,7 +4,9 @@ var promises = require('fs/promises');
4
4
  var path = require('path');
5
5
  var matter = require('gray-matter');
6
6
 
7
- const DEFAULT_DIMENSIONS = 768;
7
+ const OPENAI_DEFAULT_DIMENSIONS = 768;
8
+ const OPENAI_COMPATIBLE_DEFAULT_BATCH_SIZE = 32;
9
+ const LOCAL_DIMENSIONS = 384;
8
10
  const DEFAULT_MAX_LENGTH = 8e3;
9
11
  const DEFAULT_TIMEOUT_MS = 3e4;
10
12
  const LOCAL_MODEL = "Xenova/all-MiniLM-L6-v2";
@@ -33,17 +35,66 @@ function getEnvironmentVariable(name) {
33
35
  return void 0;
34
36
  }
35
37
  }
36
- async function getLocalEmbeddingModel(modelName) {
38
+ function deletePendingLocalModelCache(modelName, entry) {
39
+ if (!entry.settled && entry.waiters === 0 && localModelCacheByModel.get(modelName) === entry) {
40
+ localModelCacheByModel.delete(modelName);
41
+ }
42
+ }
43
+ async function getLocalEmbeddingModel(modelName, signal) {
37
44
  const cached = localModelCacheByModel.get(modelName);
38
- if (cached?.model) {
39
- return cached.model;
45
+ if (cached) {
46
+ cached.waiters++;
47
+ try {
48
+ return await waitForLocalEmbeddingModel(modelName, cached, signal);
49
+ } finally {
50
+ cached.waiters--;
51
+ deletePendingLocalModelCache(modelName, cached);
52
+ }
53
+ }
54
+ const modelPromise = (async () => {
55
+ console.log(`Loading local embedding model (${modelName})...`);
56
+ const { pipeline } = await import('@huggingface/transformers');
57
+ const model = await pipeline("feature-extraction", modelName);
58
+ console.log("Local model loaded successfully");
59
+ return model;
60
+ })();
61
+ const entry = {
62
+ promise: modelPromise,
63
+ settled: false,
64
+ waiters: 1
65
+ };
66
+ localModelCacheByModel.set(modelName, entry);
67
+ modelPromise.then(() => {
68
+ entry.settled = true;
69
+ }).catch(() => {
70
+ localModelCacheByModel.delete(modelName);
71
+ });
72
+ try {
73
+ return await waitForLocalEmbeddingModel(modelName, entry, signal);
74
+ } finally {
75
+ entry.waiters--;
76
+ deletePendingLocalModelCache(modelName, entry);
77
+ }
78
+ }
79
+ async function waitForLocalEmbeddingModel(modelName, entry, signal) {
80
+ if (signal.aborted) {
81
+ deletePendingLocalModelCache(modelName, entry);
82
+ throw providerError("local", "model inference was aborted");
83
+ }
84
+ let rejectAbort = () => {
85
+ };
86
+ const abortPromise = new Promise((_resolve, reject) => {
87
+ rejectAbort = reject;
88
+ });
89
+ const onAbort = () => {
90
+ rejectAbort(providerError("local", "model inference was aborted"));
91
+ };
92
+ signal.addEventListener("abort", onAbort, { once: true });
93
+ try {
94
+ return await Promise.race([entry.promise, abortPromise]);
95
+ } finally {
96
+ signal.removeEventListener("abort", onAbort);
40
97
  }
41
- console.log(`Loading local embedding model (${modelName})...`);
42
- const { pipeline } = await import('@xenova/transformers');
43
- const model = await pipeline("feature-extraction", modelName);
44
- localModelCacheByModel.set(modelName, { model });
45
- console.log("Local model loaded successfully");
46
- return model;
47
98
  }
48
99
  function getPositiveInteger(value, optionName) {
49
100
  if (!Number.isInteger(value) || value <= 0) {
@@ -61,6 +112,7 @@ function resolveProviderName(provider) {
61
112
  case "openai":
62
113
  case "mistral":
63
114
  case "cloudflare":
115
+ case "openai-compatible":
64
116
  return provider ?? "local";
65
117
  default:
66
118
  throw new Error(`Unknown embedding provider: ${String(provider)}`);
@@ -85,6 +137,13 @@ function getOptionalTrimmedCredential(value) {
85
137
  const trimmed = value?.trim();
86
138
  return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
87
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
+ }
88
147
  function hasOwnProperty(value, property) {
89
148
  return Object.prototype.hasOwnProperty.call(value, property);
90
149
  }
@@ -245,7 +304,27 @@ async function embedSequentially(provider, operation, texts, signal, embedOne) {
245
304
  }
246
305
  return results;
247
306
  }
248
- 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) {
249
328
  switch (provider) {
250
329
  case "local":
251
330
  return Object.freeze({
@@ -282,9 +361,25 @@ function createProviderMetadata(provider, dimensions) {
282
361
  dimensions: CLOUDFLARE_DIMENSIONS,
283
362
  batch: Object.freeze({ mode: "native" })
284
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
+ });
285
371
  }
286
372
  }
287
373
  function getEffectiveDimensions(provider, dimensions) {
374
+ if (provider === "local") {
375
+ if (dimensions !== void 0 && dimensions !== LOCAL_DIMENSIONS) {
376
+ throw providerError(
377
+ "local",
378
+ `${LOCAL_MODEL} returns ${LOCAL_DIMENSIONS} dimensions; received dimensions ${String(dimensions)}`
379
+ );
380
+ }
381
+ return LOCAL_DIMENSIONS;
382
+ }
288
383
  if (provider === "gemini") {
289
384
  const effectiveDimensions = dimensions ?? GEMINI_DIMENSIONS;
290
385
  if (!Number.isInteger(effectiveDimensions) || effectiveDimensions < GEMINI_MIN_DIMENSIONS || effectiveDimensions > GEMINI_MAX_DIMENSIONS) {
@@ -313,7 +408,13 @@ function getEffectiveDimensions(provider, dimensions) {
313
408
  }
314
409
  return CLOUDFLARE_DIMENSIONS;
315
410
  }
316
- return getPositiveInteger(dimensions ?? DEFAULT_DIMENSIONS, "dimensions");
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
+ }
417
+ return getPositiveInteger(dimensions ?? OPENAI_DEFAULT_DIMENSIONS, "dimensions");
317
418
  }
318
419
  function assertBatchSize(metadata, count) {
319
420
  if (metadata.batch.maxSize !== void 0 && count > metadata.batch.maxSize) {
@@ -323,6 +424,13 @@ function assertBatchSize(metadata, count) {
323
424
  );
324
425
  }
325
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
+ }
326
434
  function createEmbeddingBatchResult(metadata, intent, embeddings) {
327
435
  return Object.freeze({
328
436
  embeddings,
@@ -350,14 +458,13 @@ class LocalEmbeddingProvider {
350
458
  "model inference",
351
459
  this.#timeoutMs,
352
460
  async (signal) => {
353
- const model = await getLocalEmbeddingModel(this.metadata.model);
461
+ const model = await getLocalEmbeddingModel(this.metadata.model, signal);
354
462
  return embedSequentially("local", "model inference", texts, signal, async (text) => {
355
463
  const output = await model(text, {
356
464
  pooling: "mean",
357
465
  normalize: true
358
466
  });
359
- const embedding = Array.from(output.data);
360
- return padEmbedding(embedding, this.metadata.dimensions);
467
+ return Array.from(output.data);
361
468
  });
362
469
  },
363
470
  options.signal
@@ -424,18 +531,23 @@ async function requestOpenAICompatibleEmbeddings(request) {
424
531
  if (request.encodingFormat !== void 0) {
425
532
  body.encoding_format = request.encodingFormat;
426
533
  }
534
+ const headers = {
535
+ "Content-Type": "application/json"
536
+ };
537
+ if (request.apiKey !== void 0) {
538
+ headers.Authorization = `Bearer ${request.apiKey}`;
539
+ }
427
540
  const response = await fetch(request.endpoint, {
428
541
  method: "POST",
429
- headers: {
430
- "Content-Type": "application/json",
431
- "Authorization": `Bearer ${request.apiKey}`
432
- },
542
+ headers,
433
543
  signal: request.signal,
434
- body: JSON.stringify(body)
544
+ body: JSON.stringify(body),
545
+ ...request.redirect ? { redirect: request.redirect } : {}
435
546
  });
436
547
  if (!response.ok) {
437
- const requestId = getSafeResponseHeader(response, "x-request-id");
438
- 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}`;
439
551
  throw new Error(context);
440
552
  }
441
553
  const data = await response.json();
@@ -459,92 +571,13 @@ async function requestOpenAICompatibleEmbeddings(request) {
459
571
  return parsed;
460
572
  });
461
573
  }
462
- class OpenAIEmbeddingProvider {
463
- metadata;
464
- #apiKey;
465
- #timeoutMs;
466
- constructor(metadata, apiKey, timeoutMs) {
467
- this.metadata = metadata;
468
- this.#apiKey = apiKey;
469
- this.#timeoutMs = timeoutMs;
470
- }
471
- async embed(texts, options = {}) {
472
- const intent = resolveIntent(options.intent);
473
- if (texts.length === 0) {
474
- return createEmbeddingBatchResult(this.metadata, intent, []);
475
- }
476
- assertBatchSize(this.metadata, texts.length);
477
- const items = await withTimeout(
478
- "openai",
479
- "API request",
480
- this.#timeoutMs,
481
- async (signal) => requestOpenAICompatibleEmbeddings({
482
- responseLabel: "OpenAI",
483
- endpoint: OPENAI_EMBEDDINGS_URL,
484
- apiKey: this.#apiKey,
485
- model: this.metadata.model,
486
- texts,
487
- signal,
488
- dimensions: this.metadata.dimensions
489
- }),
490
- options.signal,
491
- [this.#apiKey]
492
- );
493
- return createEmbeddingBatchResult(
494
- this.metadata,
495
- intent,
496
- validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "openai")
497
- );
498
- }
499
- }
500
- class MistralEmbeddingProvider {
501
- metadata;
502
- #apiKey;
503
- #timeoutMs;
504
- constructor(metadata, apiKey, timeoutMs) {
505
- this.metadata = metadata;
506
- this.#apiKey = apiKey;
507
- this.#timeoutMs = timeoutMs;
508
- }
509
- async embed(texts, options = {}) {
510
- const intent = resolveIntent(options.intent);
511
- if (texts.length === 0) {
512
- return createEmbeddingBatchResult(this.metadata, intent, []);
513
- }
514
- assertBatchSize(this.metadata, texts.length);
515
- const items = await withTimeout(
516
- "mistral",
517
- "API request",
518
- this.#timeoutMs,
519
- async (signal) => requestOpenAICompatibleEmbeddings({
520
- responseLabel: "Mistral",
521
- endpoint: MISTRAL_EMBEDDINGS_URL,
522
- apiKey: this.#apiKey,
523
- model: this.metadata.model,
524
- texts,
525
- signal,
526
- encodingFormat: "float",
527
- requireIndex: true
528
- }),
529
- options.signal,
530
- [this.#apiKey]
531
- );
532
- return createEmbeddingBatchResult(
533
- this.metadata,
534
- intent,
535
- validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "mistral")
536
- );
537
- }
538
- }
539
- class CloudflareEmbeddingProvider {
574
+ class OpenAICompatibleEmbeddingProvider {
540
575
  metadata;
541
- #accountId;
542
- #apiToken;
543
576
  #timeoutMs;
544
- constructor(metadata, accountId, apiToken, timeoutMs) {
577
+ #profile;
578
+ constructor(metadata, profile, timeoutMs) {
545
579
  this.metadata = metadata;
546
- this.#accountId = accountId;
547
- this.#apiToken = apiToken;
580
+ this.#profile = profile;
548
581
  this.#timeoutMs = timeoutMs;
549
582
  }
550
583
  async embed(texts, options = {}) {
@@ -553,53 +586,45 @@ class CloudflareEmbeddingProvider {
553
586
  return createEmbeddingBatchResult(this.metadata, intent, []);
554
587
  }
555
588
  assertBatchSize(this.metadata, texts.length);
556
- const endpoint = createCloudflareEmbeddingsUrl(this.#accountId);
557
589
  const items = await withTimeout(
558
- "cloudflare",
590
+ this.#profile.provider,
559
591
  "API request",
560
592
  this.#timeoutMs,
561
593
  async (signal) => {
562
- const response = await fetch(endpoint, {
563
- method: "POST",
564
- headers: {
565
- "Content-Type": "application/json",
566
- "Authorization": `Bearer ${this.#apiToken}`
567
- },
568
- signal,
569
- body: JSON.stringify({
570
- model: this.metadata.model,
571
- input: texts
572
- })
573
- });
574
- if (!response.ok) {
575
- const cfRay = getSafeResponseHeader(response, "cf-ray");
576
- const context = cfRay ? ` status ${response.status}, cf-ray ${cfRay}` : ` status ${response.status}`;
577
- throw new Error(context);
578
- }
579
- const data = await response.json();
580
- if (!Array.isArray(data.data)) {
581
- 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);
582
618
  }
583
- return data.data.map((item) => {
584
- const typed = item;
585
- const embedding = typed.embedding;
586
- if (!Array.isArray(embedding)) {
587
- throw new Error("Cloudflare response item did not include an embedding array");
588
- }
589
- const parsed = { embedding };
590
- if (hasOwnProperty(typed, "index")) {
591
- parsed.index = typed.index;
592
- }
593
- return parsed;
594
- });
619
+ return results;
595
620
  },
596
621
  options.signal,
597
- [this.#apiToken, this.#accountId, encodeURIComponent(this.#accountId)]
622
+ this.#profile.exactSecrets ?? []
598
623
  );
599
624
  return createEmbeddingBatchResult(
600
625
  this.metadata,
601
626
  intent,
602
- validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "cloudflare")
627
+ validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, this.#profile.provider)
603
628
  );
604
629
  }
605
630
  }
@@ -624,7 +649,16 @@ function createEmbeddingProvider(options = {}) {
624
649
  if (!key) {
625
650
  throw new Error("OPENAI_API_KEY is required for OpenAI embeddings");
626
651
  }
627
- 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);
628
662
  }
629
663
  case "mistral": {
630
664
  const key = getOptionalTrimmedCredential(
@@ -633,7 +667,17 @@ function createEmbeddingProvider(options = {}) {
633
667
  if (!key) {
634
668
  throw new Error("MISTRAL_API_KEY is required for Mistral embeddings");
635
669
  }
636
- 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);
637
681
  }
638
682
  case "cloudflare": {
639
683
  const accountId = getOptionalTrimmedCredential(
@@ -648,13 +692,56 @@ function createEmbeddingProvider(options = {}) {
648
692
  if (!apiToken) {
649
693
  throw new Error("CLOUDFLARE_API_TOKEN is required for Cloudflare embeddings");
650
694
  }
651
- 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);
652
733
  }
653
734
  }
654
735
  }
655
736
  function getEmbeddingProviderMetadata(options = {}) {
656
737
  const provider = resolveProviderName(options.provider);
657
- 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);
658
745
  }
659
746
  async function generateEmbeddings(texts, options = {}) {
660
747
  const maxLength = getPositiveInteger(options.maxLength ?? DEFAULT_MAX_LENGTH, "maxLength");
@@ -824,7 +911,7 @@ async function insertDocument(client, document, quotedTableName) {
824
911
  ]
825
912
  });
826
913
  }
827
- async function createTable(client, tableName = "articles", dimensions = 768) {
914
+ async function createTable(client, tableName = "articles", dimensions = 384) {
828
915
  const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
829
916
  const vectorDimensions = normalizeVectorDimensions(dimensions);
830
917
  const quotedEmbeddingIndexName = quoteSqlIdentifier(`${tableName}_embedding_idx`, "embedding index name");
package/dist/index.d.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  import { Client } from '@libsql/client';
2
2
 
3
- type EmbeddingProvider = 'local' | 'gemini' | 'openai' | 'mistral' | 'cloudflare';
3
+ /**
4
+ * Multi-provider embedding generation
5
+ * Supports local Hugging Face Transformers, Gemini, OpenAI, Mistral,
6
+ * Cloudflare Workers AI, and custom OpenAI-compatible endpoints
7
+ */
8
+ type EmbeddingProvider = 'local' | 'gemini' | 'openai' | 'mistral' | 'cloudflare' | 'openai-compatible';
4
9
  type EmbeddingIntent = 'document' | 'query';
5
10
  type EmbeddingBatchMode = 'native' | 'sequential';
6
11
  interface EmbeddingBatchBehavior {
@@ -33,6 +38,9 @@ interface EmbeddingOptions {
33
38
  apiKey?: string;
34
39
  accountId?: string;
35
40
  apiToken?: string;
41
+ baseUrl?: string;
42
+ model?: string;
43
+ batchSize?: number;
36
44
  dimensions?: number;
37
45
  maxLength?: number;
38
46
  intent?: EmbeddingIntent;