From 6b75ff3f0c09abac0de1f648e91adb2828efe2a9 Mon Sep 17 00:00:00 2001 From: Ivan Malison Date: Fri, 24 Jul 2026 16:45:40 -0700 Subject: [PATCH] fix(server): drop non-interactive Codex models from the model picker `model/list` exposes a `hidden` flag that the provider dropped on the floor, and codex-cli 0.145.0 additionally reports `codex-auto-review` (the automatic approval-review model) with `hidden: false` and the display name of another catalog entry, so the picker renders two identical "GPT-5.6-Luna" rows. Threads started on the auto-review model cannot execute anything: every tool call fails locally with "unsupported custom tool call" and every follow-up turn is rejected with a 400 for an unknown `namespace` input parameter. Honor `hidden` and skip known non-interactive Codex slugs when parsing the catalog. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/provider/Layers/CodexProvider.test.ts | 69 ++++++++++++++++++- .../src/provider/Layers/CodexProvider.ts | 18 ++++- 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/apps/server/src/provider/Layers/CodexProvider.test.ts b/apps/server/src/provider/Layers/CodexProvider.test.ts index 2aeebdb2ccd..283daeea57f 100644 --- a/apps/server/src/provider/Layers/CodexProvider.test.ts +++ b/apps/server/src/provider/Layers/CodexProvider.test.ts @@ -1,6 +1,26 @@ import { assert, it } from "@effect/vitest"; +import type * as CodexSchema from "effect-codex-app-server/schema"; -import { applyPreferredCodexDefaultModel, mapCodexModelCapabilities } from "./CodexProvider.ts"; +import { + applyPreferredCodexDefaultModel, + mapCodexModelCapabilities, + parseCodexModelListResponse, +} from "./CodexProvider.ts"; + +const catalogModel = ( + overrides: Partial & + Pick, +): CodexSchema.V2ModelListResponse__Model => ({ + additionalSpeedTiers: [], + defaultReasoningEffort: "medium", + description: "Test model", + displayName: overrides.model, + hidden: false, + id: overrides.model, + isDefault: false, + supportedReasoningEfforts: [], + ...overrides, +}); it("maps current Codex model capability fields", () => { const capabilities = mapCodexModelCapabilities({ @@ -136,6 +156,53 @@ it("keeps Codex's own default when no preferred model is available", () => { assert.deepStrictEqual(models.find((model) => model.isDefault)?.slug, "gpt-5.4"); }); +it("keeps selectable models from the catalog", () => { + const models = parseCodexModelListResponse({ + data: [ + catalogModel({ model: "gpt-5.6-sol", displayName: "gpt-5.6-sol", isDefault: true }), + catalogModel({ model: "gpt-5.6-luna", displayName: "gpt-5.6-luna" }), + ], + }); + + assert.deepStrictEqual( + models.map((model) => ({ slug: model.slug, name: model.name, isDefault: model.isDefault })), + [ + { slug: "gpt-5.6-sol", name: "GPT-5.6-Sol", isDefault: true }, + { slug: "gpt-5.6-luna", name: "GPT-5.6-Luna", isDefault: undefined }, + ], + ); +}); + +it("drops catalog models the app server marks as hidden", () => { + const models = parseCodexModelListResponse({ + data: [ + catalogModel({ model: "gpt-5.6-sol" }), + catalogModel({ model: "gpt-5.6-internal", hidden: true }), + ], + }); + + assert.deepStrictEqual( + models.map((model) => model.slug), + ["gpt-5.6-sol"], + ); +}); + +it("drops non-interactive Codex models even when they are not marked hidden", () => { + // codex-cli 0.145.0 reports codex-auto-review with hidden: false and the + // display name of another model, so it appears as a duplicate picker row. + const models = parseCodexModelListResponse({ + data: [ + catalogModel({ model: "codex-auto-review", displayName: "gpt-5.6-luna" }), + catalogModel({ model: "gpt-5.6-luna", displayName: "gpt-5.6-luna" }), + ], + }); + + assert.deepStrictEqual( + models.map((model) => ({ slug: model.slug, name: model.name })), + [{ slug: "gpt-5.6-luna", name: "GPT-5.6-Luna" }], + ); +}); + it("ignores custom models that shadow a preferred slug", () => { const models = applyPreferredCodexDefaultModel([ { slug: "gpt-5.6-sol", name: "gpt-5.6-sol", isCustom: true, capabilities: null }, diff --git a/apps/server/src/provider/Layers/CodexProvider.ts b/apps/server/src/provider/Layers/CodexProvider.ts index 1ed9c750c18..27e1b5c9018 100644 --- a/apps/server/src/provider/Layers/CodexProvider.ts +++ b/apps/server/src/provider/Layers/CodexProvider.ts @@ -182,10 +182,24 @@ const toDisplayName = (model: CodexSchema.V2ModelListResponse__Model): string => .replace(/-([a-z])/g, (_, c) => "-" + c.toUpperCase()); }; -function parseCodexModelListResponse( +// Codex models that exist for internal Codex machinery rather than interactive +// use, and that `model/list` does not mark as hidden. As of codex-cli 0.145.0 +// `codex-auto-review` (the automatic approval-review model) is returned with +// `hidden: false` and the display name of another catalog entry +// ("GPT-5.6-Luna"), so it renders as a duplicate picker row. Selecting it +// produces a session where every tool call fails locally with "unsupported +// custom tool call" and every follow-up turn is rejected by the API. +const NON_INTERACTIVE_CODEX_MODEL_SLUGS = new Set(["codex-auto-review"]); + +function isSelectableCodexModel(model: CodexSchema.V2ModelListResponse__Model): boolean { + return !model.hidden && !NON_INTERACTIVE_CODEX_MODEL_SLUGS.has(model.model); +} + +/** @internal */ +export function parseCodexModelListResponse( response: CodexSchema.V2ModelListResponse, ): ReadonlyArray { - return response.data.map((model) => ({ + return response.data.filter(isSelectableCodexModel).map((model) => ({ slug: model.model, name: toDisplayName(model), isCustom: false,