fix(memory): resolve remote embedding dimensions for reindex (#8074) (#8220)

Use getEmbeddingDimension() in resolveEmbeddingSource so sqlite-vec can
create vec_memories before the first upsert, and abort reindex batches
when ensureReady returns ready=false instead of wasting embed credits.
This commit is contained in:
Prudhvi Vuda
2026-07-23 04:33:33 -04:00
committed by GitHub
parent 71887ae529
commit a29341ff1b
3 changed files with 96 additions and 17 deletions

View File

@@ -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`);
}
}

View File

@@ -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)),

View File

@@ -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({