From 170b549bc79ffad4ef6c274c1f2d5185535f4b58 Mon Sep 17 00:00:00 2001 From: "R.D." Date: Fri, 29 May 2026 15:40:33 -0400 Subject: [PATCH] fix(executor): normalize max effort for openai shape providers --- open-sse/executors/base.ts | 30 +++++++++++++-- .../base-executor-sanitize-effort.test.ts | 38 +++++++++++++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts index f45c12fe68..084db6a919 100644 --- a/open-sse/executors/base.ts +++ b/open-sse/executors/base.ts @@ -221,7 +221,8 @@ function hasActiveClaudeThinking(body: Record): 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 = { ...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).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) diff --git a/tests/unit/base-executor-sanitize-effort.test.ts b/tests/unit/base-executor-sanitize-effort.test.ts index 910e1c8fd6..9f2eea1266 100644 --- a/tests/unit/base-executor-sanitize-effort.test.ts +++ b/tests/unit/base-executor-sanitize-effort.test.ts @@ -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",