From 3176944ed9ac78495c89057d251ccad4c1f500c9 Mon Sep 17 00:00:00 2001 From: Terry Tan Date: Sat, 29 Aug 2026 18:55:37 -0700 Subject: [PATCH 1/4] fix(catalog): display effective model aliases in Codex picker --- .../src/content/docs/reference/configuration.md | 2 +- src/codex/catalog/provider-fetch.ts | 12 +++++++++++- structure/03_catalog-and-subagents.md | 5 +++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docs-site/src/content/docs/reference/configuration.md b/docs-site/src/content/docs/reference/configuration.md index b79db69fe1..29e0c59053 100644 --- a/docs-site/src/content/docs/reference/configuration.md +++ b/docs-site/src/content/docs/reference/configuration.md @@ -46,7 +46,7 @@ Aliases are optional short request names. They never change the native model id } ``` -Aliases match case-insensitively. A model alias works as `or/opus` or, when globally unique, bare `opus`; an ambiguous bare alias reports its qualified candidates. A provider's `defaultAliases` value overrides `defaultModelAliases`. Built-ins are skipped when multiple models in one provider match the same pattern. +Aliases match case-insensitively. A model alias works as `or/opus` or, when globally unique, bare `opus`; an ambiguous bare alias reports its qualified candidates. Codex model pickers show the qualified alias while preserving the canonical `provider/model` routing id. A provider's `defaultAliases` value overrides `defaultModelAliases`. Built-ins are skipped when multiple models in one provider match the same pattern. Valid values in `config.json` override built-in defaults. Missing optional fields use the defaults documented on the domain pages. `OPENCODEX_HOME` takes precedence over the default configuration diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts index 6ca2c5a700..2cd2e66f17 100644 --- a/src/codex/catalog/provider-fetch.ts +++ b/src/codex/catalog/provider-fetch.ts @@ -42,6 +42,7 @@ import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryT import { parseAntigravityAvailableModels, registerAntigravityDiscoveredWireModels } from "../../providers/antigravity-models"; import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap"; import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget"; +import { effectiveModelAliases } from "../../providers/default-aliases"; import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec"; import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity"; import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery"; @@ -2151,6 +2152,12 @@ async function gatherRoutedModelsUncached( // Custom rows override discovered rows that encode to the same Codex-facing slug. const customKeys = new Set(customModels.map(c => routedSlug(c.provider, c.id))); const deduped = all.filter(m => !customKeys.has(routedSlug(m.provider, m.id))); + const models = [...deduped, ...customModels]; + // ponytail: catalog-scale scan; index ids by provider if catalog growth makes this measurable. + const aliasDisplayNames = new Map(activeProviders.flatMap(({ name, provider }) => ( + [...effectiveModelAliases(config, provider, models.filter(model => model.provider === name).map(model => model.id))] + .map(([id, { alias }]) => [`${name}/${id}`, `${provider.alias || name}/${alias}`] as const) + ))); const providerModelOutcomes = providerResults.map(result => ( result.outcome.provider === OPENAI_API_PROVIDER_ID && capture.openAiApiPolicy.state === "captured" @@ -2159,7 +2166,10 @@ async function gatherRoutedModelsUncached( : result.outcome )); return { - models: [...deduped, ...customModels], + models: models.map(model => { + const displayName = aliasDisplayNames.get(`${model.provider}/${model.id}`); + return displayName && !model.displayName ? { ...model, displayName } : model; + }), comboOmissions: localOmissions, providerAuthOutcomes: localProviderAuthOutcomes, providerModelOutcomes, diff --git a/structure/03_catalog-and-subagents.md b/structure/03_catalog-and-subagents.md index 5db21f986e..3260fe9de1 100644 --- a/structure/03_catalog-and-subagents.md +++ b/structure/03_catalog-and-subagents.md @@ -101,8 +101,9 @@ liveness contract. ## Entry shape Routed entries keep Codex-required metadata such as reasoning levels, shell type, API support flags, -base instructions, modalities, auto-compact fields, and strict parser booleans. The public slug and -display name use `provider/model`. +base instructions, modalities, auto-compact fields, and strict parser booleans. The public slug uses +the canonical `provider/model`; its display name uses the qualified provider/model alias when +configured, without changing the routing slug. ## Native passthrough From 4bf80514a0d6c76c529ac69cc0eb82f927757e84 Mon Sep 17 00:00:00 2001 From: Terry Tan Date: Sat, 29 Aug 2026 19:13:38 -0700 Subject: [PATCH 2/4] test(catalog): cover alias picker labels --- tests/codex-catalog.test.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/codex-catalog.test.ts b/tests/codex-catalog.test.ts index 57377ed3b7..ddaecc00e8 100644 --- a/tests/codex-catalog.test.ts +++ b/tests/codex-catalog.test.ts @@ -1921,6 +1921,33 @@ describe("configured CatalogModel displayName -> catalog display_name", () => { } }); + test("provider and model aliases label picker rows without changing routing slugs", async () => { + clearModelCache("google-antigravity"); + try { + const models = await gatherRoutedModels({ + port: 10100, + defaultProvider: "google-antigravity", + providers: { + "google-antigravity": { + baseUrl: "https://example.invalid/v1", + adapter: "openai-chat", + liveModels: false, + models: ["gemini-3.7-flash"], + alias: "ga", + modelAliases: { "gemini-3.7-flash": "g3f" }, + }, + }, + }); + const row = buildCatalogEntries(nativeTemplate(), [], models) + .find(entry => entry.slug === "google-antigravity/gemini-3.7-flash"); + + expect(row?.display_name).toBe("ga/g3f"); + expect(row?.slug).toBe("google-antigravity/gemini-3.7-flash"); + } finally { + clearModelCache("google-antigravity"); + } + }); + test("a custom row clamps its soft budget to the provider max-input ceiling", async () => { const models = await gatherRoutedModels({ port: 10100, From 99f5ca7e385c8c930e2b22726cb0c991fa31585b Mon Sep 17 00:00:00 2001 From: Terry Tan Date: Sun, 30 Aug 2026 16:53:17 -0700 Subject: [PATCH 3/4] test(catalog): cover alias precedence cases --- tests/codex-catalog.test.ts | 61 +++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/codex-catalog.test.ts b/tests/codex-catalog.test.ts index ddaecc00e8..128f8a9105 100644 --- a/tests/codex-catalog.test.ts +++ b/tests/codex-catalog.test.ts @@ -1948,6 +1948,67 @@ describe("configured CatalogModel displayName -> catalog display_name", () => { } }); + test("the issue reproduction uses the effective model alias for the picker label", async () => { + clearModelCache("google-antigravity"); + try { + const models = await gatherRoutedModels({ + port: 10100, + defaultProvider: "google-antigravity", + providers: { + "google-antigravity": { + adapter: "google", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + authMode: "oauth", + liveModels: false, + models: ["gemini-3.7-flash"], + modelAliases: { "gemini-3.7-flash": "gemini-3.7" }, + }, + }, + }); + const row = buildCatalogEntries(nativeTemplate(), [], models) + .find(entry => entry.slug === "google-antigravity/gemini-3.7-flash"); + + expect(row?.display_name).toBe("google-antigravity/gemini-3.7"); + expect(row?.slug).toBe("google-antigravity/gemini-3.7-flash"); + } finally { + clearModelCache("google-antigravity"); + } + }); + + test("an explicit custom displayName wins over an effective model alias", async () => { + clearModelCache("google-antigravity"); + try { + const models = await gatherRoutedModels({ + port: 10100, + defaultProvider: "google-antigravity", + providers: { + "google-antigravity": { + adapter: "google", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + authMode: "oauth", + liveModels: false, + models: ["gemini-3.7-flash"], + modelAliases: { "gemini-3.7-flash": "gemini-3.7" }, + }, + }, + customModels: [{ + id: "custom-gemini", + provider: "google-antigravity", + modelId: "gemini-3.7-flash", + displayName: "My Gemini", + addedAt: "2026-01-01T00:00:00.000Z", + }], + }); + const row = buildCatalogEntries(nativeTemplate(), [], models) + .find(entry => entry.slug === "google-antigravity/gemini-3.7-flash"); + + expect(row?.display_name).toBe("My Gemini"); + expect(row?.slug).toBe("google-antigravity/gemini-3.7-flash"); + } finally { + clearModelCache("google-antigravity"); + } + }); + test("a custom row clamps its soft budget to the provider max-input ceiling", async () => { const models = await gatherRoutedModels({ port: 10100, From 5bea946dff43db7ab7843e8399ee09292e1bebda Mon Sep 17 00:00:00 2001 From: Terry Tan Date: Sun, 30 Aug 2026 17:19:58 -0700 Subject: [PATCH 4/4] fix(catalog): handle alias case drift --- src/codex/catalog/provider-fetch.ts | 17 ++++++-- tests/codex-catalog.test.ts | 62 +++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts index 2cd2e66f17..87afd0bb12 100644 --- a/src/codex/catalog/provider-fetch.ts +++ b/src/codex/catalog/provider-fetch.ts @@ -2154,10 +2154,19 @@ async function gatherRoutedModelsUncached( const deduped = all.filter(m => !customKeys.has(routedSlug(m.provider, m.id))); const models = [...deduped, ...customModels]; // ponytail: catalog-scale scan; index ids by provider if catalog growth makes this measurable. - const aliasDisplayNames = new Map(activeProviders.flatMap(({ name, provider }) => ( - [...effectiveModelAliases(config, provider, models.filter(model => model.provider === name).map(model => model.id))] - .map(([id, { alias }]) => [`${name}/${id}`, `${provider.alias || name}/${alias}`] as const) - ))); + const aliasDisplayNames = new Map(activeProviders.flatMap(({ name, provider }) => { + const providerModels = models.filter(model => model.provider === name); + const aliases = [...effectiveModelAliases(config, provider, providerModels.map(model => model.id))]; + return aliases.flatMap(([id, { alias }]) => { + const exact = providerModels.filter(model => model.id === id); + const matches = exact.length > 0 + ? exact + : providerModels.filter(model => model.id.toLowerCase() === id.toLowerCase()); + return matches.length === 1 + ? [[`${name}/${matches[0]!.id}`, `${provider.alias || name}/${alias}`] as const] + : []; + }); + })); const providerModelOutcomes = providerResults.map(result => ( result.outcome.provider === OPENAI_API_PROVIDER_ID && capture.openAiApiPolicy.state === "captured" diff --git a/tests/codex-catalog.test.ts b/tests/codex-catalog.test.ts index 128f8a9105..f174acbcde 100644 --- a/tests/codex-catalog.test.ts +++ b/tests/codex-catalog.test.ts @@ -2009,6 +2009,68 @@ describe("configured CatalogModel displayName -> catalog display_name", () => { } }); + test("a case-folded live model id keeps its configured picker alias", async () => { + clearModelCache("mixed-case-live"); + const originalFetch = globalThis.fetch; + globalThis.fetch = (async () => new Response(JSON.stringify({ + data: [{ id: "LIVE-Model" }, { id: "MODEL" }, { id: "model" }], + }), { status: 200, headers: { "content-type": "application/json" } })) as typeof fetch; + try { + const models = await gatherRoutedModels({ + port: 10100, + defaultProvider: "mixed-case-live", + providers: { + "mixed-case-live": { + adapter: "openai-chat", + baseUrl: "https://example.invalid/v1", + authMode: "key", + apiKey: "test-key", + liveModels: true, + modelAliases: { "live-model": "short", "mOdEl": "ambiguous" }, + }, + }, + }); + const entries = buildCatalogEntries(nativeTemplate(), [], models); + const row = entries.find(entry => entry.slug === "mixed-case-live/LIVE-Model"); + + expect(row?.display_name).toBe("mixed-case-live/short"); + expect(row?.slug).toBe("mixed-case-live/LIVE-Model"); + expect(entries.find(entry => entry.slug === "mixed-case-live/MODEL")?.display_name) + .toBe("mixed-case-live/MODEL"); + expect(entries.find(entry => entry.slug === "mixed-case-live/model")?.display_name) + .toBe("mixed-case-live/model"); + } finally { + globalThis.fetch = originalFetch; + clearModelCache("mixed-case-live"); + } + }); + + test("built-in model aliases label picker rows without changing routing slugs", async () => { + clearModelCache("builtin-alias"); + try { + const models = await gatherRoutedModels({ + port: 10100, + defaultProvider: "builtin-alias", + providers: { + "builtin-alias": { + adapter: "openai-chat", + baseUrl: "https://example.invalid/v1", + liveModels: false, + defaultAliases: true, + models: ["grok-4.6"], + }, + }, + }); + const row = buildCatalogEntries(nativeTemplate(), [], models) + .find(entry => entry.slug === "builtin-alias/grok-4.6"); + + expect(row?.display_name).toBe("builtin-alias/grok"); + expect(row?.slug).toBe("builtin-alias/grok-4.6"); + } finally { + clearModelCache("builtin-alias"); + } + }); + test("a custom row clamps its soft budget to the provider max-input ceiling", async () => { const models = await gatherRoutedModels({ port: 10100,