fix(catalog): stop advertising auto/* when auto routing is disabled (#10857)

Obrigado — /v1/models continuava anunciando 38 IDs auto/* mesmo com autoRoutingEnabled: false, todos garantidos a falhar em tempo de request (HTTP 400). Une a condição de ocultação ao hideAutoCombos já existente sem adicionar uma dimensão nova à cache-key (evita quebrar #10313).

Validação (worktree combinado a partir de origin/release/v3.8.50, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- tests/unit/catalog-auto-routing-disabled-10831.test.ts — 2/2 passando
- Suítes catalog relacionadas (hide-auto-no-think, cache-key-hashing, eventloop-yield) — 9/9 passando
This commit is contained in:
Nguyen Thanh Dat
2026-08-21 01:20:08 +07:00
committed by GitHub
parent a280bfc112
commit 3112304db6
3 changed files with 109 additions and 2 deletions

View File

@@ -0,0 +1 @@
- **fix(catalog):** `/v1/models` no longer advertises the built-in `auto/*` ids while auto routing is disabled — they were listed but rejected at request time with `Auto routing is disabled` ([#10831](https://github.com/diegosouzapw/OmniRoute/issues/10831), [#10857](https://github.com/diegosouzapw/OmniRoute/pull/10857)) — thanks @ntdat812

View File

@@ -189,7 +189,11 @@ export async function getUnifiedModelsResponse(
{ corsHeaders, diagnosticHeaders },
buildCatalogPayload,
{
hideAutoCombos: settingsForAuth?.hideAutoCombos === true,
// #10831: a disabled router hides auto/* just as hideAutoCombos does, so
// the two collapse into one cache dimension — the resulting catalogs are
// identical and do not need separate entries.
hideAutoCombos:
settingsForAuth?.hideAutoCombos === true || settingsForAuth?.autoRoutingEnabled === false,
hideNoThinkVariants: settingsForAuth?.hideNoThinkVariants === true,
}
);
@@ -301,7 +305,11 @@ async function buildUnifiedModelsResponseCore(
// #9418: Opt-in filter — skip the entire auto/* synthesis loop when the operator
// does not want built-in virtual combos advertised in the catalog. User-defined
// combos are unaffected; routing still works for ids sent explicitly.
const hideAuto = settings.hideAutoCombos === true;
// #10831: also drop them when auto routing is switched off. Unlike
// hideAutoCombos — which only unadvertises ids that still route when sent
// explicitly — a disabled router rejects every auto/* id with a 400, so
// listing them offers the client a choice that cannot succeed.
const hideAuto = settings.hideAutoCombos === true || settings.autoRoutingEnabled === false;
const shouldHidePaid = (providerKey: string, modelId: string, pricing?: unknown): boolean => {
if (!hidePaid) return false;
const provider = aliasToProviderId[providerKey] || providerKey;

View File

@@ -0,0 +1,98 @@
/**
* #10831 — when auto routing is switched off, `auto/*` ids must not be
* advertised in `/v1/models`.
*
* Unlike `hideAutoCombos` (#9418), which only unadvertises ids that still route
* when a client sends them explicitly, `autoRoutingEnabled: false` makes the
* router reject every `auto/*` id with
* "Auto routing is disabled. Enable it in Settings > Routing." (see
* src/sse/handlers/autoRouting.ts). Listing them therefore offers the picker a
* choice that can only fail.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-auto-routing-10831-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const settingsDb = await import("../../src/lib/db/settings.ts");
const providersDb = await import("../../src/lib/db/providers.ts");
const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts");
async function fetchCatalog(): Promise<Array<{ id: string }>> {
const res = await v1ModelsCatalog.getUnifiedModelsResponse(
new Request("http://localhost/api/v1/models", { method: "GET" })
);
if (res.status !== 200) {
const body = await res.text();
assert.fail(`Expected 200, got ${res.status}: ${body.slice(0, 500)}`);
}
const body = (await res.json()) as { data: Array<{ id: string }> };
return body.data;
}
const isAutoId = (m: { id: string }) => m.id.startsWith("auto/");
test.after(() => {
core.resetDbInstance();
try {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
} catch {
/* best-effort */
}
});
test("autoRoutingEnabled=false removes auto/* ids from /v1/models", async () => {
await providersDb.createProviderConnection({
provider: "openai",
authType: "apikey",
name: "openai-main",
apiKey: "sk-test",
isActive: true,
});
// Baseline: routing on, ids advertised.
await settingsDb.updateSettings({ autoRoutingEnabled: true, hideAutoCombos: false });
const on = await fetchCatalog();
const autoWhenOn = on.filter(isAutoId).map((m) => m.id);
assert.equal(
autoWhenOn.length > 0,
true,
`expected auto/* ids while auto routing is enabled, got ${autoWhenOn.length}`
);
// Routing off: none may remain.
await settingsDb.updateSettings({ autoRoutingEnabled: false, hideAutoCombos: false });
const off = await fetchCatalog();
const leaked = off.filter(isAutoId).map((m) => m.id);
assert.deepEqual(
leaked,
[],
`auto/* ids leaked while auto routing is disabled: ${leaked.join(", ")}`
);
// Everything else must survive — this is a filter, not a catalog wipe.
const hasProviderModel = off.some((m) => m.id.startsWith("openai/") || m.id.startsWith("oa/"));
assert.equal(hasProviderModel, true, "provider models must remain when auto routing is disabled");
});
test("re-enabling auto routing brings auto/* ids back (cache key varies on the flag)", async () => {
await settingsDb.updateSettings({ autoRoutingEnabled: false, hideAutoCombos: false });
const off = await fetchCatalog();
assert.deepEqual(
off.filter(isAutoId).map((m) => m.id),
[]
);
await settingsDb.updateSettings({ autoRoutingEnabled: true, hideAutoCombos: false });
const back = await fetchCatalog();
assert.equal(
back.filter(isAutoId).length > 0,
true,
"auto/* ids must return once auto routing is re-enabled — a stale cached catalog would fail here"
);
});