mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-08 20:43:57 +00:00
fix(codex): honor Pro reasoning effort contracts (#101484)
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user