fix(codex): honor Pro reasoning effort contracts (#101484)

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
xingzhou
2026-07-07 18:13:50 +08:00
committed by GitHub
parent 6e792f08f8
commit f14eb97ae7
4 changed files with 76 additions and 15 deletions

View File

@@ -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();

View File

@@ -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"
);

View File

@@ -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");
});
});

View File

@@ -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";
}