mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 10:52:17 +03:00
fix(combo): survive a malformed customModels row when building auto/* pools
prepareVirtualAutoComboInputs() reads getCustomModels(providerId) straight into
`for (const m of customModels) if (m.id ...)`. That blob is operator-writable and
is returned as raw parsed JSON, so a null / non-object row threw "Cannot read
properties of null (reading 'id')" and EVERY auto/* combo failed to materialize
("[catalog] Could not materialize built-in auto model auto/<id>"), silently
degrading the whole zero-setup routing surface to its minimal catalog entries.
Filter the rows to objects first, the same way the /v1/models custom-model pass
already does. Regression test in combo-auto-pool-visible-only.test.ts fails with
the original TypeError before the guard.
This commit is contained in:
@@ -685,10 +685,21 @@ export async function prepareVirtualAutoComboInputs(
|
||||
// back to the static catalog only when the user has none. This keeps catalog-only
|
||||
// models (e.g. openrouter/auto) out of every auto/* pool when the operator only
|
||||
// synced a subset (e.g. OpenRouter with importFreeModelsOnly).
|
||||
const [syncedByConnection, customModels] = await Promise.all([
|
||||
const [syncedByConnection, rawCustomModels] = await Promise.all([
|
||||
getSyncedAvailableModelsByConnection(providerId),
|
||||
getCustomModels(providerId),
|
||||
]);
|
||||
// The `customModels` key_value blob is operator-writable and is stored as raw
|
||||
// parsed JSON, so a row can be `null` or a non-object. The catalog builder
|
||||
// already filters those out (catalog.ts, "Add custom models"); without the same
|
||||
// filter here every read below null-derefs and the whole auto/* pool fails to
|
||||
// materialize ("Could not materialize built-in auto model auto/<id>").
|
||||
const customModels: Array<{ id?: string }> = (
|
||||
Array.isArray(rawCustomModels) ? rawCustomModels : []
|
||||
).filter(
|
||||
(model: unknown): model is { id?: string } =>
|
||||
!!model && typeof model === "object" && !Array.isArray(model)
|
||||
);
|
||||
const userVisibleIds = new Set<string>();
|
||||
for (const models of Object.values(syncedByConnection)) {
|
||||
for (const m of models) if (m.id && !hiddenModels?.has(m.id)) userVisibleIds.add(m.id);
|
||||
|
||||
@@ -173,3 +173,40 @@ test("virtual auto-combo pool filters EVERY provider with partial sync, not just
|
||||
"kilocode pool must contain exactly the two synced models"
|
||||
);
|
||||
});
|
||||
|
||||
test("virtual auto-combo pool survives a malformed customModels row", async () => {
|
||||
// The `customModels` key_value blob is operator-writable and is read back as raw
|
||||
// parsed JSON, so a row can be null / a non-object / carry no id. Before the guard
|
||||
// in prepareVirtualAutoComboInputs those rows threw
|
||||
// "Cannot read properties of null (reading 'id')" and EVERY auto/* combo failed to
|
||||
// materialize ("[catalog] Could not materialize built-in auto model auto/<id>").
|
||||
const conn = await providersDb.createProviderConnection({
|
||||
provider: "openai",
|
||||
authType: "apikey",
|
||||
name: "OpenAI",
|
||||
apiKey: "sk-test-openai",
|
||||
});
|
||||
const connectionId = (conn as { id?: string }).id;
|
||||
await modelsDb.replaceSyncedAvailableModelsForConnection("openai", connectionId, [
|
||||
{ id: "gpt-4o-mini", name: "GPT-4o mini", source: "imported" as const },
|
||||
]);
|
||||
core
|
||||
.getDbInstance()
|
||||
.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)")
|
||||
.run(
|
||||
"customModels",
|
||||
"openai",
|
||||
JSON.stringify([null, "not-an-object", { name: "Missing Id" }, { id: "operator-custom" }])
|
||||
);
|
||||
|
||||
const prepared = await virtualFactory.prepareVirtualAutoComboInputs();
|
||||
const openaiCandidates = prepared.regularCandidates.filter((c) => c.provider === "openai");
|
||||
assert.ok(
|
||||
openaiCandidates.some((c) => c.model === "gpt-4o-mini"),
|
||||
"the synced model must still reach the pool despite the malformed custom rows"
|
||||
);
|
||||
assert.ok(
|
||||
openaiCandidates.some((c) => c.model === "operator-custom"),
|
||||
"the one well-formed custom row must still reach the pool"
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user