mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
@@ -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`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user