diff --git a/apps/server/src/provider/ModelManifest.test.ts b/apps/server/src/provider/ModelManifest.test.ts index 7933f8e2e962..6f3276e3d34f 100644 --- a/apps/server/src/provider/ModelManifest.test.ts +++ b/apps/server/src/provider/ModelManifest.test.ts @@ -75,6 +75,26 @@ describe("classifyModels", () => { ], ); }); + it("applies catalog badges to discovered models", () => { + const manifest: ModelManifestData = { + version: 1, + currentModels: { codex: ["gpt-test"] }, + providers: { + codex: { + profiles: {}, + models: [{ slug: "gpt-test", name: "GPT Test", status: "current", badge: "new" }], + }, + }, + }; + const models = [model({ slug: "openai.gpt-test" }), model({ slug: "gpt-other" })]; + assert.deepStrictEqual( + classifyModels(models, manifest, CODEX).map((entry) => [entry.slug, entry.badge]), + [ + ["openai.gpt-test", "new"], + ["gpt-other", undefined], + ], + ); + }); }); describe("applyManifestDefault", () => { diff --git a/apps/server/src/provider/ModelManifest.ts b/apps/server/src/provider/ModelManifest.ts index d1af1086e750..81e8c9cab7be 100644 --- a/apps/server/src/provider/ModelManifest.ts +++ b/apps/server/src/provider/ModelManifest.ts @@ -209,18 +209,27 @@ export const encodeManifestCache = Schema.encodeEffect( ), ); +function findCatalogModel( + manifest: ModelManifestData, + driverKind: ProviderDriverKind, + slug: string, +) { + const family = driverKind === "codex" ? codexModelFamily(slug) : slug; + const catalog = manifest.providers?.[driverKind]?.models; + return ( + catalog?.find((model) => model.slug === slug) ?? catalog?.find((model) => model.slug === family) + ); +} + /** True when the manifest classifies `slug` as legacy for `driverKind`. */ function isLegacyModel( manifest: ModelManifestData, driverKind: ProviderDriverKind, slug: string, ): boolean { - const family = driverKind === "codex" ? codexModelFamily(slug) : slug; - const catalog = manifest.providers?.[driverKind]?.models; - const catalogModel = - catalog?.find((model) => model.slug === slug) ?? - catalog?.find((model) => model.slug === family); + const catalogModel = findCatalogModel(manifest, driverKind, slug); if (catalogModel) return catalogModel.status === "legacy"; + const family = driverKind === "codex" ? codexModelFamily(slug) : slug; const currentModels = manifest.currentModels[driverKind]; if (!currentModels) return false; return !currentModels.includes(slug) && !currentModels.includes(family); @@ -298,8 +307,11 @@ export function classifyModels( manifest: ModelManifestData, driverKind: ProviderDriverKind, ): ReadonlyArray { - return models.map((model) => { - if (model.isCustom) return model; + return models.map((entry) => { + if (entry.isCustom) return entry; + // Discovered models carry no badge, so the manifest can mark a launch. + const badge = findCatalogModel(manifest, driverKind, entry.slug)?.badge; + const model = badge && entry.badge !== badge ? { ...entry, badge } : entry; if (isLegacyModel(manifest, driverKind, model.slug)) { return model.isLegacy ? model : { ...model, isLegacy: true }; } diff --git a/apps/server/src/provider/model-manifest.json b/apps/server/src/provider/model-manifest.json index 97734ea49060..591fe944ca5a 100644 --- a/apps/server/src/provider/model-manifest.json +++ b/apps/server/src/provider/model-manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "updatedAt": "2026-09-29T01:03:44Z", + "updatedAt": "2026-09-29T19:00:00Z", "compatibility": [ { "driver": "codex", @@ -60,9 +60,9 @@ ], "currentModels": { "codex": [ + "gpt-6.1-sol", "gpt-6-astra", "gpt-6-luna", - "gpt-6-sol", "gpt-daybreak-blue-latest", "gpt-daybreak-red-latest" ], @@ -70,6 +70,12 @@ "antigravity": ["gemini-3.8-flash-high", "gemini-3.8-flash-medium", "gemini-3.8-flash-low"] }, "providers": { + "codex": { + "profiles": {}, + "models": [ + { "slug": "gpt-6.1-sol", "name": "GPT-6.1-Sol", "status": "current", "badge": "new" } + ] + }, "claudeAgent": { "defaults": { "chat": "claude-fable-5-1" diff --git a/docs/internals/model-manifest.md b/docs/internals/model-manifest.md index 7d3eadae7f58..290c3d4c099d 100644 --- a/docs/internals/model-manifest.md +++ b/docs/internals/model-manifest.md @@ -20,7 +20,8 @@ gets its model list from its app server. releases that predate catalog discovery; it does not add models to their catalogs. Catalog-aware releases use `providers.claudeAgent.models[].status` instead. Codex uses `currentModels.codex` as a legacy-classification overlay for discovered -models. +models. A `providers.codex.models` entry overrides that status and can add a +`new` badge, but never adds a model the app server does not list. Model data is schema-validated configuration. Tests should cover resolver, cache, and adapter semantics with synthetic model names, so adding a model never requires