diff --git a/src/lib/memory/embedding/index.ts b/src/lib/memory/embedding/index.ts index 0033e6c9e6..2713847d7c 100644 --- a/src/lib/memory/embedding/index.ts +++ b/src/lib/memory/embedding/index.ts @@ -1,6 +1,7 @@ import { EMBEDDING_PROVIDERS, buildDynamicEmbeddingProvider, + getEmbeddingDimension, type EmbeddingProviderNodeRow, } from "@omniroute/open-sse/config/embeddingRegistry.ts"; import { getProviderCredentials } from "@/sse/services/auth"; @@ -40,6 +41,31 @@ function makeSignature( return `${source ?? "null"}:${model ?? "null"}:${dim ?? "null"}`; } +/** + * Look up a remote model's vector size from the embedding registry. + * Returns null when the model is unknown / has no recorded dimensions so + * callers can keep the lazy-probe path (#8074). + */ +function resolveRemoteDimensions(model: string): number | null { + const dim = getEmbeddingDimension(model); + return typeof dim === "number" ? dim : null; +} + +/** Build the remote EmbeddingResolution used by both explicit + auto paths. */ +function remoteResolution(model: string, reasonPrefix: string): EmbeddingResolution { + const dimensions = resolveRemoteDimensions(model); + return { + source: "remote", + model, + dimensions, + signature: makeSignature("remote", model, dimensions), + reason: + dimensions !== null + ? `${reasonPrefix}: ${model} (dim=${dimensions})` + : `${reasonPrefix}: ${model} (dim=unknown, will probe at embed time)`, + }; +} + /** * Resolve which embedding source is active for the given settings (D4). * Pure: no heavy I/O. Provider key check done via synchronous registry lookup. @@ -61,14 +87,9 @@ export function resolveEmbeddingSource(settings: MemorySettingsExtended): Embedd } // We can't do async here, so we report it as potentially available // and the caller will attempt embed + get no_key error on failure. - // For resolution purposes, mark as remote (will fail at embed time if no key). - return { - source: "remote", - model, - dimensions: null, - signature: makeSignature("remote", model, null), - reason: `remote provider configured: ${model}`, - }; + // Dimensions come from the embedding registry when known so sqlite-vec + // can create `vec_memories` before the first embed (#8074). + return remoteResolution(model, "remote provider configured"); } if (source === "static") { @@ -123,13 +144,8 @@ export function resolveEmbeddingSource(settings: MemorySettingsExtended): Embedd // We defer the actual hasKey check to listEmbeddingProviders (async). // For resolveEmbeddingSource (sync), we report "possibly remote" when model is set. // If no key, embed will return EmbeddingError{reason:"no_key"}. - return { - source: "remote", - model: providerModel, - dimensions: null, - signature: makeSignature("remote", providerModel, null), - reason: `auto: provider ${providerId} configured`, - }; + // Dimensions are resolved from the registry when known (#8074). + return remoteResolution(providerModel, `auto: provider ${providerId} configured`); } } diff --git a/src/lib/memory/reindex.ts b/src/lib/memory/reindex.ts index 1b9175fb5b..da7317b10a 100644 --- a/src/lib/memory/reindex.ts +++ b/src/lib/memory/reindex.ts @@ -50,9 +50,21 @@ export async function runReindexBatch( return { processed: 0, errors: 0 }; } - // Ensure the vector table is ready before processing + // Ensure the vector table is ready before processing. ensureReady() returns + // `{ ready: false }` (without throwing) when dimensions are still unknown — + // abort the batch in that case so we don't burn embed credits upserting into + // a missing `vec_memories` table (#8074). try { - await vec.ensureReady(resolution); + const ready = await vec.ensureReady(resolution); + if (!ready.ready) { + log.warn("memory.reindex.ensure_ready.skipped", { + reason: ready.reason, + pending: queue.length, + model: resolution.model, + dimensions: resolution.dimensions, + }); + return { processed: 0, errors: 0 }; + } } catch (err: unknown) { log.warn("memory.reindex.ensure_ready.fail", { error: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)), diff --git a/tests/unit/memory-embedding-resolve.test.ts b/tests/unit/memory-embedding-resolve.test.ts index 7a38d6d0af..590ee309b1 100644 --- a/tests/unit/memory-embedding-resolve.test.ts +++ b/tests/unit/memory-embedding-resolve.test.ts @@ -93,6 +93,57 @@ describe("resolveEmbeddingSource", () => { assert.strictEqual(res.model, "openai/text-embedding-3-small"); }); + // #8074 — remote resolution must surface registry dimensions so sqlite-vec + // can create `vec_memories` before the first embed/upsert. + it("explicit 'remote' + known registry model => dimensions from embeddingRegistry (#8074)", () => { + const res = resolveEmbeddingSource( + makeSettings({ + embeddingSource: "remote", + embeddingProviderModel: "openai/text-embedding-3-small", + }) + ); + assert.strictEqual(res.dimensions, 1536); + assert.ok( + res.signature.endsWith(":1536"), + `signature should include dim=1536, got: ${res.signature}` + ); + assert.ok( + res.reason.includes("dim=1536"), + `reason should mention dim=1536, got: ${res.reason}` + ); + }); + + it("auto + known nvidia model => dimensions from embeddingRegistry (#8074)", () => { + const res = resolveEmbeddingSource( + makeSettings({ + embeddingSource: "auto", + embeddingProviderModel: "nvidia/nv-embedqa-e5-v5", + }) + ); + assert.strictEqual(res.source, "remote"); + assert.strictEqual(res.dimensions, 1024); + assert.ok( + res.signature.endsWith(":1024"), + `signature should include dim=1024, got: ${res.signature}` + ); + }); + + it("explicit 'remote' + unknown custom model => dimensions null (lazy probe) (#8074)", () => { + const res = resolveEmbeddingSource( + makeSettings({ + embeddingSource: "remote", + // Not in EMBEDDING_PROVIDERS — keep the lazy-probe path. + embeddingProviderModel: "openai-compatible-local/my-custom-embed", + }) + ); + assert.strictEqual(res.source, "remote"); + assert.strictEqual(res.dimensions, null); + assert.ok( + res.reason.includes("dim=unknown"), + `reason should mention dim=unknown, got: ${res.reason}` + ); + }); + it("explicit 'static' + staticEnabled=true => source static", () => { const res = resolveEmbeddingSource( makeSettings({