fix(executor): normalize max effort for openai shape providers

This commit is contained in:
R.D.
2026-05-29 15:40:33 -04:00
committed by diegosouzapw
parent 863ee3e269
commit 28ded81e74
2 changed files with 64 additions and 4 deletions

View File

@@ -221,7 +221,8 @@ function hasActiveClaudeThinking(body: Record<string, unknown>): boolean {
* through unchanged, and Claude models default to xhigh support unless marked
* as legacy unsupported entries. max support is Claude/CC-compatible only and
* intentionally separate: older Opus/Sonnet models may support max even when
* they do not support xhigh.
* they do not support xhigh. For OpenAI-shape providers, normalize max to
* xhigh when that top tier is allowed; otherwise downgrade to high.
*/
const MISTRAL_NO_REASONING_EFFORT_PATTERN = /devstral/i;
const GITHUB_NO_REASONING_EFFORT_PATTERN = /(claude|haiku|oswe)/i;
@@ -251,9 +252,27 @@ export function sanitizeReasoningEffortForProvider(
const effortStr = typeof effort === "string" ? effort.toLowerCase() : "";
const modelStr = model || "";
const shouldDowngradeXHigh = effortStr === "xhigh" && !supportsXHighEffort(provider, modelStr);
const supportsXHigh = supportsXHighEffort(provider, modelStr);
const shouldDowngradeXHigh = effortStr === "xhigh" && !supportsXHigh;
const shouldNormalizeMaxToXHigh =
effortStr === "max" && !supportsMaxEffortForProvider(provider, modelStr) && supportsXHigh;
const shouldDowngradeMax =
effortStr === "max" && !supportsMaxEffortForProvider(provider, modelStr);
effortStr === "max" && !supportsMaxEffortForProvider(provider, modelStr) && !supportsXHigh;
if (shouldNormalizeMaxToXHigh) {
log?.info?.(
"REASONING_SANITIZE",
`${provider}/${modelStr}: normalized reasoning_effort max → xhigh`
);
const next: Record<string, unknown> = { ...b };
if (hasTopLevelReasoningEffort) {
next.reasoning_effort = "xhigh";
}
if (reasoning) {
next.reasoning = { ...reasoning, effort: "xhigh" };
}
return next;
}
if (shouldDowngradeXHigh || shouldDowngradeMax) {
log?.info?.(
@@ -797,7 +816,10 @@ export class BaseExecutor {
delete (tb.output_config as Record<string, unknown>).effort;
}
appliedEffort = "off";
} else if (headerEffort && ["low", "medium", "high", "xhigh", "max"].includes(headerEffort)) {
} else if (
headerEffort &&
["low", "medium", "high", "xhigh", "max"].includes(headerEffort)
) {
const oc =
tb.output_config && typeof tb.output_config === "object"
? (tb.output_config as Record<string, unknown>)

View File

@@ -43,6 +43,44 @@ test("sanitizeReasoningEffortForProvider: xiaomi-mimo downgrades max → high",
);
});
test("sanitizeReasoningEffortForProvider: OpenAI-compatible Gemini normalizes max → xhigh", () => {
const log = makeLog();
const body = {
model: "gemini-3.1-pro-preview",
reasoning_effort: "max",
messages: [{ role: "user", content: "hi" }],
};
const result = sanitizeReasoningEffortForProvider(
body,
"openai-compatible-free1",
"gemini-3.1-pro-preview",
log
);
assert.notEqual(result, body, "must return a new object when mutating");
assert.equal((result as any).reasoning_effort, "xhigh");
assert.ok(
log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /max → xhigh/.test(m)),
"logs the normalization"
);
});
test("sanitizeReasoningEffortForProvider: nested OpenAI reasoning max normalizes to xhigh", () => {
const body = {
model: "gemini-3.1-pro-preview",
reasoning: { effort: "max", summary: "auto" },
input: [],
};
const result = sanitizeReasoningEffortForProvider(
body,
"openai-compatible-free1",
"gemini-3.1-pro-preview",
null
);
assert.equal((result as any).reasoning.effort, "xhigh");
assert.equal((result as any).reasoning.summary, "auto", "other reasoning fields preserved");
assert.equal((result as any).reasoning_effort, undefined);
});
test("sanitizeReasoningEffortForProvider: claude preserves max for Opus/Sonnet and downgrades Haiku", () => {
const sonnetBody = {
model: "claude-sonnet-4-6",