diff --git a/changelog.d/fixes/10857-hide-auto-models-when-routing-disabled.md b/changelog.d/fixes/10857-hide-auto-models-when-routing-disabled.md new file mode 100644 index 0000000000..39da596ee3 --- /dev/null +++ b/changelog.d/fixes/10857-hide-auto-models-when-routing-disabled.md @@ -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 diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index d4797775db..a864ca3608 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -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; diff --git a/tests/unit/catalog-auto-routing-disabled-10831.test.ts b/tests/unit/catalog-auto-routing-disabled-10831.test.ts new file mode 100644 index 0000000000..f2f96ef95f --- /dev/null +++ b/tests/unit/catalog-auto-routing-disabled-10831.test.ts @@ -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> { + 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" + ); +});