From f14eb97ae7c5eded219c1901eee7cda44a948438 Mon Sep 17 00:00:00 2001 From: xingzhou Date: Tue, 7 Jul 2026 18:13:50 +0800 Subject: [PATCH] fix(codex): honor Pro reasoning effort contracts (#101484) Co-authored-by: Vincent Koc --- extensions/codex/provider.test.ts | 30 +++++++++++++++++++ extensions/codex/provider.ts | 22 +++++++++++--- .../src/app-server/thread-lifecycle.test.ts | 18 ++++++++--- .../codex/src/app-server/thread-lifecycle.ts | 21 ++++++++----- 4 files changed, 76 insertions(+), 15 deletions(-) diff --git a/extensions/codex/provider.test.ts b/extensions/codex/provider.test.ts index d7213b355928..e1324c627e30 100644 --- a/extensions/codex/provider.test.ts +++ b/extensions/codex/provider.test.ts @@ -80,6 +80,20 @@ function mockCallArg(mockFn: { mock: { calls: unknown[][] } }, callIndex: number } describe("codex provider", () => { + it.each(["gpt-5.5-pro", "gpt-5.4-pro"] as const)( + "classifies %s as a modern Codex model", + (modelId) => { + const provider = buildCodexProvider(); + + expect( + provider.isModernModelRef?.({ + provider: "openai", + modelId, + } as never), + ).toBe(true); + }, + ); + it("maps Codex app-server models to a Codex provider catalog", async () => { const listModels = vi.fn(async () => ({ models: [ @@ -406,6 +420,22 @@ describe("codex provider", () => { ).toEqual(["off", "medium", "high", "xhigh"]); }); + it.each(["gpt-5.5-pro", "gpt-5.4-pro"] as const)( + "uses the known %s effort profile when app-server metadata is absent", + (modelId) => { + const provider = buildCodexProvider(); + + expect( + provider + .resolveThinkingProfile?.({ + provider: "codex", + modelId, + } as never) + ?.levels.map((level) => level.id), + ).toEqual(["off", "medium", "high", "xhigh"]); + }, + ); + it("declares synthetic auth because the harness owns Codex credentials", () => { const provider = buildCodexProvider(); diff --git a/extensions/codex/provider.ts b/extensions/codex/provider.ts index 3d6e572f7ba6..bee5cba9da5c 100644 --- a/extensions/codex/provider.ts +++ b/extensions/codex/provider.ts @@ -36,6 +36,7 @@ const CODEX_APP_SERVER_SETUP_METHOD_ID = "app-server"; const CODEX_DEFAULT_MODEL_REF = `${CODEX_PROVIDER_ID}/${FALLBACK_CODEX_MODELS[0].id}`; const codexCatalogLog = createSubsystemLogger("codex/catalog"); const CODEX_REASONING_EFFORTS = ["minimal", "low", "medium", "high", "xhigh", "max"] as const; +const GPT_5_PRO_REASONING_EFFORTS = ["medium", "high", "xhigh"] as const; export type CodexReasoningEffort = (typeof CODEX_REASONING_EFFORTS)[number]; @@ -310,6 +311,10 @@ function resolveCodexThinkingEfforts(params: { if (params.supportedReasoningEfforts) { return normalizeCodexReasoningEfforts(params.supportedReasoningEfforts); } + const fallbackEfforts = resolveCodexFallbackReasoningEfforts(params.modelId); + if (fallbackEfforts) { + return [...fallbackEfforts]; + } return [ "minimal", "low", @@ -336,17 +341,26 @@ export function resolveCodexSupportedReasoningEffort(params: { ); } -/** - * Returns true for Codex models that use the modern reasoning effort enum and - * reject the legacy CLI `minimal` default. - */ +/** Return the known effort contract when app-server model metadata is unavailable. */ +export function resolveCodexFallbackReasoningEfforts( + modelId: string, +): readonly CodexReasoningEffort[] | undefined { + const normalized = modelId.trim().toLowerCase(); + return normalized === "gpt-5.5-pro" || normalized === "gpt-5.4-pro" + ? GPT_5_PRO_REASONING_EFFORTS + : undefined; +} + +/** Return whether the model uses the modern Codex reasoning profile. */ export function isModernCodexModel(modelId: string): boolean { const lower = modelId.trim().toLowerCase(); return ( lower === "gpt-5.6" || lower.startsWith("gpt-5.6-") || lower === "gpt-5.5" || + lower === "gpt-5.5-pro" || lower === "gpt-5.4" || + lower === "gpt-5.4-pro" || lower === "gpt-5.4-mini" || lower === "gpt-5.3-codex-spark" ); diff --git a/extensions/codex/src/app-server/thread-lifecycle.test.ts b/extensions/codex/src/app-server/thread-lifecycle.test.ts index e28430c1d121..7697d751be13 100644 --- a/extensions/codex/src/app-server/thread-lifecycle.test.ts +++ b/extensions/codex/src/app-server/thread-lifecycle.test.ts @@ -1409,13 +1409,23 @@ describe("resolveReasoningEffort (#71946)", () => { expect(resolveReasoningEffort("minimal", " gpt-5.4-mini ")).toBe("low"); }); + it.each(["gpt-5.5-pro", "gpt-5.4-pro"] as const)( + "uses the %s minimum effort when metadata is unavailable", + (modelId) => { + expect(resolveReasoningEffort("minimal", modelId)).toBe("medium"); + expect(resolveReasoningEffort("low", modelId)).toBe("medium"); + expect(resolveReasoningEffort("medium", modelId)).toBe("medium"); + expect(resolveReasoningEffort("max", modelId)).toBe("xhigh"); + }, + ); + it("honors stricter app-server reasoning metadata", () => { const supported = ["medium", "high", "xhigh"]; - expect(resolveReasoningEffort("minimal", "gpt-5.4-pro", supported)).toBe("medium"); - expect(resolveReasoningEffort("low", "gpt-5.4-pro", supported)).toBe("medium"); - expect(resolveReasoningEffort("medium", "gpt-5.4-pro", supported)).toBe("medium"); - expect(resolveReasoningEffort("max", "gpt-5.4-pro", supported)).toBe("xhigh"); + expect(resolveReasoningEffort("minimal", "gpt-5.5-pro", supported)).toBe("medium"); + expect(resolveReasoningEffort("low", "gpt-5.5-pro", supported)).toBe("medium"); + expect(resolveReasoningEffort("medium", "gpt-5.5-pro", supported)).toBe("medium"); + expect(resolveReasoningEffort("max", "gpt-5.5-pro", supported)).toBe("xhigh"); }); }); diff --git a/extensions/codex/src/app-server/thread-lifecycle.ts b/extensions/codex/src/app-server/thread-lifecycle.ts index 4af169fe8172..cd6ad0e0b1cc 100644 --- a/extensions/codex/src/app-server/thread-lifecycle.ts +++ b/extensions/codex/src/app-server/thread-lifecycle.ts @@ -14,6 +14,7 @@ import { isMaxReasoningCodexModel, isModernCodexModel, readCodexSupportedReasoningEfforts, + resolveCodexFallbackReasoningEfforts, resolveCodexSupportedReasoningEffort, type CodexReasoningEffort, } from "../../provider.js"; @@ -1868,13 +1869,10 @@ export function resolveCodexAppServerModelProvider(params: { return normalizedLower === "openai" ? "openai" : normalized; } -// Modern Codex models (gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex-spark) use the -// none/low/medium/high/xhigh effort enum and reject "minimal". The CLI -// defaults thinkLevel to "minimal", so without translation EVERY agent turn -// on those models pays a wasted first request + retry-with-low fallback in -// embedded-agent-runner. Map "minimal" -> "low" upfront for modern models so the -// first request is accepted. Older Codex models still accept "minimal" -// directly. (#71946) +// Modern Codex models reject the legacy CLI `minimal` default. Prefer +// app-server metadata, then use the provider-owned fallback effort contract +// for Pro models whose minimum supported effort is `medium`. +// Other modern models translate `minimal` to `low`. (#71946) // Exported for unit-test coverage of the model-aware translation path. export function resolveReasoningEffort( thinkLevel: EmbeddedRunAttemptParams["thinkLevel"], @@ -1892,6 +1890,15 @@ export function resolveReasoningEffort( }) ?? null ); } + const fallbackReasoningEfforts = resolveCodexFallbackReasoningEfforts(modelId); + if (fallbackReasoningEfforts) { + return ( + resolveCodexSupportedReasoningEffort({ + requested: thinkLevel, + supportedReasoningEfforts: fallbackReasoningEfforts, + }) ?? null + ); + } if (thinkLevel === "minimal") { return isModernCodexModel(modelId) ? "low" : "minimal"; }