libsql-search 0.10.1 → 0.11.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/dist/turso.cjs CHANGED
@@ -1,8 +1,65 @@
1
1
  'use strict';
2
2
 
3
+ const MAX_QUERY_STATEMENT_CACHE_SIZE = 32;
3
4
  function tursoAdapter(database) {
4
5
  assertTursoDatabase(database);
5
- return {
6
+ const queryStatementsBySql = /* @__PURE__ */ new Map();
7
+ const liveQueryStatements = /* @__PURE__ */ new Set();
8
+ let disposed = false;
9
+ let disposePromise;
10
+ const assertUsable = () => {
11
+ if (disposed) {
12
+ throw new Error("This Turso adapter has been disposed");
13
+ }
14
+ };
15
+ const closeQueryStatement = (entry) => {
16
+ entry.closePromise ??= closeStatement(entry.statement).finally(() => {
17
+ liveQueryStatements.delete(entry);
18
+ });
19
+ return entry.closePromise;
20
+ };
21
+ const retireQueryStatement = (entry) => {
22
+ if (queryStatementsBySql.get(entry.sql) === entry) {
23
+ queryStatementsBySql.delete(entry.sql);
24
+ }
25
+ entry.retired = true;
26
+ if (entry.pending === 0) {
27
+ void closeQueryStatement(entry);
28
+ }
29
+ };
30
+ const acquireQueryStatement = (sql) => {
31
+ assertUsable();
32
+ let entry = queryStatementsBySql.get(sql);
33
+ if (entry === void 0) {
34
+ entry = {
35
+ sql,
36
+ statement: database.prepare(sql),
37
+ tail: Promise.resolve(),
38
+ pending: 0,
39
+ retired: false
40
+ };
41
+ queryStatementsBySql.set(sql, entry);
42
+ liveQueryStatements.add(entry);
43
+ if (queryStatementsBySql.size > MAX_QUERY_STATEMENT_CACHE_SIZE) {
44
+ const oldest = queryStatementsBySql.values().next().value;
45
+ if (oldest !== void 0) {
46
+ retireQueryStatement(oldest);
47
+ }
48
+ }
49
+ } else {
50
+ queryStatementsBySql.delete(sql);
51
+ queryStatementsBySql.set(sql, entry);
52
+ }
53
+ entry.pending += 1;
54
+ const turn = entry.tail;
55
+ let release;
56
+ const completion = new Promise((resolve) => {
57
+ release = resolve;
58
+ });
59
+ entry.tail = turn.then(() => completion);
60
+ return { entry, turn, release };
61
+ };
62
+ const adapter = {
6
63
  libsqlSearchAdapter: true,
7
64
  backend: "turso",
8
65
  /**
@@ -12,15 +69,24 @@ function tursoAdapter(database) {
12
69
  */
13
70
  supportsVectorIndex: false,
14
71
  async executeDdl(sql) {
72
+ assertUsable();
15
73
  await database.exec(sql);
16
74
  },
17
75
  async executeQuery(sql, args) {
18
- const statement = database.prepare(sql);
76
+ const { entry, turn, release } = acquireQueryStatement(sql);
77
+ await turn;
19
78
  try {
20
- const rows = await (args === void 0 ? statement.all() : statement.all(args));
79
+ const rows = await (args === void 0 ? entry.statement.all() : entry.statement.all(args));
21
80
  return rows;
81
+ } catch (error) {
82
+ retireQueryStatement(entry);
83
+ throw error;
22
84
  } finally {
23
- closeStatement(statement);
85
+ entry.pending -= 1;
86
+ release();
87
+ if (entry.retired && entry.pending === 0) {
88
+ await closeQueryStatement(entry);
89
+ }
24
90
  }
25
91
  },
26
92
  /**
@@ -37,6 +103,7 @@ function tursoAdapter(database) {
37
103
  * this turns N+1 prepares into exactly 2 regardless of corpus size.
38
104
  */
39
105
  async executeAtomicWrite(statements) {
106
+ assertUsable();
40
107
  const preparedBySql = /* @__PURE__ */ new Map();
41
108
  const prepareOnce = (sql) => {
42
109
  let prepared = preparedBySql.get(sql);
@@ -62,16 +129,32 @@ function tursoAdapter(database) {
62
129
  throw error;
63
130
  } finally {
64
131
  for (const prepared of preparedBySql.values()) {
65
- closeStatement(prepared);
132
+ await closeStatement(prepared);
66
133
  }
67
134
  preparedBySql.clear();
68
135
  }
136
+ },
137
+ dispose() {
138
+ disposePromise ??= (async () => {
139
+ disposed = true;
140
+ for (const entry of [...queryStatementsBySql.values()]) {
141
+ retireQueryStatement(entry);
142
+ }
143
+ await Promise.all(
144
+ [...liveQueryStatements].map(async (entry) => {
145
+ await entry.tail;
146
+ await closeQueryStatement(entry);
147
+ })
148
+ );
149
+ })();
150
+ return disposePromise;
69
151
  }
70
152
  };
153
+ return adapter;
71
154
  }
72
- function closeStatement(statement) {
155
+ async function closeStatement(statement) {
73
156
  try {
74
- statement.close?.();
157
+ await statement.close?.();
75
158
  } catch {
76
159
  }
77
160
  }
package/dist/turso.d.ts CHANGED
@@ -86,11 +86,21 @@ interface DatabaseAdapter {
86
86
  * import { tursoAdapter } from 'libsql-search/turso';
87
87
  * import { createTable, indexContent, search } from 'libsql-search';
88
88
  *
89
- * const client = tursoAdapter(await connect('./local.db'));
89
+ * const database = await connect('./local.db');
90
+ * const client = tursoAdapter(database);
91
+ * const embeddingOptions = {
92
+ * provider: 'openai-compatible' as const,
93
+ * baseUrl: 'https://embeddings.example.com/v1',
94
+ * model: 'bge-large-en-v1.5',
95
+ * dimensions: 1024
96
+ * };
90
97
  *
91
- * await createTable(client);
92
- * await indexContent({ client, contentPath: './content' });
93
- * const results = await search({ client, query: 'vector search' });
98
+ * await createTable(client, 'articles', 1024);
99
+ * await indexContent({ client, contentPath: './content', embeddingOptions });
100
+ * const results = await search({ client, query: 'vector search', embeddingOptions });
101
+ *
102
+ * await client.dispose();
103
+ * await database.close();
94
104
  * ```
95
105
  *
96
106
  * @module libsql-search/turso
@@ -113,8 +123,9 @@ interface TursoStatement {
113
123
  *
114
124
  * Not closing leaks roughly 10 KB of native memory per prepare on
115
125
  * `@tursodatabase/database`, which the garbage collector does not reclaim
116
- * because it is not JavaScript heap. A server calling `search()` per request
117
- * grows without bound until the process is killed.
126
+ * because it is not JavaScript heap. The adapter therefore closes
127
+ * transaction-local statements immediately and query statements on safe LRU
128
+ * eviction or disposal.
118
129
  */
119
130
  close?(): unknown;
120
131
  }
@@ -130,6 +141,16 @@ interface TursoDatabase {
130
141
  exec(sql: string): unknown;
131
142
  prepare(sql: string): TursoStatement;
132
143
  }
144
+ /**
145
+ * A Turso-backed adapter with an explicit prepared-statement disposal hook.
146
+ *
147
+ * `dispose()` is terminal: it waits for queued query calls, closes every
148
+ * cached query statement, and rejects later adapter operations. It does not
149
+ * close the caller-owned {@link TursoDatabase} handle.
150
+ */
151
+ interface TursoAdapter extends DatabaseAdapter {
152
+ dispose(): Promise<void>;
153
+ }
133
154
  /**
134
155
  * Wrap a `@tursodatabase/database` handle so this library's functions can use
135
156
  * it.
@@ -150,7 +171,7 @@ interface TursoDatabase {
150
171
  * This preserves the guarantee that a failed rebuild leaves the previous
151
172
  * index intact.
152
173
  */
153
- declare function tursoAdapter(database: TursoDatabase): DatabaseAdapter;
174
+ declare function tursoAdapter(database: TursoDatabase): TursoAdapter;
154
175
 
155
176
  export { tursoAdapter };
156
- export type { DatabaseAdapter, TursoDatabase, TursoStatement };
177
+ export type { DatabaseAdapter, TursoAdapter, TursoDatabase, TursoStatement };
package/dist/turso.esm.js CHANGED
@@ -1,6 +1,63 @@
1
+ const MAX_QUERY_STATEMENT_CACHE_SIZE = 32;
1
2
  function tursoAdapter(database) {
2
3
  assertTursoDatabase(database);
3
- return {
4
+ const queryStatementsBySql = /* @__PURE__ */ new Map();
5
+ const liveQueryStatements = /* @__PURE__ */ new Set();
6
+ let disposed = false;
7
+ let disposePromise;
8
+ const assertUsable = () => {
9
+ if (disposed) {
10
+ throw new Error("This Turso adapter has been disposed");
11
+ }
12
+ };
13
+ const closeQueryStatement = (entry) => {
14
+ entry.closePromise ??= closeStatement(entry.statement).finally(() => {
15
+ liveQueryStatements.delete(entry);
16
+ });
17
+ return entry.closePromise;
18
+ };
19
+ const retireQueryStatement = (entry) => {
20
+ if (queryStatementsBySql.get(entry.sql) === entry) {
21
+ queryStatementsBySql.delete(entry.sql);
22
+ }
23
+ entry.retired = true;
24
+ if (entry.pending === 0) {
25
+ void closeQueryStatement(entry);
26
+ }
27
+ };
28
+ const acquireQueryStatement = (sql) => {
29
+ assertUsable();
30
+ let entry = queryStatementsBySql.get(sql);
31
+ if (entry === void 0) {
32
+ entry = {
33
+ sql,
34
+ statement: database.prepare(sql),
35
+ tail: Promise.resolve(),
36
+ pending: 0,
37
+ retired: false
38
+ };
39
+ queryStatementsBySql.set(sql, entry);
40
+ liveQueryStatements.add(entry);
41
+ if (queryStatementsBySql.size > MAX_QUERY_STATEMENT_CACHE_SIZE) {
42
+ const oldest = queryStatementsBySql.values().next().value;
43
+ if (oldest !== void 0) {
44
+ retireQueryStatement(oldest);
45
+ }
46
+ }
47
+ } else {
48
+ queryStatementsBySql.delete(sql);
49
+ queryStatementsBySql.set(sql, entry);
50
+ }
51
+ entry.pending += 1;
52
+ const turn = entry.tail;
53
+ let release;
54
+ const completion = new Promise((resolve) => {
55
+ release = resolve;
56
+ });
57
+ entry.tail = turn.then(() => completion);
58
+ return { entry, turn, release };
59
+ };
60
+ const adapter = {
4
61
  libsqlSearchAdapter: true,
5
62
  backend: "turso",
6
63
  /**
@@ -10,15 +67,24 @@ function tursoAdapter(database) {
10
67
  */
11
68
  supportsVectorIndex: false,
12
69
  async executeDdl(sql) {
70
+ assertUsable();
13
71
  await database.exec(sql);
14
72
  },
15
73
  async executeQuery(sql, args) {
16
- const statement = database.prepare(sql);
74
+ const { entry, turn, release } = acquireQueryStatement(sql);
75
+ await turn;
17
76
  try {
18
- const rows = await (args === void 0 ? statement.all() : statement.all(args));
77
+ const rows = await (args === void 0 ? entry.statement.all() : entry.statement.all(args));
19
78
  return rows;
79
+ } catch (error) {
80
+ retireQueryStatement(entry);
81
+ throw error;
20
82
  } finally {
21
- closeStatement(statement);
83
+ entry.pending -= 1;
84
+ release();
85
+ if (entry.retired && entry.pending === 0) {
86
+ await closeQueryStatement(entry);
87
+ }
22
88
  }
23
89
  },
24
90
  /**
@@ -35,6 +101,7 @@ function tursoAdapter(database) {
35
101
  * this turns N+1 prepares into exactly 2 regardless of corpus size.
36
102
  */
37
103
  async executeAtomicWrite(statements) {
104
+ assertUsable();
38
105
  const preparedBySql = /* @__PURE__ */ new Map();
39
106
  const prepareOnce = (sql) => {
40
107
  let prepared = preparedBySql.get(sql);
@@ -60,16 +127,32 @@ function tursoAdapter(database) {
60
127
  throw error;
61
128
  } finally {
62
129
  for (const prepared of preparedBySql.values()) {
63
- closeStatement(prepared);
130
+ await closeStatement(prepared);
64
131
  }
65
132
  preparedBySql.clear();
66
133
  }
134
+ },
135
+ dispose() {
136
+ disposePromise ??= (async () => {
137
+ disposed = true;
138
+ for (const entry of [...queryStatementsBySql.values()]) {
139
+ retireQueryStatement(entry);
140
+ }
141
+ await Promise.all(
142
+ [...liveQueryStatements].map(async (entry) => {
143
+ await entry.tail;
144
+ await closeQueryStatement(entry);
145
+ })
146
+ );
147
+ })();
148
+ return disposePromise;
67
149
  }
68
150
  };
151
+ return adapter;
69
152
  }
70
- function closeStatement(statement) {
153
+ async function closeStatement(statement) {
71
154
  try {
72
- statement.close?.();
155
+ await statement.close?.();
73
156
  } catch {
74
157
  }
75
158
  }
package/docs/API.md CHANGED
@@ -113,7 +113,7 @@ Indexes Markdown files from a directory on disk.
113
113
  interface IndexerOptions {
114
114
  client: Client | DatabaseAdapter;
115
115
  contentPath: string;
116
- embeddingOptions?: EmbeddingOptions;
116
+ embeddingOptions: EmbeddingOptions;
117
117
  fileExtensions?: string[];
118
118
  exclude?: string[];
119
119
  tableName?: string;
@@ -187,7 +187,16 @@ class IndexingError extends Error {
187
187
  import { indexContent, IndexingError } from "libsql-search";
188
188
 
189
189
  try {
190
- await indexContent({ client, contentPath: "./content" });
190
+ await indexContent({
191
+ client,
192
+ contentPath: "./content",
193
+ embeddingOptions: {
194
+ provider: "openai-compatible",
195
+ baseUrl: process.env.EMBEDDING_BASE_URL!,
196
+ model: "bge-large-en-v1.5",
197
+ dimensions: 1024,
198
+ },
199
+ });
191
200
  } catch (error) {
192
201
  if (error instanceof IndexingError) {
193
202
  console.error(error.phase, error.failures);
@@ -212,7 +221,7 @@ interface SearchOptions {
212
221
  query: string;
213
222
  limit?: number;
214
223
  tableName?: string;
215
- embeddingOptions?: EmbeddingOptions;
224
+ embeddingOptions: EmbeddingOptions;
216
225
  candidates?: number;
217
226
  exact?: boolean;
218
227
  }
@@ -246,7 +255,7 @@ Controls how many rows the index returns for the exact re-rank. It must be an in
246
255
 
247
256
  ```ts
248
257
  // Trade query cost for recall on a large corpus
249
- const results = await search({ client, query, limit: 10, candidates: 200 });
258
+ const results = await search({ client, query, embeddingOptions, limit: 10, candidates: 200 });
250
259
  ```
251
260
 
252
261
  `candidates` has no effect when `exact` is `true` — that path scans every row — but it is **still validated**. `search({ exact: true, limit: 10, candidates: 5 })` throws, exactly as it would on the index path. Validity does not depend on which path a call happens to take.
@@ -262,7 +271,7 @@ Related exported constants:
262
271
  Set `exact: true` to bypass the index and score every row in the table.
263
272
 
264
273
  ```ts
265
- const results = await search({ client, query, exact: true });
274
+ const results = await search({ client, query, embeddingOptions, exact: true });
266
275
  ```
267
276
 
268
277
  This is the guaranteed-exact path: it computes `vector_distance_cos` for every row with a non-`NULL` embedding, sorts by `(distance, id)`, and trims to `limit`. Cost grows linearly with table size, so it is intended for small corpora, correctness checks against the index path, and tables that have no vector index.
@@ -330,8 +339,7 @@ All retrieval helpers validate `tableName` before executing SQL, and all of them
330
339
 
331
340
  ```ts
332
341
  interface EmbeddingOptions {
333
- provider?:
334
- | "local"
342
+ provider:
335
343
  | "cloudflare"
336
344
  | "mistral"
337
345
  | "gemini"
@@ -353,7 +361,7 @@ interface EmbeddingOptions {
353
361
 
354
362
  Important option rules:
355
363
 
356
- - `provider` defaults to `local`
364
+ - `provider` is required; every provider is an external service
357
365
  - `maxLength` defaults to `8000`
358
366
  - `timeoutMs` defaults to `30000`
359
367
  - `model` is only used by `openai-compatible`
@@ -364,7 +372,6 @@ Important option rules:
364
372
 
365
373
  Dimension rules:
366
374
 
367
- - local: fixed `384`
368
375
  - Cloudflare: fixed `1024`
369
376
  - Mistral: fixed `1024`
370
377
  - Gemini: default `3072`, allowed integer range `128-3072`
@@ -373,7 +380,7 @@ Dimension rules:
373
380
 
374
381
  See [Provider matrix and credential rules](./PROVIDERS.md) for the canonical provider table.
375
382
 
376
- ### `generateEmbedding(text, options?)`
383
+ ### `generateEmbedding(text, options)`
377
384
 
378
385
  Generates one embedding vector.
379
386
 
@@ -385,15 +392,15 @@ const embedding = await generateEmbedding("deploy docs", {
385
392
  });
386
393
  ```
387
394
 
388
- ### `generateEmbeddings(texts, options?)`
395
+ ### `generateEmbeddings(texts, options)`
389
396
 
390
397
  Generates an ordered batch of embeddings.
391
398
 
392
- - empty batches return `[]` without loading the local model or making a hosted call
399
+ - empty batches return `[]` without configuring credentials or making a provider call
393
400
  - OpenAI batches above `2048` inputs are rejected before network work
394
401
  - `openai-compatible` batches are chunked sequentially according to `batchSize`
395
402
 
396
- ### `createEmbeddingProvider(options?)`
403
+ ### `createEmbeddingProvider(options)`
397
404
 
398
405
  Creates a provider client with immutable metadata and an `embed(texts, options?)` method.
399
406
 
@@ -413,7 +420,7 @@ Provider clients return a rich `EmbeddingBatchResult`; the compatibility helpers
413
420
 
414
421
  Hosted provider clients are scoped to their current options. The library does not reuse a Cloudflare, Mistral, Gemini, or OpenAI client across different credential sets or configurations.
415
422
 
416
- ### `getEmbeddingProviderMetadata(options?)`
423
+ ### `getEmbeddingProviderMetadata(options)`
417
424
 
418
425
  Returns the same metadata exposed by `createEmbeddingProvider(options).metadata` without resolving hosted-provider credentials.
419
426
 
@@ -422,7 +429,6 @@ Metadata shape:
422
429
  ```ts
423
430
  interface EmbeddingProviderMetadata {
424
431
  name:
425
- | "local"
426
432
  | "cloudflare"
427
433
  | "mistral"
428
434
  | "gemini"
@@ -451,7 +457,6 @@ Batch interpretation:
451
457
  interface EmbeddingBatchResult {
452
458
  embeddings: number[][];
453
459
  provider:
454
- | "local"
455
460
  | "cloudflare"
456
461
  | "mistral"
457
462
  | "gemini"
@@ -474,7 +479,7 @@ Validates provider responses before they reach the database:
474
479
 
475
480
  ### `padEmbedding(embedding, targetDimensions)`
476
481
 
477
- Pads or truncates a vector to the target width. This is exported for compatibility and migration workflows, but the current local provider uses its native `384` dimensions rather than padding by default.
482
+ Pads or truncates a vector to the target width. This is exported for compatibility and migration workflows; provider adapters otherwise validate and preserve the vectors returned by their external service.
478
483
 
479
484
  ### `prepareTextForEmbedding(fields)`
480
485
 
package/docs/INDEXING.md CHANGED
@@ -18,13 +18,18 @@ The slug is derived from the file path relative to `contentPath`.
18
18
  `indexContent()` replaces the whole target table:
19
19
 
20
20
  ```ts
21
+ const embeddingOptions = {
22
+ provider: "openai-compatible" as const,
23
+ baseUrl: process.env.EMBEDDING_BASE_URL!,
24
+ model: "bge-large-en-v1.5",
25
+ dimensions: 1024,
26
+ };
27
+
21
28
  await indexContent({
22
29
  client,
23
30
  contentPath: "./content",
24
- tableName: "articles_local_384",
25
- embeddingOptions: {
26
- provider: "local",
27
- },
31
+ tableName: "articles_bge_1024",
32
+ embeddingOptions,
28
33
  });
29
34
  ```
30
35
 
@@ -69,7 +74,7 @@ Both are governed by `failurePolicy` like any other build failure, so they abort
69
74
  import { indexContent, IndexingError } from "libsql-search";
70
75
 
71
76
  try {
72
- await indexContent({ client, contentPath: "./content" });
77
+ await indexContent({ client, contentPath: "./content", embeddingOptions });
73
78
  } catch (error) {
74
79
  if (error instanceof IndexingError) {
75
80
  for (const failure of error.failures) {
@@ -87,6 +92,7 @@ By default one bad file aborts the whole rebuild. To index everything that can b
87
92
  const result = await indexContent({
88
93
  client,
89
94
  contentPath: "./content",
95
+ embeddingOptions,
90
96
  failurePolicy: "skip",
91
97
  });
92
98
 
@@ -105,6 +111,7 @@ An empty source directory throws by default, because silently leaving stale rows
105
111
  await indexContent({
106
112
  client,
107
113
  contentPath: "./content",
114
+ embeddingOptions,
108
115
  allowEmptyIndex: true,
109
116
  });
110
117
  ```
@@ -173,7 +180,6 @@ Many projects wire indexing into a dedicated script and call it before their sit
173
180
 
174
181
  ## Runtime Notes
175
182
 
176
- - local embeddings may download and cache a model on the first run
177
183
  - Node users need `@libsql/client` installed alongside the package, at `^0.15.0 || ^0.17.0`; the packaged build is smoke-tested against both arms (`0.15.15` and `0.17.4`), which covers table and vector-index creation. `batch()` rollback behaves identically on both at the contract level, though its error text differs — see [Version differences](./TROUBLESHOOTING.md#libsqlclient-version-differences). Upgrading the client is not a prerequisite for upgrading this package. Deno/JSR users are not covered by that range and should pin the client themselves — see [Install](../README.md#install)
178
- - hosted providers send indexed or queried text to external services
184
+ - all embedding providers send indexed or queried text to external services; this library never loads a model in-process
179
185
  - the repository validates package build and `deno check`, but indexing still depends on filesystem access
@@ -55,13 +55,6 @@ These presets keep credentials out of the source file while making dimensions an
55
55
 
56
56
  ```ts
57
57
  const providerPresets = {
58
- local: {
59
- tableName: "articles_local_384",
60
- dimensions: 384,
61
- embeddingOptions: {
62
- provider: "local" as const,
63
- },
64
- },
65
58
  cloudflare: {
66
59
  tableName: "articles_cf_bgem3_1024",
67
60
  dimensions: 1024,
@@ -137,9 +130,13 @@ export const POST: APIRoute = async ({ request }) => {
137
130
  client,
138
131
  query,
139
132
  limit,
140
- tableName: "articles_local_384",
133
+ tableName: "articles_tei_1024",
141
134
  embeddingOptions: {
142
- provider: "local",
135
+ provider: "openai-compatible",
136
+ baseUrl: process.env.EMBEDDING_BASE_URL!,
137
+ model: process.env.EMBEDDING_MODEL!,
138
+ dimensions: 1024,
139
+ apiKey: process.env.EMBEDDING_API_KEY,
143
140
  intent: "query",
144
141
  },
145
142
  });
@@ -162,14 +159,14 @@ const client = createClient({
162
159
  });
163
160
 
164
161
  export async function getStaticPaths() {
165
- const articles = await getAllArticles(client, "articles_local_384");
162
+ const articles = await getAllArticles(client, "articles_tei_1024");
166
163
 
167
164
  return articles.map((article) => ({
168
165
  params: { slug: article.slug },
169
166
  }));
170
167
  }
171
168
 
172
- const article = await getArticleBySlug(client, "guides/getting-started", "articles_local_384");
169
+ const article = await getArticleBySlug(client, "guides/getting-started", "articles_tei_1024");
173
170
  ```
174
171
 
175
172
  ## Next.js Route Handler
@@ -191,9 +188,13 @@ export async function POST(request: NextRequest) {
191
188
  client,
192
189
  query,
193
190
  limit,
194
- tableName: "articles_local_384",
191
+ tableName: "articles_tei_1024",
195
192
  embeddingOptions: {
196
- provider: "local",
193
+ provider: "openai-compatible",
194
+ baseUrl: process.env.EMBEDDING_BASE_URL!,
195
+ model: process.env.EMBEDDING_MODEL!,
196
+ dimensions: 1024,
197
+ apiKey: process.env.EMBEDDING_API_KEY,
197
198
  intent: "query",
198
199
  },
199
200
  });
@@ -214,7 +215,7 @@ const client = createClient({
214
215
  });
215
216
 
216
217
  export async function generateStaticParams() {
217
- const articles = await getAllArticles(client, "articles_local_384");
218
+ const articles = await getAllArticles(client, "articles_tei_1024");
218
219
 
219
220
  return articles.map((article) => ({
220
221
  slug: article.slug,
@@ -223,7 +224,7 @@ export async function generateStaticParams() {
223
224
 
224
225
  export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
225
226
  const { slug } = await params;
226
- const article = await getArticleBySlug(client, slug, "articles_local_384");
227
+ const article = await getArticleBySlug(client, slug, "articles_tei_1024");
227
228
 
228
229
  return <article>{article?.title}</article>;
229
230
  }
@@ -241,13 +242,6 @@ const client = createClient({
241
242
  });
242
243
 
243
244
  const providerPresets = {
244
- local: {
245
- tableName: "articles_local_384",
246
- dimensions: 384,
247
- embeddingOptions: {
248
- provider: "local" as const,
249
- },
250
- },
251
245
  cloudflare: {
252
246
  tableName: "articles_cf_bgem3_1024",
253
247
  dimensions: 1024,
@@ -299,7 +293,7 @@ const providerPresets = {
299
293
  },
300
294
  } as const;
301
295
 
302
- const provider = process.env.EMBEDDING_PROVIDER ?? "local";
296
+ const provider = process.env.EMBEDDING_PROVIDER ?? "openai-compatible";
303
297
 
304
298
  if (!(provider in providerPresets)) {
305
299
  throw new Error(
@@ -349,4 +343,4 @@ await generateEmbeddings(["doc one", "doc two"], {
349
343
  });
350
344
  ```
351
345
 
352
- The repository test suite should not require real provider credentials. See [Testing guidance](./TESTING.md) for local model mocks, Gemini SDK mocks, and validation-before-network assertions.
346
+ The repository test suite should not require real provider credentials. See [Testing guidance](./TESTING.md) for embedding-service mocks, Gemini SDK mocks, and validation-before-network assertions.