Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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`.
14 changes: 6 additions & 8 deletions open-sse/executors/base/reasoningEffort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -216,10 +217,7 @@ function writeEffortValue(
}

/** Strip the effort field from every carrier that was present. */
function stripEffortValue(
b: Record<string, unknown>,
c: EffortCarriers
): Record<string, unknown> {
function stripEffortValue(b: Record<string, unknown>, c: EffortCarriers): Record<string, unknown> {
const next: Record<string, unknown> = { ...b };
if (c.hasTopLevelReasoningEffort) delete next.reasoning_effort;
if (c.hasReasoningEffort && c.reasoning) {
Expand Down
1 change: 0 additions & 1 deletion open-sse/translator/request/claude-to-openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ function normalizeToolSchema(schema: unknown): Record<string, unknown> {
function normalizeOpenAIReasoningEffort(effort: unknown): string | undefined {
if (typeof effort !== "string") return undefined;
const normalized = effort.toLowerCase();
if (normalized === "max") return "xhigh";
return normalized || undefined;
}

Expand Down
41 changes: 41 additions & 0 deletions tests/unit/base-executor-sanitize-effort.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]> = [];
Expand Down Expand Up @@ -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<string, unknown>;

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<string, unknown>;

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",
Expand Down
6 changes: 2 additions & 4 deletions tests/unit/translator-claude-to-openai.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
{
Expand All @@ -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", () => {
Expand Down
Loading