diff --git a/src/lib/ccDiscoveryAliasResolve.ts b/src/lib/ccDiscoveryAliasResolve.ts index 2112affbec..00a0d7ba83 100644 --- a/src/lib/ccDiscoveryAliasResolve.ts +++ b/src/lib/ccDiscoveryAliasResolve.ts @@ -20,6 +20,7 @@ import { } from "@omniroute/open-sse/handlers/chatCore/ccDiscoveryAliasStrip.ts"; import { getModelsByProviderId } from "@omniroute/open-sse/config/providerModels.ts"; import { getRegistryEntry } from "@omniroute/open-sse/config/providerRegistry.ts"; +import { getProviderById, getProviderByAlias } from "@/shared/constants/providers"; import { getCachedProviderNodes } from "@/lib/db/readCache"; import { getComboByName } from "@/lib/db/combos"; import { @@ -136,6 +137,22 @@ export async function resolveCcDiscoveryAliasStripWith( }); } +/** + * True when `prefix` names a provider the router can actually reach. + * + * Deliberately broader than the `open-sse` REGISTRY alone: enterprise-cloud + * providers such as `azure-ai` / `azure-openai` live only in the provider + * CATALOG (src/shared/constants/providers/…) yet route fine, so a registry-only + * check made the request path reject `claude/azure-ai/` ids that the + * catalog had already advertised — see cc-discovery-alias-routable-prefix.test.ts. + */ +export function isRoutableProviderPrefix(prefix: string): boolean { + if (!prefix) return false; + if (getRegistryEntry(prefix) !== null) return true; + if (getProviderById(prefix) !== undefined) return true; + return getProviderByAlias(prefix) !== null; +} + /** * Production entry point: build the real lookups and resolve. Cheap-exit for any * id that does not start with `claude/` (the overwhelmingly common case) so a @@ -169,7 +186,7 @@ export async function resolveCcDiscoveryAliasStrip( const result = await resolveCcDiscoveryAliasStripWith(modelStr, { claudeModelIds, - isRegistryProvider: (prefix) => getRegistryEntry(prefix) !== null, + isRegistryProvider: (prefix) => isRoutableProviderPrefix(prefix), customProviderPrefixes, getCombo: (name) => getComboByName(name), gateGlobal: () => globalEnabled, diff --git a/tests/unit/cc-discovery-alias-routable-prefix.test.ts b/tests/unit/cc-discovery-alias-routable-prefix.test.ts new file mode 100644 index 0000000000..f884a7c17a --- /dev/null +++ b/tests/unit/cc-discovery-alias-routable-prefix.test.ts @@ -0,0 +1,43 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { isRoutableProviderPrefix } from "../../src/lib/ccDiscoveryAliasResolve.ts"; + +/** + * Regression guard for the "listed but rejected" cc-discovery alias mismatch. + * + * The /v1/models catalog mirrors `claude//` ids based purely on + * the alias gate (src/app/api/v1/models/ccAliasPredicate.ts — it does NOT consult + * any provider registry). The request path additionally required the prefix to be + * an `open-sse` REGISTRY entry or an operator-defined custom node. + * + * Enterprise-cloud providers such as `azure-ai` / `azure-openai` live in the + * provider CATALOG (src/shared/constants/providers/apikey/enterprise-cloud.ts) + * and route fine directly (`azure-ai/Phi-4` → 200), but have no `open-sse` + * registry entry. So the catalog advertised `claude/azure-ai/` while the + * request path refused to strip it — the id fell through with `claude` parsed as + * the provider, and every request was routed to the Claude provider instead. + * + * These assertions pin the predicate to "can the router actually reach it", + * which is the property the catalog side already assumes. + */ + +test("catalog-only enterprise-cloud providers are routable (azure-ai regression)", () => { + assert.equal(isRoutableProviderPrefix("azure-ai"), true); + assert.equal(isRoutableProviderPrefix("azure-openai"), true); +}); + +test("open-sse registry providers stay routable", () => { + assert.equal(isRoutableProviderPrefix("openai"), true); + assert.equal(isRoutableProviderPrefix("anthropic"), true); +}); + +test("provider aliases resolve too", () => { + // `azure` is the declared alias of the `azure-openai` catalog entry. + assert.equal(isRoutableProviderPrefix("azure"), true); +}); + +test("an unknown prefix is not routable", () => { + assert.equal(isRoutableProviderPrefix("definitely-not-a-provider-xyz"), false); + assert.equal(isRoutableProviderPrefix(""), false); +});