From e30b950cb55f5c3cf42fc26ef4c154ca00e3e7e7 Mon Sep 17 00:00:00 2001 From: ikelvingo Date: Fri, 31 Jul 2026 18:21:16 +0800 Subject: [PATCH] fix(translator): pass output_config.effort=max through verbatim The claude->openai translator was unconditionally rewriting max to xhigh, which broke any OpenAI-shape upstream that accepts max literally (e.g. ollama-cloud, opencode-go deepseek, moonshot k3, native Claude). Provider-aware effort policy is owned by sanitizeReasoningEffortForProvider in the executor; the translator should only do form conversion. Regression guard: tests/unit/base-executor-sanitize-effort.test.ts end-to-end case (claude -> ollama-cloud preserves max). --- .../9053-claude-to-openai-max-passthrough.md | 1 + open-sse/executors/base/reasoningEffort.ts | 14 +++---- .../translator/request/claude-to-openai.ts | 1 - .../base-executor-sanitize-effort.test.ts | 41 +++++++++++++++++++ .../unit/translator-claude-to-openai.test.ts | 6 +-- 5 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 changelog.d/fixes/9053-claude-to-openai-max-passthrough.md diff --git a/changelog.d/fixes/9053-claude-to-openai-max-passthrough.md b/changelog.d/fixes/9053-claude-to-openai-max-passthrough.md new file mode 100644 index 000000000000..86dac223ba2a --- /dev/null +++ b/changelog.d/fixes/9053-claude-to-openai-max-passthrough.md @@ -0,0 +1 @@ +- **fix(translator):** pass `output_config.effort="max"` through verbatim instead of unconditionally rewriting it to `xhigh`, so Anthropic → OpenAI-shape upstream calls reach `sanitizeReasoningEffortForProvider` with the carrier intact and providers that accept `max` literally (Ollama Cloud, opencode-go DeepSeek, Moonshot K3, native Claude) no longer 400 on `invalid reasoning value: 'xhigh'`. Regression guard: end-to-end test in `tests/unit/base-executor-sanitize-effort.test.ts`. diff --git a/open-sse/executors/base/reasoningEffort.ts b/open-sse/executors/base/reasoningEffort.ts index b613f0c3c05f..6c87a25ce7bc 100644 --- a/open-sse/executors/base/reasoningEffort.ts +++ b/open-sse/executors/base/reasoningEffort.ts @@ -7,10 +7,11 @@ import { supportsClaudeMaxEffort, supportsXHighEffort } from "../../config/provi /** * Sanitize reasoning_effort for providers that don't accept all values. * - * The claude→openai translator may emit reasoning_effort=max/xhigh when the - * client sends output_config.effort=max on a Claude-shape request. Combined with - * runtime alias remapping (e.g. claude-opus-4-6 → mimo/mimo-v2.5-pro), this - * routes xhigh to OpenAI-shape providers that don't accept the value: + * The claude→openai translator passes output_config.effort through verbatim + * (including max) and only performs form conversion; provider-aware effort + * policy is owned here. Combined with runtime alias remapping (e.g. + * claude-opus-4-6 → mimo/mimo-v2.5-pro), this routes a client's effort value + * to OpenAI-shape providers that don't accept it: * * xiaomi-mimo : low|medium|high only — 400 literal_error on xhigh * mistral : devstral models reject reasoning_effort entirely @@ -216,10 +217,7 @@ function writeEffortValue( } /** Strip the effort field from every carrier that was present. */ -function stripEffortValue( - b: Record, - c: EffortCarriers -): Record { +function stripEffortValue(b: Record, c: EffortCarriers): Record { const next: Record = { ...b }; if (c.hasTopLevelReasoningEffort) delete next.reasoning_effort; if (c.hasReasoningEffort && c.reasoning) { diff --git a/open-sse/translator/request/claude-to-openai.ts b/open-sse/translator/request/claude-to-openai.ts index ab50607e75ef..1ba8a6e7f136 100644 --- a/open-sse/translator/request/claude-to-openai.ts +++ b/open-sse/translator/request/claude-to-openai.ts @@ -36,7 +36,6 @@ function normalizeToolSchema(schema: unknown): Record { function normalizeOpenAIReasoningEffort(effort: unknown): string | undefined { if (typeof effort !== "string") return undefined; const normalized = effort.toLowerCase(); - if (normalized === "max") return "xhigh"; return normalized || undefined; } diff --git a/tests/unit/base-executor-sanitize-effort.test.ts b/tests/unit/base-executor-sanitize-effort.test.ts index 2fda8aab1070..4b8066fed534 100644 --- a/tests/unit/base-executor-sanitize-effort.test.ts +++ b/tests/unit/base-executor-sanitize-effort.test.ts @@ -3,6 +3,8 @@ import assert from "node:assert/strict"; const { sanitizeReasoningEffortForProvider } = await import("../../open-sse/executors/base.ts"); const { DefaultExecutor } = await import("../../open-sse/executors/default.ts"); +const { translateRequest } = await import("../../open-sse/translator/index.ts"); +const { FORMATS } = await import("../../open-sse/translator/formats.ts"); function makeLog() { const messages: Array<[string, string]> = []; @@ -102,6 +104,45 @@ test("sanitizeReasoningEffortForProvider: Ollama Cloud preserves max", () => { assert.equal(log.messages.length, 0); }); +test("end-to-end: Anthropic output_config.effort=max reaches Ollama Cloud as max (not xhigh)", () => { + // Bug: the claude→openai translator previously normalized max → xhigh, and the + // sanitizer could not recover the original intent because the carrier was already + // xhigh. Ollama Cloud accepts max literally but rejects xhigh (HTTP 400). + // The translator must pass max through verbatim and the sanitizer must keep it. + const translated = translateRequest( + FORMATS.CLAUDE, + FORMATS.OPENAI, + "gemma4:31b", + { + model: "gemma4:31b", + messages: [{ role: "user", content: "hi" }], + output_config: { effort: "max" }, + }, + false, + null, + "ollama-cloud" + ) as Record; + + assert.equal( + translated.reasoning_effort, + "max", + "translator must pass max through verbatim instead of rewriting it to xhigh" + ); + + const sanitized = sanitizeReasoningEffortForProvider( + translated, + "ollama-cloud", + "gemma4:31b", + null + ) as Record; + + assert.equal( + sanitized.reasoning_effort, + "max", + "Ollama Cloud accepts max literally — no downgrade, no rewrite to xhigh" + ); +}); + test("sanitizeReasoningEffortForProvider: Ollama Cloud preserves nested max", () => { const body = { model: "glm-5.2", diff --git a/tests/unit/translator-claude-to-openai.test.ts b/tests/unit/translator-claude-to-openai.test.ts index 2e539aa23952..0acfce1ef86d 100644 --- a/tests/unit/translator-claude-to-openai.test.ts +++ b/tests/unit/translator-claude-to-openai.test.ts @@ -54,8 +54,6 @@ test("Claude -> OpenAI maps system blocks, parameters, tool declarations and too }); }); - - test("Claude -> OpenAI maps Claude server WebSearch to native Responses web_search", () => { const result = claudeToOpenAIRequest( "gpt-5.5", @@ -408,7 +406,7 @@ test("Claude -> OpenAI maps thinking.budget_tokens to reasoning_effort buckets", } }); -test("Claude -> OpenAI normalizes output_config.effort=max to xhigh", () => { +test("Claude -> OpenAI passes output_config.effort=max through verbatim", () => { const result = claudeToOpenAIRequest( "gpt-5", { @@ -418,7 +416,7 @@ test("Claude -> OpenAI normalizes output_config.effort=max to xhigh", () => { false ); - assert.equal(result.reasoning_effort, "xhigh"); + assert.equal(result.reasoning_effort, "max"); }); test("Claude -> OpenAI ignores disabled thinking and leaves reasoning_effort unset", () => {