From 2e533dc8c7864c4f2ab2ae0cfb89dbae1aa8f69c Mon Sep 17 00:00:00 2001 From: NoRain211 <170875756+NoRain211@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:48:34 -0700 Subject: [PATCH 1/8] fix(router): decode routed slugs via modelDisplayNames ids --- src/router.ts | 3 +++ tests/codex-integration/slug-codec.test.ts | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/router.ts b/src/router.ts index d3939298e05..11901318556 100644 --- a/src/router.ts +++ b/src/router.ts @@ -189,6 +189,9 @@ export function knownModelIdsForProvider( .digest("hex"); }); for (const cached of cachedModels ?? []) ids.add(cached.id); + // Display-name keys are operator-declared native ids; they keep a routed slug decodable while + // the live roster is cold or its discovery timed out. + for (const id of Object.keys(prov.modelDisplayNames ?? {})) ids.add(id); for (const model of config?.customModels ?? []) { if (model.provider === provName && model.modelId) ids.add(model.modelId); } diff --git a/tests/codex-integration/slug-codec.test.ts b/tests/codex-integration/slug-codec.test.ts index 3a07001def8..4706502f5d0 100644 --- a/tests/codex-integration/slug-codec.test.ts +++ b/tests/codex-integration/slug-codec.test.ts @@ -301,6 +301,24 @@ describe("routeModel decode (proxy layer)", () => { expect(route.modelId).toBe("vendor/m-2"); }); + test("modelDisplayNames keys decode a provider-qualified slug with a cold live cache", () => { + const config: OcxConfig = { + port: 10100, + defaultProvider: "other", + providers: { + other: { + adapter: "openai-chat", + baseUrl: "https://example.com/v1", + apiKey: "k", + modelDisplayNames: { "vendor/m-3": "Vendor M3" }, + }, + }, + }; + const route = routeModel(config, "other/vendor-m-3"); + expect(route.providerName).toBe("other"); + expect(route.modelId).toBe("vendor/m-3"); + }); + test("knownModelIdsForProvider unions config, registry, and hint-map ids", () => { const ids = knownModelIdsForProvider("zenmux", zenmuxConfig().providers.zenmux!); expect(ids).toContain("moonshotai/kimi-k3-free"); From 719b4ffcdd7334bcf587fa7161d74ea5b04a3ebd Mon Sep 17 00:00:00 2001 From: NoRain211 <170875756+NoRain211@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:12:12 -0700 Subject: [PATCH 2/8] fix(router): use display names only as a last-resort slug decode --- src/router.ts | 10 +++---- tests/codex-integration/slug-codec.test.ts | 35 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/router.ts b/src/router.ts index 11901318556..2936301b995 100644 --- a/src/router.ts +++ b/src/router.ts @@ -30,7 +30,7 @@ import { LEGACY_OPENAI_MULTI_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID, } from "./providers/openai-tiers"; -import { decodeRoutedModelIdOrThrow, encodeRoutedModelId } from "./providers/slug-codec"; +import { decodeRoutedModelId, decodeRoutedModelIdOrThrow, encodeRoutedModelId } from "./providers/slug-codec"; import { effectiveProviderAliasDecision, resolveModelAlias } from "./providers/default-aliases"; import { resolveBlockedModelRedirect } from "./lib/shadow-call"; import { workBuddyCatalogIdentity } from "./providers/workbuddy"; @@ -189,9 +189,6 @@ export function knownModelIdsForProvider( .digest("hex"); }); for (const cached of cachedModels ?? []) ids.add(cached.id); - // Display-name keys are operator-declared native ids; they keep a routed slug decodable while - // the live roster is cold or its discovery timed out. - for (const id of Object.keys(prov.modelDisplayNames ?? {})) ids.add(id); for (const model of config?.customModels ?? []) { if (model.provider === provName && model.modelId) ids.add(model.modelId); } @@ -586,7 +583,10 @@ function routeModelInternal( const decoded = decodeRoutedModelIdOrThrow(requestedModel, known); const nativeModel = known.includes(decoded) ? decoded - : resolveModelAlias(config, prov, known, requestedModel) ?? decoded; + : resolveModelAlias(config, prov, known, requestedModel) + // Last resort while the live roster is cold: display-name keys are operator-declared + // native ids, consulted only here so they never claim aliases or shadow known ids. + ?? decodeRoutedModelId(decoded, Object.keys(prov.modelDisplayNames ?? {})); return routeResult( config, provName, diff --git a/tests/codex-integration/slug-codec.test.ts b/tests/codex-integration/slug-codec.test.ts index 4706502f5d0..1c5033e9058 100644 --- a/tests/codex-integration/slug-codec.test.ts +++ b/tests/codex-integration/slug-codec.test.ts @@ -319,6 +319,41 @@ describe("routeModel decode (proxy layer)", () => { expect(route.modelId).toBe("vendor/m-3"); }); + test("a stale modelDisplayNames key does not shadow a configured id", () => { + const config: OcxConfig = { + port: 10100, + defaultProvider: "other", + providers: { + other: { + adapter: "openai-chat", + baseUrl: "https://example.com/v1", + apiKey: "k", + models: ["a-b"], + modelDisplayNames: { "a/b": "Stale A/B" }, + }, + }, + }; + expect(routeModel(config, "other/a-b").modelId).toBe("a-b"); + }); + + test("a stale modelDisplayNames key does not break an existing default alias", () => { + const config: OcxConfig = { + port: 10100, + defaultProvider: "other", + providers: { + other: { + adapter: "openai-chat", + baseUrl: "https://example.com/v1", + apiKey: "k", + defaultAliases: true, + models: ["vendor/claude-opus-5"], + modelDisplayNames: { "vendor/claude-opus-5-old": "Stale Opus" }, + }, + }, + }; + expect(routeModel(config, "other/opus").modelId).toBe("vendor/claude-opus-5"); + }); + test("knownModelIdsForProvider unions config, registry, and hint-map ids", () => { const ids = knownModelIdsForProvider("zenmux", zenmuxConfig().providers.zenmux!); expect(ids).toContain("moonshotai/kimi-k3-free"); From 8b6fa8def6f51a64cfdc57f43f9bcea136b3e315 Mon Sep 17 00:00:00 2001 From: NoRain211 <170875756+NoRain211@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:47:55 -0700 Subject: [PATCH 3/8] fix(router): keep concrete policy and combo targets exact --- src/router.ts | 8 ++++--- tests/routing/policy-execution.test.ts | 29 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/router.ts b/src/router.ts index 2936301b995..7338f84c514 100644 --- a/src/router.ts +++ b/src/router.ts @@ -584,9 +584,11 @@ function routeModelInternal( const nativeModel = known.includes(decoded) ? decoded : resolveModelAlias(config, prov, known, requestedModel) - // Last resort while the live roster is cold: display-name keys are operator-declared - // native ids, consulted only here so they never claim aliases or shadow known ids. - ?? decodeRoutedModelId(decoded, Object.keys(prov.modelDisplayNames ?? {})); + // Last resort for direct requests while the live roster is cold: display-name keys are + // operator-declared native ids, consulted only here so they never claim aliases or shadow + // known ids. Concrete policy/combo/redirect targets (bypassCombos) keep the exact model + // their evaluator selected, so evidence and eligibility describe what is sent. + ?? (bypassCombos ? decoded : decodeRoutedModelId(decoded, Object.keys(prov.modelDisplayNames ?? {}))); return routeResult( config, provName, diff --git a/tests/routing/policy-execution.test.ts b/tests/routing/policy-execution.test.ts index 96b6405a5b5..3508c5a2594 100644 --- a/tests/routing/policy-execution.test.ts +++ b/tests/routing/policy-execution.test.ts @@ -134,6 +134,35 @@ describe("policy execution (RI-05)", () => { expect(route.modelId).toBe("m1"); }); + test("policy and combo targets keep their exact model when a display name could re-decode it", () => { + const base = baseConfig(); + const config = baseConfig({ + providers: { + ...base.providers, + other: { + adapter: "openai-chat", + baseUrl: "https://other.example/v1", + apiKey: "ko", + modelDisplayNames: { "a/b": "Display-only A/B" }, + }, + }, + combos: { exactcombo: { strategy: "failover", targets: [{ provider: "other", model: "a-b" }] } }, + routingProfiles: { + exact: { + candidates: [{ provider: "other", model: "a-b" }], + unknownEvidence: { capability: "penalize", health: "penalize", quota: "penalize", cost: "penalize" }, + }, + }, + }); + expect(routeModel(config, "other/a-b").modelId).toBe("a/b"); + const policy = routeModel(config, "policy/exact"); + expect(policy.modelId).toBe("a-b"); + expect(policy.routeDecision!.selected.model).toBe("a-b"); + const combo = routeModel(config, "combo/exactcombo"); + expect(combo.modelId).toBe("a-b"); + expect(combo.combo!.target.model).toBe("a-b"); + }); + test("existing explicit, combo, native and default routes are unchanged", () => { const config = baseConfig(); expect(routeModel(config, "a/m1")).toMatchObject({ routeKind: "explicit-provider", providerName: "a", modelId: "m1" }); From a33745a53c8c5bde42a23927cac76d6fad7b10e9 Mon Sep 17 00:00:00 2001 From: NoRain211 <170875756+NoRain211@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:00:57 -0700 Subject: [PATCH 4/8] fix(router): keep retained ids ahead of display-name decode --- src/router.ts | 8 +++++--- tests/codex-integration/slug-codec.test.ts | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/router.ts b/src/router.ts index 7338f84c514..68ae9ca013e 100644 --- a/src/router.ts +++ b/src/router.ts @@ -586,9 +586,11 @@ function routeModelInternal( : resolveModelAlias(config, prov, known, requestedModel) // Last resort for direct requests while the live roster is cold: display-name keys are // operator-declared native ids, consulted only here so they never claim aliases or shadow - // known ids. Concrete policy/combo/redirect targets (bypassCombos) keep the exact model - // their evaluator selected, so evidence and eligibility describe what is sent. - ?? (bypassCombos ? decoded : decodeRoutedModelId(decoded, Object.keys(prov.modelDisplayNames ?? {}))); + // known or retained ids. Concrete policy/combo/redirect targets (bypassCombos) keep the + // exact model their evaluator selected, so evidence and eligibility describe what is sent. + ?? (bypassCombos || prov.retainModels?.includes(decoded) + ? decoded + : decodeRoutedModelId(decoded, Object.keys(prov.modelDisplayNames ?? {}))); return routeResult( config, provName, diff --git a/tests/codex-integration/slug-codec.test.ts b/tests/codex-integration/slug-codec.test.ts index 1c5033e9058..a5e0ae79319 100644 --- a/tests/codex-integration/slug-codec.test.ts +++ b/tests/codex-integration/slug-codec.test.ts @@ -336,6 +336,23 @@ describe("routeModel decode (proxy layer)", () => { expect(routeModel(config, "other/a-b").modelId).toBe("a-b"); }); + test("a stale modelDisplayNames key does not shadow a retained id", () => { + const config: OcxConfig = { + port: 10100, + defaultProvider: "other", + providers: { + other: { + adapter: "openai-chat", + baseUrl: "https://example.com/v1", + apiKey: "k", + retainModels: ["a-b"], + modelDisplayNames: { "a/b": "Stale A/B" }, + }, + }, + }; + expect(routeModel(config, "other/a-b").modelId).toBe("a-b"); + }); + test("a stale modelDisplayNames key does not break an existing default alias", () => { const config: OcxConfig = { port: 10100, From a2cc0d9149db23eb264ac7e57498316c3834c077 Mon Sep 17 00:00:00 2001 From: JUN Date: Fri, 30 Oct 2026 20:10:41 +0900 Subject: [PATCH 5/8] docs(structure): record the modelDisplayNames slug-decode fallback --- structure/runtime.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structure/runtime.md b/structure/runtime.md index e40e2fc0ed0..5621876406b 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -18,7 +18,7 @@ Release packaging, Docker, installed-artifact and service smoke jobs, desktop sh `src/router.ts` attaches one frozen `ResolvedModelPolicy` to every `RouteResult`. Fast observation, persistence and cost provenance follow the [response-tier authority contract](transports/responses.md#response-tier-observation-authority); outbound Fast policy is unchanged. Policy/combo route spreads retain that object. Every initial, fallback, and recovery route is recaptured for the request's original inbound protocol before route-dependent normalization, and all adapter rebuilds consume its recorded adapter. A translated Chat or Anthropic replay therefore cannot inherit a Responses-only default. Credential, account, quota, health, cooldown, and observed transport evidence remain late and cannot widen a captured static limit. -Virtual models are the sole model-identity transition: the ordinary and compact paths preserve the selected public id in diagnostics, rewrite `route.modelId` to the upstream wire id, and atomically replace `route.staticPolicy` before adapter or capability decisions continue. Model aliases are resolved before the route result is built, so their policy is already keyed by the native wire id. `src/router.ts` applies bare `blockedModelRedirects` keys after this resolution; ordinary targets substitute one upstream model id on the selected provider/account, including slash-valued targets, while only targets explicitly naming a different configured provider reroute. Qualified source keys take precedence only for those cross-provider targets. A cross-provider redirect sets `RouteResult.credentialDomainRewrite`, which Responses and Chat header scoping treat like combo/policy routes, so a caller credential for the source route is never forwarded. `src/cli/alias.ts` consumes the JSON output flag before choosing its default list action. `ocx alias --json` and both flag positions around an explicit action preserve the selected read or mutation. Live selector hints obey the [credential-scoped cache contract](catalog.md): a selection change cannot reuse the previous credential's roster to choose an alias target. Passive OAuth observation neither refreshes credentials nor repairs their storage. Cross-provider redirects require an enabled destination with a resolved key, usable stored OAuth account, or local/keyless auth (never source-caller forward auth), and share one visited set and five-edge budget across alias, provider, combo and policy resolution; every policy redirect must end at an eligible declared candidate, including same-provider substitutions and candidate fallback retries, and pinned account selectors reject provider changes. Decision traces name the final provider/model while policy candidates retain selection evidence and combos retain their route kind and redirect reason. +Virtual models are the sole model-identity transition: the ordinary and compact paths preserve the selected public id in diagnostics, rewrite `route.modelId` to the upstream wire id, and atomically replace `route.staticPolicy` before adapter or capability decisions continue. Model aliases are resolved before the route result is built, so their policy is already keyed by the native wire id. `src/router.ts` applies bare `blockedModelRedirects` keys after this resolution; ordinary targets substitute one upstream model id on the selected provider/account, including slash-valued targets, while only targets explicitly naming a different configured provider reroute. Qualified source keys take precedence only for those cross-provider targets. A cross-provider redirect sets `RouteResult.credentialDomainRewrite`, which Responses and Chat header scoping treat like combo/policy routes, so a caller credential for the source route is never forwarded. `src/cli/alias.ts` consumes the JSON output flag before choosing its default list action. `ocx alias --json` and both flag positions around an explicit action preserve the selected read or mutation. Live selector hints obey the [credential-scoped cache contract](catalog.md): a selection change cannot reuse the previous credential's roster to choose an alias target. Passive OAuth observation neither refreshes credentials nor repairs their storage. Cross-provider redirects require an enabled destination with a resolved key, usable stored OAuth account, or local/keyless auth (never source-caller forward auth), and share one visited set and five-edge budget across alias, provider, combo and policy resolution; every policy redirect must end at an eligible declared candidate, including same-provider substitutions and candidate fallback retries, and pinned account selectors reject provider changes. Decision traces name the final provider/model while policy candidates retain selection evidence and combos retain their route kind and redirect reason. An explicit provider-qualified slug decodes against `knownModelIdsForProvider` first, then model aliases. Only when both miss on a direct request does `src/router.ts` decode once more against the provider's `modelDisplayNames` keys, which are operator-declared native ids; this keeps a live-discovery provider such as Baseten routable while its roster cache is cold. The fallback never feeds `knownModelIdsForProvider`, so display-name keys claim no aliases and never shadow a configured or `retainModels` id, and concrete policy, combo and blocked-model redirect targets keep the exact model their evaluator selected (`tests/codex-integration/slug-codec.test.ts`, `tests/routing/policy-execution.test.ts`). Routed Meta Muse requests use the registry-owned [Muse effort and header contract](providers-and-adapters.md); `max` reaches the provider through the existing reasoning mapper. From 1c5ef789ceb1c9a2af7349016fe6b4804acb6c27 Mon Sep 17 00:00:00 2001 From: JUN Date: Fri, 30 Oct 2026 20:22:29 +0900 Subject: [PATCH 6/8] fix(router): try retained ids before display-name keys in the slug fallback An encoded slash-valued retainModels id now decodes ahead of a stale modelDisplayNames key, and the structure contract states the narrower guarantee that concrete policy, combo and redirect targets skip the display-name fallback. Co-authored-by: NoRain211 <170875756+NoRain211@users.noreply.github.com> --- src/router.ts | 15 ++++++++++++--- structure/runtime.md | 2 +- tests/codex-integration/slug-codec.test.ts | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/router.ts b/src/router.ts index 68ae9ca013e..94d29681b73 100644 --- a/src/router.ts +++ b/src/router.ts @@ -200,6 +200,17 @@ export function knownModelIdsForProvider( * (never a caller-supplied string), so the log carries no request content. */ const compactionFallbackWarnings = new Set(); +/** + * Last-resort decode for a direct provider-qualified slug the live roster cannot resolve yet. + * Retained ids win (an encoded slash-valued retained id included), then the operator-declared + * modelDisplayNames keys. Neither list feeds alias generation. + */ +function decodeDisplayNameFallback(prov: OcxProviderConfig, decoded: string): string { + const retained = decodeRoutedModelId(decoded, prov.retainModels ?? []); + if (retained !== decoded || prov.retainModels?.includes(decoded)) return retained; + return decodeRoutedModelId(decoded, Object.keys(prov.modelDisplayNames ?? {})); +} + function warnCompactionDefaultProviderFallbackOnce(providerName: string): void { if (compactionFallbackWarnings.has(providerName)) return; compactionFallbackWarnings.add(providerName); @@ -588,9 +599,7 @@ function routeModelInternal( // operator-declared native ids, consulted only here so they never claim aliases or shadow // known or retained ids. Concrete policy/combo/redirect targets (bypassCombos) keep the // exact model their evaluator selected, so evidence and eligibility describe what is sent. - ?? (bypassCombos || prov.retainModels?.includes(decoded) - ? decoded - : decodeRoutedModelId(decoded, Object.keys(prov.modelDisplayNames ?? {}))); + ?? (bypassCombos ? decoded : decodeDisplayNameFallback(prov, decoded)); return routeResult( config, provName, diff --git a/structure/runtime.md b/structure/runtime.md index 5621876406b..1edfd4a9a14 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -18,7 +18,7 @@ Release packaging, Docker, installed-artifact and service smoke jobs, desktop sh `src/router.ts` attaches one frozen `ResolvedModelPolicy` to every `RouteResult`. Fast observation, persistence and cost provenance follow the [response-tier authority contract](transports/responses.md#response-tier-observation-authority); outbound Fast policy is unchanged. Policy/combo route spreads retain that object. Every initial, fallback, and recovery route is recaptured for the request's original inbound protocol before route-dependent normalization, and all adapter rebuilds consume its recorded adapter. A translated Chat or Anthropic replay therefore cannot inherit a Responses-only default. Credential, account, quota, health, cooldown, and observed transport evidence remain late and cannot widen a captured static limit. -Virtual models are the sole model-identity transition: the ordinary and compact paths preserve the selected public id in diagnostics, rewrite `route.modelId` to the upstream wire id, and atomically replace `route.staticPolicy` before adapter or capability decisions continue. Model aliases are resolved before the route result is built, so their policy is already keyed by the native wire id. `src/router.ts` applies bare `blockedModelRedirects` keys after this resolution; ordinary targets substitute one upstream model id on the selected provider/account, including slash-valued targets, while only targets explicitly naming a different configured provider reroute. Qualified source keys take precedence only for those cross-provider targets. A cross-provider redirect sets `RouteResult.credentialDomainRewrite`, which Responses and Chat header scoping treat like combo/policy routes, so a caller credential for the source route is never forwarded. `src/cli/alias.ts` consumes the JSON output flag before choosing its default list action. `ocx alias --json` and both flag positions around an explicit action preserve the selected read or mutation. Live selector hints obey the [credential-scoped cache contract](catalog.md): a selection change cannot reuse the previous credential's roster to choose an alias target. Passive OAuth observation neither refreshes credentials nor repairs their storage. Cross-provider redirects require an enabled destination with a resolved key, usable stored OAuth account, or local/keyless auth (never source-caller forward auth), and share one visited set and five-edge budget across alias, provider, combo and policy resolution; every policy redirect must end at an eligible declared candidate, including same-provider substitutions and candidate fallback retries, and pinned account selectors reject provider changes. Decision traces name the final provider/model while policy candidates retain selection evidence and combos retain their route kind and redirect reason. An explicit provider-qualified slug decodes against `knownModelIdsForProvider` first, then model aliases. Only when both miss on a direct request does `src/router.ts` decode once more against the provider's `modelDisplayNames` keys, which are operator-declared native ids; this keeps a live-discovery provider such as Baseten routable while its roster cache is cold. The fallback never feeds `knownModelIdsForProvider`, so display-name keys claim no aliases and never shadow a configured or `retainModels` id, and concrete policy, combo and blocked-model redirect targets keep the exact model their evaluator selected (`tests/codex-integration/slug-codec.test.ts`, `tests/routing/policy-execution.test.ts`). +Virtual models are the sole model-identity transition: the ordinary and compact paths preserve the selected public id in diagnostics, rewrite `route.modelId` to the upstream wire id, and atomically replace `route.staticPolicy` before adapter or capability decisions continue. Model aliases are resolved before the route result is built, so their policy is already keyed by the native wire id. `src/router.ts` applies bare `blockedModelRedirects` keys after this resolution; ordinary targets substitute one upstream model id on the selected provider/account, including slash-valued targets, while only targets explicitly naming a different configured provider reroute. Qualified source keys take precedence only for those cross-provider targets. A cross-provider redirect sets `RouteResult.credentialDomainRewrite`, which Responses and Chat header scoping treat like combo/policy routes, so a caller credential for the source route is never forwarded. `src/cli/alias.ts` consumes the JSON output flag before choosing its default list action. `ocx alias --json` and both flag positions around an explicit action preserve the selected read or mutation. Live selector hints obey the [credential-scoped cache contract](catalog.md): a selection change cannot reuse the previous credential's roster to choose an alias target. Passive OAuth observation neither refreshes credentials nor repairs their storage. Cross-provider redirects require an enabled destination with a resolved key, usable stored OAuth account, or local/keyless auth (never source-caller forward auth), and share one visited set and five-edge budget across alias, provider, combo and policy resolution; every policy redirect must end at an eligible declared candidate, including same-provider substitutions and candidate fallback retries, and pinned account selectors reject provider changes. Decision traces name the final provider/model while policy candidates retain selection evidence and combos retain their route kind and redirect reason. An explicit provider-qualified slug decodes against `knownModelIdsForProvider` first, then model aliases. Only when both miss on a direct request does `src/router.ts` decode once more against the provider's `modelDisplayNames` keys, which are operator-declared native ids; this keeps a live-discovery provider such as Baseten routable while its roster cache is cold. The fallback never feeds `knownModelIdsForProvider`, so display-name keys claim no aliases and never shadow a configured id; `retainModels` ids (an encoded slash-valued one included) are tried before display-name keys, and concrete policy, combo and blocked-model redirect targets skip the display-name fallback (`tests/codex-integration/slug-codec.test.ts`, `tests/routing/policy-execution.test.ts`). Routed Meta Muse requests use the registry-owned [Muse effort and header contract](providers-and-adapters.md); `max` reaches the provider through the existing reasoning mapper. diff --git a/tests/codex-integration/slug-codec.test.ts b/tests/codex-integration/slug-codec.test.ts index a5e0ae79319..e0ec08c278a 100644 --- a/tests/codex-integration/slug-codec.test.ts +++ b/tests/codex-integration/slug-codec.test.ts @@ -353,6 +353,23 @@ describe("routeModel decode (proxy layer)", () => { expect(routeModel(config, "other/a-b").modelId).toBe("a-b"); }); + test("an encoded slash-valued retained id wins over a stale display-name key", () => { + const config: OcxConfig = { + port: 10100, + defaultProvider: "other", + providers: { + other: { + adapter: "openai-chat", + baseUrl: "https://example.com/v1", + apiKey: "k", + retainModels: ["a/b"], + modelDisplayNames: { "a-b": "Stale A-B" }, + }, + }, + }; + expect(routeModel(config, "other/a-b").modelId).toBe("a/b"); + }); + test("a stale modelDisplayNames key does not break an existing default alias", () => { const config: OcxConfig = { port: 10100, From 92b9dc6f01dd88c30cdf6d81f45f017a4ed55463 Mon Sep 17 00:00:00 2001 From: JUN Date: Fri, 30 Oct 2026 20:24:26 +0900 Subject: [PATCH 7/8] fix(router): let an ambiguous retained claim end the display-name fallback Co-authored-by: NoRain211 <170875756+NoRain211@users.noreply.github.com> --- src/router.ts | 7 +++++-- tests/codex-integration/slug-codec.test.ts | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/router.ts b/src/router.ts index 94d29681b73..b9f4b85ae99 100644 --- a/src/router.ts +++ b/src/router.ts @@ -206,8 +206,11 @@ const compactionFallbackWarnings = new Set(); * modelDisplayNames keys. Neither list feeds alias generation. */ function decodeDisplayNameFallback(prov: OcxProviderConfig, decoded: string): string { - const retained = decodeRoutedModelId(decoded, prov.retainModels ?? []); - if (retained !== decoded || prov.retainModels?.includes(decoded)) return retained; + const retained = prov.retainModels ?? []; + // Any retained claim, even an ambiguous one the codec refuses to guess, ends the fallback. + if (retained.some(id => id === decoded || encodeRoutedModelId(id) === decoded)) { + return decodeRoutedModelId(decoded, retained); + } return decodeRoutedModelId(decoded, Object.keys(prov.modelDisplayNames ?? {})); } diff --git a/tests/codex-integration/slug-codec.test.ts b/tests/codex-integration/slug-codec.test.ts index e0ec08c278a..1f521e3ebfc 100644 --- a/tests/codex-integration/slug-codec.test.ts +++ b/tests/codex-integration/slug-codec.test.ts @@ -368,6 +368,9 @@ describe("routeModel decode (proxy layer)", () => { }, }; expect(routeModel(config, "other/a-b").modelId).toBe("a/b"); + config.providers.other!.retainModels = ["x/y-z", "x-y/z"]; + config.providers.other!.modelDisplayNames = { "x/y-z": "Stale" }; + expect(routeModel(config, "other/x-y-z").modelId).toBe("x-y-z"); }); test("a stale modelDisplayNames key does not break an existing default alias", () => { From 8677c9da459f42bf249e2dd66e12d2b2f7999f14 Mon Sep 17 00:00:00 2001 From: JUN Date: Fri, 30 Oct 2026 20:27:18 +0900 Subject: [PATCH 8/8] docs(providers): note the modelDisplayNames slug-decode fallback Co-authored-by: NoRain211 <170875756+NoRain211@users.noreply.github.com> --- .../src/content/docs/ja/reference/configuration/providers.md | 2 +- .../src/content/docs/ko/reference/configuration/providers.md | 2 +- docs-site/src/content/docs/reference/configuration/providers.md | 2 +- .../src/content/docs/ru/reference/configuration/providers.md | 2 +- .../src/content/docs/zh-cn/reference/configuration/providers.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs-site/src/content/docs/ja/reference/configuration/providers.md b/docs-site/src/content/docs/ja/reference/configuration/providers.md index 616cb1e164e..8705a8b7c64 100644 --- a/docs-site/src/content/docs/ja/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ja/reference/configuration/providers.md @@ -118,7 +118,7 @@ account を削除しても mapping は保持され、同じ id を再追加す | `models?` | `string[]` | 初期/フォールバックモデル一覧。`liveModels: false` で `models` が空でなければ、その後に `retainModels` を追加します。`models` が空または省略されている場合は、設定済みの `defaultModel`、`retainModels` の順に初期一覧を作り、重複 ID は最初の出現だけを残します。 | | `liveModels?` | `boolean` |開始/同期時にライブ カタログをフェッチします (デフォルトは `true`)。カスタムプロバイダーは `${baseUrl}/models` を使用します。組み込みはレジストリ URL とフィルターを使用する場合があります。 | | `selectedModels?` | `string[]` |検出後のカタログ許可リスト。空でない場合は、それらの ID のみが公開されます。空または省略すると、検出されたすべてのモデルが公開されます。 | -| `modelDisplayNames?` | `Record` | このプロバイダーの正確なネイティブモデル ID をキーにした、永続的な表示専用ラベルです。大文字と小文字は区別されます。ラベルはプロバイダーカタログのメタデータより優先され、認証、アダプター、ルーティング、課金、上流リクエストには影響しません。マップは検出上限と同じ 2,000 件までです。 | +| `modelDisplayNames?` | `Record` | このプロバイダーの正確なネイティブモデル ID をキーにした、永続的な表示専用ラベルです。大文字と小文字は区別されます。ラベルはプロバイダーカタログのメタデータより優先され、認証、アダプター、ルーティング、課金、上流リクエストには影響しません。例外として、直接の `provider/model` セレクターが既知の ID、エイリアス、`retainModels` のいずれにも一致しない場合(ライブ一覧がまだ取得されていない間など)、エンコード形が一致するキーをネイティブ ID として送信します。マップは検出上限と同じ 2,000 件までです。 | | `contextWindow?` | `number` | アップストリームのメタデータが無い場合に使うプロバイダー全体のコンテキスト値。メタデータがある場合は上限として働き、より小さいライブ値をそのまま残します。Models ダッシュボードでは `providerContextCaps` とは別に設定します。 | | `modelContextWindows?` | `Record` | モデルごとのコンテキスト値および上限。`contextWindow` より優先され、ウィンドウが不明なら設定値を使い、より小さいライブメタデータがあればそちらが優先されます。 | | `inferContextWindow?` | `boolean` | カスタムプロバイダーのみ、既定は無効。`true` で欠けたウィンドウを同梱モデルメタデータから補完します。推定値であり、中継の容量を検証したものではありません。 | diff --git a/docs-site/src/content/docs/ko/reference/configuration/providers.md b/docs-site/src/content/docs/ko/reference/configuration/providers.md index b454ff2022d..0bb995ed1ef 100644 --- a/docs-site/src/content/docs/ko/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ko/reference/configuration/providers.md @@ -118,7 +118,7 @@ managed map을 활성화하면 privacy-safe selector를 만들고, 이후 계정 | `models?` | `string[]` | 초기/폴백 모델 목록입니다. `liveModels: false`에서 `models`가 비어 있지 않으면 `models`, `retainModels` 순으로 구성합니다. `models`가 비어 있거나 생략되면 설정된 `defaultModel`, `retainModels` 순으로 구성하고, 중복 ID는 처음 나온 항목만 남깁니다. | | `liveModels?` | `boolean` | 시작 또는 동기화 시 라이브 카탈로그를 가져옵니다. 기본값은 `true`입니다. 사용자 지정 공급자는 `${baseUrl}/models`를 사용하고, 내장은 레지스트리 URL을 사용한 뒤 필터링할 수 있습니다. | | `selectedModels?` | `string[]` | 발견 후 카탈로그 허용 목록입니다. 값이 비어 있지 않으면 그 id만 노출하고, 비어 있거나 생략하면 발견된 모델을 모두 노출합니다. | -| `modelDisplayNames?` | `Record` | 이 공급자의 정확한 네이티브 모델 id를 키로 쓰는 영구 표시 전용 이름입니다. 키는 대소문자를 구분합니다. 이름은 공급자 카탈로그 메타데이터보다 우선하며 인증, 어댑터, 라우팅, 청구 또는 업스트림 요청을 바꾸지 않습니다. 맵은 발견 한도와 같은 최대 2,000개 항목을 가질 수 있습니다. | +| `modelDisplayNames?` | `Record` | 이 공급자의 정확한 네이티브 모델 id를 키로 쓰는 영구 표시 전용 이름입니다. 키는 대소문자를 구분합니다. 이름은 공급자 카탈로그 메타데이터보다 우선하며 인증, 어댑터, 라우팅, 청구 또는 업스트림 요청을 바꾸지 않습니다. 단, 직접 `provider/model` 선택자가 알려진 id, 별칭, `retainModels` 어디에도 맞지 않으면(예: 라이브 목록을 아직 받지 못한 동안) 인코딩 형태가 같은 키를 네이티브 id로 보냅니다. 맵은 발견 한도와 같은 최대 2,000개 항목을 가질 수 있습니다. | | `contextWindow?` | `number` | 업스트림 메타데이터가 없을 때 쓰이는 공급자 전반의 컨텍스트 값입니다. 메타데이터가 있으면 상한으로 동작해 더 작은 라이브 값을 그대로 둡니다. Models 대시보드에서 `providerContextCaps`와 별도로 설정합니다. | | `modelContextWindows?` | `Record` | 모델별 컨텍스트 값이자 상한입니다. `contextWindow`보다 우선하며, 창 크기를 알 수 없으면 설정값을 쓰고 더 작은 라이브 메타데이터가 있으면 그쪽을 따릅니다. | | `inferContextWindow?` | `boolean` | 사용자 지정 공급자 전용이며 기본값은 꺼짐입니다. `true`이면 빠진 창을 번들 모델 메타데이터로 채웁니다. 추정값이며 중계의 실제 용량을 검증하지 않습니다. | diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index f77855f4cc4..ae998f0f119 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -335,7 +335,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity | `liveModels?` | `boolean` | Fetch the live catalog on start/sync (default `true`). Custom providers use `${baseUrl}/models`; built-ins may use a registry URL and filter. | | `selectedModels?` | `string[]` | Catalog allowlist after discovery. Non-empty exposes only those ids; empty or omitted exposes all discovered models. | | `retainModels?` | `string[]` | Ids kept in the catalog even when live discovery omits them. They need not be repeated in `models`. Empty or omitted keeps today's behavior. | -| `modelDisplayNames?` | `Record` | Durable labels used only for display, keyed by this provider's exact upstream model id. Labels win over provider catalog metadata, survive discovery refreshes and provider edits, and never change authentication, adapter behavior, routing, billing, upstream request construction, the routed `provider/model` selector, or the upstream wire model. Keys are exact and case sensitive. Unknown model ids are kept so a temporarily missing model receives its label when it returns. The map accepts at most 2,000 entries, matching the discovery limit. | +| `modelDisplayNames?` | `Record` | Durable labels used only for display, keyed by this provider's exact upstream model id. Labels win over provider catalog metadata, survive discovery refreshes and provider edits, and never change authentication, adapter behavior, billing, upstream request construction, the routed `provider/model` selector, or the wire model of an id the provider already knows. One routing exception: when a direct `provider/model` selector matches no known id, alias or `retainModels` id (for example while a live roster is still cold), a key whose encoded form matches is sent as the native id. Keys are exact and case sensitive. Unknown model ids are kept so a temporarily missing model receives its label when it returns. The map accepts at most 2,000 entries, matching the discovery limit. | | `contextWindow?` | `number` | Provider-wide context fallback when upstream metadata is absent; otherwise a cap that retains smaller live metadata. Registry defaults cannot override a lower configured limit; explicit `modelContextWindows` entries take priority. The Models dashboard exposes this separately from `providerContextCaps`. | | `modelContextWindows?` | `Record` | Per-model context fallbacks/caps. These override `contextWindow`: an unknown window uses the configured value, while smaller live metadata remains authoritative. | | `inferContextWindow?` | `boolean` | Custom providers only, default off. `true` fills a missing window from bundled model-ID metadata ([details](#context-windows-on-custom-providers)); the value is an estimate, not verified relay capacity. | diff --git a/docs-site/src/content/docs/ru/reference/configuration/providers.md b/docs-site/src/content/docs/ru/reference/configuration/providers.md index f8ef70c7a55..af23015f1de 100644 --- a/docs-site/src/content/docs/ru/reference/configuration/providers.md +++ b/docs-site/src/content/docs/ru/reference/configuration/providers.md @@ -131,7 +131,7 @@ cross-route credential fallback не существует. Строки API GPT- | `models?` | `string[]` | Начальный список моделей или список для резервного режима. При `liveModels: false` за непустым `models` следует `retainModels`; если `models` пуст или отсутствует, сначала берётся настроенный `defaultModel`, затем `retainModels`. Для каждого ID сохраняется только первое вхождение. | | `liveModels?` | `boolean` | Получать live-каталог на start/sync (по умолчанию `true`). Custom-провайдеры используют `${baseUrl}/models`; built-in могут использовать registry URL и дополнительно фильтровать результат. | | `selectedModels?` | `string[]` | Allowlist каталога после discovery. Непустой список показывает только эти id; пустой или отсутствующий показывает всё, что было обнаружено. | -| `modelDisplayNames?` | `Record` | Постоянные display-only имена с точным нативным id модели этого провайдера в качестве ключа. Ключи чувствительны к регистру. Имена имеют приоритет над metadata каталога провайдера и не меняют аутентификацию, adapter, routing, billing или upstream-запросы. Карта содержит не более 2 000 записей, как и discovery. | +| `modelDisplayNames?` | `Record` | Постоянные display-only имена с точным нативным id модели этого провайдера в качестве ключа. Ключи чувствительны к регистру. Имена имеют приоритет над metadata каталога провайдера и не меняют аутентификацию, adapter, routing, billing или upstream-запросы. Исключение: если прямой селектор `provider/model` не совпадает ни с известным id, ни с alias, ни с `retainModels` (например, пока live-каталог ещё не загружен), ключ с совпадающей закодированной формой отправляется как нативный id. Карта содержит не более 2 000 записей, как и discovery. | | `contextWindow?` | `number` | Значение контекста для всего провайдера, применяемое когда upstream не отдаёт metadata; при наличии metadata работает как cap и сохраняет более маленькое live-значение. Панель Models настраивает его отдельно от `providerContextCaps`. | | `modelContextWindows?` | `Record` | Значения и cap'ы контекста по отдельным моделям. Перекрывают `contextWindow`: если окно неизвестно, берётся заданное значение, а более маленькая live-metadata остаётся авторитетной. | | `inferContextWindow?` | `boolean` | Только для пользовательских провайдеров, по умолчанию выключено. `true` заполняет отсутствующее окно из встроенных метаданных модели; это оценка, а не проверенная ёмкость посредника. | diff --git a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md index 2703729ead1..e7b199e536d 100644 --- a/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md +++ b/docs-site/src/content/docs/zh-cn/reference/configuration/providers.md @@ -120,7 +120,7 @@ selector,而不是分配一个新名称。 | `models?` | `string[]` | 初始/回退模型列表。`liveModels: false` 时,非空 `models` 后接 `retainModels`;若 `models` 为空或省略,则按已配置的 `defaultModel`、`retainModels` 顺序构建初始列表,重复 ID 仅保留首次出现的位置。 | | `liveModels?` | `boolean` | 启动/同步时获取实时目录(默认 `true`)。自定义提供者使用 `${baseUrl}/models`;内置项可能使用注册表 URL 并进行过滤。 | | `selectedModels?` | `string[]` | 发现之后的目录允许列表。非空时只暴露这些 id;为空或省略时则暴露全部发现到的模型。 | -| `modelDisplayNames?` | `Record` | 持久的仅显示名称,以此提供者的精确原生模型 id 为键。键区分大小写。名称优先于提供者目录元数据,并且不会改变身份验证、适配器、路由、计费或上游请求。该映射最多可包含 2,000 个条目,与发现上限相同。 | +| `modelDisplayNames?` | `Record` | 持久的仅显示名称,以此提供者的精确原生模型 id 为键。键区分大小写。名称优先于提供者目录元数据,并且不会改变身份验证、适配器、路由、计费或上游请求。例外:当直接的 `provider/model` 选择器不匹配任何已知 id、别名或 `retainModels`(例如实时目录尚未获取时),编码形式相同的键会作为原生 id 发送。该映射最多可包含 2,000 个条目,与发现上限相同。 | | `contextWindow?` | `number` | 上游缺少元数据时使用的提供者级上下文数值;有元数据时作为上限,保留更小的实时数值。Models 面板中与 `providerContextCaps` 分开设置。 | | `modelContextWindows?` | `Record` | 按模型设置的上下文数值与上限。优先于 `contextWindow`:窗口未知时采用所配置的数值,而更小的实时元数据仍然优先。 | | `inferContextWindow?` | `boolean` | 仅限自定义渠道,默认关闭。设为 `true` 时用内置模型元数据补全缺失窗口;这是估计值,不代表中转的实测容量。 |