Skip to content

Commit 34baea4

Browse files
fix(codex): honor Pro reasoning effort contracts
Co-authored-by: Vincent Koc <[email protected]>
1 parent 079a704 commit 34baea4

4 files changed

Lines changed: 76 additions & 15 deletions

File tree

extensions/codex/provider.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,20 @@ function mockCallArg(mockFn: { mock: { calls: unknown[][] } }, callIndex: number
8080
}
8181

8282
describe("codex provider", () => {
83+
it.each(["gpt-5.5-pro", "gpt-5.4-pro"] as const)(
84+
"classifies %s as a modern Codex model",
85+
(modelId) => {
86+
const provider = buildCodexProvider();
87+
88+
expect(
89+
provider.isModernModelRef?.({
90+
provider: "openai",
91+
modelId,
92+
} as never),
93+
).toBe(true);
94+
},
95+
);
96+
8397
it("maps Codex app-server models to a Codex provider catalog", async () => {
8498
const listModels = vi.fn(async () => ({
8599
models: [
@@ -406,6 +420,22 @@ describe("codex provider", () => {
406420
).toEqual(["off", "medium", "high", "xhigh"]);
407421
});
408422

423+
it.each(["gpt-5.5-pro", "gpt-5.4-pro"] as const)(
424+
"uses the known %s effort profile when app-server metadata is absent",
425+
(modelId) => {
426+
const provider = buildCodexProvider();
427+
428+
expect(
429+
provider
430+
.resolveThinkingProfile?.({
431+
provider: "codex",
432+
modelId,
433+
} as never)
434+
?.levels.map((level) => level.id),
435+
).toEqual(["off", "medium", "high", "xhigh"]);
436+
},
437+
);
438+
409439
it("declares synthetic auth because the harness owns Codex credentials", () => {
410440
const provider = buildCodexProvider();
411441

extensions/codex/provider.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const CODEX_APP_SERVER_SETUP_METHOD_ID = "app-server";
3636
const CODEX_DEFAULT_MODEL_REF = `${CODEX_PROVIDER_ID}/${FALLBACK_CODEX_MODELS[0].id}`;
3737
const codexCatalogLog = createSubsystemLogger("codex/catalog");
3838
const CODEX_REASONING_EFFORTS = ["minimal", "low", "medium", "high", "xhigh", "max"] as const;
39+
const GPT_5_PRO_REASONING_EFFORTS = ["medium", "high", "xhigh"] as const;
3940

4041
export type CodexReasoningEffort = (typeof CODEX_REASONING_EFFORTS)[number];
4142

@@ -310,6 +311,10 @@ function resolveCodexThinkingEfforts(params: {
310311
if (params.supportedReasoningEfforts) {
311312
return normalizeCodexReasoningEfforts(params.supportedReasoningEfforts);
312313
}
314+
const fallbackEfforts = resolveCodexFallbackReasoningEfforts(params.modelId);
315+
if (fallbackEfforts) {
316+
return [...fallbackEfforts];
317+
}
313318
return [
314319
"minimal",
315320
"low",
@@ -336,17 +341,26 @@ export function resolveCodexSupportedReasoningEffort(params: {
336341
);
337342
}
338343

339-
/**
340-
* Returns true for Codex models that use the modern reasoning effort enum and
341-
* reject the legacy CLI `minimal` default.
342-
*/
344+
/** Return the known effort contract when app-server model metadata is unavailable. */
345+
export function resolveCodexFallbackReasoningEfforts(
346+
modelId: string,
347+
): readonly CodexReasoningEffort[] | undefined {
348+
const normalized = modelId.trim().toLowerCase();
349+
return normalized === "gpt-5.5-pro" || normalized === "gpt-5.4-pro"
350+
? GPT_5_PRO_REASONING_EFFORTS
351+
: undefined;
352+
}
353+
354+
/** Return whether the model uses the modern Codex reasoning profile. */
343355
export function isModernCodexModel(modelId: string): boolean {
344356
const lower = modelId.trim().toLowerCase();
345357
return (
346358
lower === "gpt-5.6" ||
347359
lower.startsWith("gpt-5.6-") ||
348360
lower === "gpt-5.5" ||
361+
lower === "gpt-5.5-pro" ||
349362
lower === "gpt-5.4" ||
363+
lower === "gpt-5.4-pro" ||
350364
lower === "gpt-5.4-mini" ||
351365
lower === "gpt-5.3-codex-spark"
352366
);

extensions/codex/src/app-server/thread-lifecycle.test.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,13 +1409,23 @@ describe("resolveReasoningEffort (#71946)", () => {
14091409
expect(resolveReasoningEffort("minimal", " gpt-5.4-mini ")).toBe("low");
14101410
});
14111411

1412+
it.each(["gpt-5.5-pro", "gpt-5.4-pro"] as const)(
1413+
"uses the %s minimum effort when metadata is unavailable",
1414+
(modelId) => {
1415+
expect(resolveReasoningEffort("minimal", modelId)).toBe("medium");
1416+
expect(resolveReasoningEffort("low", modelId)).toBe("medium");
1417+
expect(resolveReasoningEffort("medium", modelId)).toBe("medium");
1418+
expect(resolveReasoningEffort("max", modelId)).toBe("xhigh");
1419+
},
1420+
);
1421+
14121422
it("honors stricter app-server reasoning metadata", () => {
14131423
const supported = ["medium", "high", "xhigh"];
14141424

1415-
expect(resolveReasoningEffort("minimal", "gpt-5.4-pro", supported)).toBe("medium");
1416-
expect(resolveReasoningEffort("low", "gpt-5.4-pro", supported)).toBe("medium");
1417-
expect(resolveReasoningEffort("medium", "gpt-5.4-pro", supported)).toBe("medium");
1418-
expect(resolveReasoningEffort("max", "gpt-5.4-pro", supported)).toBe("xhigh");
1425+
expect(resolveReasoningEffort("minimal", "gpt-5.5-pro", supported)).toBe("medium");
1426+
expect(resolveReasoningEffort("low", "gpt-5.5-pro", supported)).toBe("medium");
1427+
expect(resolveReasoningEffort("medium", "gpt-5.5-pro", supported)).toBe("medium");
1428+
expect(resolveReasoningEffort("max", "gpt-5.5-pro", supported)).toBe("xhigh");
14191429
});
14201430
});
14211431

extensions/codex/src/app-server/thread-lifecycle.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
isMaxReasoningCodexModel,
1515
isModernCodexModel,
1616
readCodexSupportedReasoningEfforts,
17+
resolveCodexFallbackReasoningEfforts,
1718
resolveCodexSupportedReasoningEffort,
1819
type CodexReasoningEffort,
1920
} from "../../provider.js";
@@ -1868,13 +1869,10 @@ export function resolveCodexAppServerModelProvider(params: {
18681869
return normalizedLower === "openai" ? "openai" : normalized;
18691870
}
18701871

1871-
// Modern Codex models (gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex-spark) use the
1872-
// none/low/medium/high/xhigh effort enum and reject "minimal". The CLI
1873-
// defaults thinkLevel to "minimal", so without translation EVERY agent turn
1874-
// on those models pays a wasted first request + retry-with-low fallback in
1875-
// embedded-agent-runner. Map "minimal" -> "low" upfront for modern models so the
1876-
// first request is accepted. Older Codex models still accept "minimal"
1877-
// directly. (#71946)
1872+
// Modern Codex models reject the legacy CLI `minimal` default. Prefer
1873+
// app-server metadata, then use the provider-owned fallback effort contract
1874+
// for Pro models whose minimum supported effort is `medium`.
1875+
// Other modern models translate `minimal` to `low`. (#71946)
18781876
// Exported for unit-test coverage of the model-aware translation path.
18791877
export function resolveReasoningEffort(
18801878
thinkLevel: EmbeddedRunAttemptParams["thinkLevel"],
@@ -1892,6 +1890,15 @@ export function resolveReasoningEffort(
18921890
}) ?? null
18931891
);
18941892
}
1893+
const fallbackReasoningEfforts = resolveCodexFallbackReasoningEfforts(modelId);
1894+
if (fallbackReasoningEfforts) {
1895+
return (
1896+
resolveCodexSupportedReasoningEffort({
1897+
requested: thinkLevel,
1898+
supportedReasoningEfforts: fallbackReasoningEfforts,
1899+
}) ?? null
1900+
);
1901+
}
18951902
if (thinkLevel === "minimal") {
18961903
return isModernCodexModel(modelId) ? "low" : "minimal";
18971904
}

0 commit comments

Comments
 (0)