fix(claude): preserve max effort for supported models (#2875)

Integrated into release/v3.8.6.
This commit is contained in:
Randi
2026-05-29 00:49:57 -04:00
committed by GitHub
parent e510a57555
commit b971db8e77
9 changed files with 168 additions and 18 deletions

View File

@@ -144,6 +144,10 @@ Models:
**Pro Tip:** Use Opus for complex tasks, Sonnet for speed. OmniRoute tracks quota per model!
Claude and Claude Code-compatible routes preserve `max` thinking effort for Opus and Sonnet
models. Haiku models do not accept the `max` effort tier, so OmniRoute downgrades that
request to a high thinking budget before sending it upstream.
#### OpenAI Codex (Plus/Pro)
```bash

View File

@@ -59,6 +59,20 @@ export function getModelsByProviderId(providerId: string): RegistryModel[] {
return PROVIDER_MODELS[alias] || [];
}
const CLAUDE_MODEL_PATTERN = /(?:^|[\/._-])claude(?:[._-]|$)/;
const CLAUDE_MAX_EFFORT_UNSUPPORTED_FAMILY_PATTERNS = [/(?:^|[\/._-])haiku(?:[._-]|$)/] as const;
export function supportsClaudeMaxEffort(modelId: string | null | undefined): boolean {
if (typeof modelId !== "string" || modelId.length === 0) return false;
const normalized = modelId.toLowerCase();
const claudeMatch = normalized.match(CLAUDE_MODEL_PATTERN);
if (!claudeMatch) return false;
const claudeScopedId = normalized.slice(claudeMatch.index ?? 0);
return !CLAUDE_MAX_EFFORT_UNSUPPORTED_FAMILY_PATTERNS.some((pattern) =>
pattern.test(claudeScopedId)
);
}
export function supportsXHighEffort(aliasOrId: string, modelId: string): boolean {
const alias = PROVIDER_ID_TO_ALIAS[aliasOrId] || aliasOrId;
const providerModels = PROVIDER_MODELS[alias] || PROVIDER_MODELS[aliasOrId];

View File

@@ -1,6 +1,6 @@
import { HTTP_STATUS, FETCH_TIMEOUT_MS } from "../config/constants.ts";
import { applyFingerprint, isCliCompatEnabled } from "../config/cliFingerprints.ts";
import { supportsXHighEffort } from "../config/providerModels.ts";
import { supportsClaudeMaxEffort, supportsXHighEffort } from "../config/providerModels.ts";
import {
getRotatingApiKey,
getValidApiKey,
@@ -205,8 +205,8 @@ function hasActiveClaudeThinking(body: Record<string, unknown>): boolean {
/**
* Sanitize reasoning_effort for providers that don't accept all values.
*
* The claude→openai translator emits reasoning_effort=xhigh when the client
* sends output_config.effort=max on a Claude-shape request. Combined with
* 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:
*
@@ -217,11 +217,22 @@ function hasActiveClaudeThinking(body: Record<string, unknown>): boolean {
* Each rejection burns a combo fallback attempt before reaching a working
* provider. Apply provider-aware sanitation here (after transformRequest, so
* reintroductions by per-provider transforms are also caught) before fetch.
* Models that genuinely support xhigh pass through unchanged. Claude models
* default to xhigh support unless they are marked as legacy unsupported entries.
* xhigh support is registry-gated: models that genuinely support xhigh pass
* 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.
*/
const MISTRAL_NO_REASONING_EFFORT_PATTERN = /devstral/i;
const GITHUB_NO_REASONING_EFFORT_PATTERN = /(claude|haiku|oswe)/i;
function supportsMaxEffortForProvider(provider: string, model: string): boolean {
return (
(provider === PROVIDER_CLAUDE || isClaudeCodeCompatible(provider)) &&
supportsClaudeMaxEffort(model)
);
}
export function sanitizeReasoningEffortForProvider(
body: unknown,
provider: string,
@@ -240,10 +251,14 @@ export function sanitizeReasoningEffortForProvider(
const effortStr = typeof effort === "string" ? effort.toLowerCase() : "";
const modelStr = model || "";
if (effortStr === "xhigh" && !supportsXHighEffort(provider, modelStr)) {
const shouldDowngradeXHigh = effortStr === "xhigh" && !supportsXHighEffort(provider, modelStr);
const shouldDowngradeMax =
effortStr === "max" && !supportsMaxEffortForProvider(provider, modelStr);
if (shouldDowngradeXHigh || shouldDowngradeMax) {
log?.info?.(
"REASONING_SANITIZE",
`${provider}/${modelStr}: downgraded reasoning_effort xhigh → high`
`${provider}/${modelStr}: downgraded reasoning_effort ${effortStr} → high`
);
const next: Record<string, unknown> = { ...b };
if (hasTopLevelReasoningEffort) {
@@ -760,7 +775,7 @@ export class BaseExecutor {
}
// Per-request behavior overrides via custom client headers.
// x-omniroute-effort: low | medium | high | xhigh | off
// x-omniroute-effort: low | medium | high | xhigh | max | off
// x-omniroute-thinking: adaptive | off
// A header value applies only when the corresponding body field is
// not already set; "off" force-strips the field.
@@ -782,7 +797,7 @@ export class BaseExecutor {
delete (tb.output_config as Record<string, unknown>).effort;
}
appliedEffort = "off";
} else if (headerEffort && ["low", "medium", "high", "xhigh"].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

@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
import { getStainlessTimeoutSeconds } from "@/shared/utils/runtimeTimeouts";
import { ANTHROPIC_VERSION_HEADER } from "../config/anthropicHeaders.ts";
import { supportsXHighEffort } from "../config/providerModels.ts";
import { supportsClaudeMaxEffort, supportsXHighEffort } from "../config/providerModels.ts";
import { prepareClaudeRequest } from "../translator/helpers/claudeHelper.ts";
import { signRequestBody } from "./claudeCodeCCH.ts";
import { remapToolNamesInRequest } from "./claudeCodeToolRemapper.ts";
@@ -451,7 +451,7 @@ export function resolveClaudeCodeCompatibleEffort(
sourceBody?: Record<string, unknown> | null,
normalizedBody?: Record<string, unknown> | null,
model?: string | null
): "low" | "medium" | "high" | "xhigh" {
): "low" | "medium" | "high" | "xhigh" | "max" {
const raw =
readNestedString(sourceBody, ["output_config", "effort"]) ||
readNestedString(sourceBody, ["reasoning", "effort"]) ||
@@ -474,7 +474,7 @@ export function resolveClaudeCodeCompatibleEffort(
return supportsClaudeXHighEffort(model) ? "xhigh" : "high";
}
if (normalizedEffort === "max") {
return supportsClaudeXHighEffort(model) ? "xhigh" : "high";
return supportsClaudeMaxEffort(model) ? "max" : "high";
}
return supportsClaudeXHighEffort(model) ? "xhigh" : "high";
}
@@ -1102,7 +1102,7 @@ function resolveClaudeCodeCompatibleOutputConfig({
sourceBody?: Record<string, unknown> | null;
normalizedBody?: Record<string, unknown> | null;
model?: string | null;
effort: "low" | "medium" | "high" | "xhigh";
effort: "low" | "medium" | "high" | "xhigh" | "max";
}) {
const outputConfig =
readRecord(cloneValue(claudeBody?.output_config)) ||

View File

@@ -1,7 +1,7 @@
import { register } from "../registry.ts";
import { FORMATS } from "../formats.ts";
// CLAUDE_SYSTEM_PROMPT import removed — no longer injected unconditionally (#1966/#2130)
import { supportsXHighEffort } from "../../config/providerModels.ts";
import { supportsClaudeMaxEffort, supportsXHighEffort } from "../../config/providerModels.ts";
import { adjustMaxTokens } from "../helpers/maxTokensHelper.ts";
import { sanitizeToolId } from "../helpers/schemaCoercion.ts";
import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingSignature.ts";
@@ -453,16 +453,18 @@ export function openaiToClaudeRequest(model, body, stream) {
// Clients like OpenCode send reasoning_effort via @ai-sdk/openai-compatible
const requestedEffort = String(body.reasoning_effort).toLowerCase();
const normalizedEffort =
requestedEffort === "xhigh" && !supportsXHighEffort("claude", model)
requestedEffort === "max" && !supportsClaudeMaxEffort(model)
? "high"
: requestedEffort;
if (normalizedEffort === "xhigh") {
: requestedEffort === "xhigh" && !supportsXHighEffort("claude", model)
? "high"
: requestedEffort;
if (normalizedEffort === "max" || normalizedEffort === "xhigh") {
result.thinking = {
type: "adaptive",
};
result.output_config = {
...(result.output_config || {}),
effort: "xhigh",
effort: normalizedEffort,
};
} else {
const effortBudgetMap: Record<string, number> = {

View File

@@ -28,6 +28,65 @@ test("sanitizeReasoningEffortForProvider: xiaomi-mimo downgrades xhigh → high"
);
});
test("sanitizeReasoningEffortForProvider: xiaomi-mimo downgrades max → high", () => {
const log = makeLog();
const body = {
model: "mimo-v2.5-pro",
reasoning_effort: "max",
messages: [{ role: "user", content: "hi" }],
};
const result = sanitizeReasoningEffortForProvider(body, "xiaomi-mimo", "mimo-v2.5-pro", log);
assert.equal((result as any).reasoning_effort, "high");
assert.ok(
log.messages.some(([tag, m]) => tag === "REASONING_SANITIZE" && /max → high/.test(m)),
"logs the downgrade"
);
});
test("sanitizeReasoningEffortForProvider: claude preserves max for Opus/Sonnet and downgrades Haiku", () => {
const sonnetBody = {
model: "claude-sonnet-4-6",
reasoning_effort: "max",
messages: [{ role: "user", content: "hi" }],
};
const sonnetResult = sanitizeReasoningEffortForProvider(
sonnetBody,
"claude",
"claude-sonnet-4-6",
null
);
assert.equal(sonnetResult, sonnetBody);
assert.equal((sonnetResult as any).reasoning_effort, "max");
const opusBody = {
model: "claude-opus-4-6",
reasoning: { effort: "max", summary: "auto" },
input: [],
};
const opusResult = sanitizeReasoningEffortForProvider(
opusBody,
"anthropic-compatible-cc-test",
"claude-opus-4-6",
null
);
assert.equal(opusResult, opusBody);
assert.equal((opusResult as any).reasoning.effort, "max");
const haikuBody = {
model: "claude-haiku-4-5-20251001",
reasoning_effort: "max",
messages: [{ role: "user", content: "hi" }],
};
const haikuResult = sanitizeReasoningEffortForProvider(
haikuBody,
"claude",
"claude-haiku-4-5-20251001",
null
);
assert.notEqual(haikuResult, haikuBody);
assert.equal((haikuResult as any).reasoning_effort, "high");
});
test("sanitizeReasoningEffortForProvider: xiaomi-mimo downgrades xhigh in nested reasoning.effort", () => {
const body = {
model: "mimo-v2.5-pro",

View File

@@ -53,6 +53,10 @@ test("Claude Code compatible effort and max token helpers cover priority fallbac
resolveClaudeCodeCompatibleEffort({ output_config: { effort: "xhigh" } }, null, xhighModel.id),
"xhigh"
);
assert.equal(
resolveClaudeCodeCompatibleEffort({ output_config: { effort: "max" } }, null, xhighModel.id),
"max"
);
assert.equal(
resolveClaudeCodeCompatibleEffort(
{ output_config: { effort: "xhigh" } },
@@ -61,6 +65,18 @@ test("Claude Code compatible effort and max token helpers cover priority fallbac
),
"high"
);
assert.equal(
resolveClaudeCodeCompatibleEffort({ output_config: { effort: "max" } }, null, standardModel.id),
"max"
);
assert.equal(
resolveClaudeCodeCompatibleEffort(
{ output_config: { effort: "max" } },
null,
"claude-haiku-4-5-20251001"
),
"high"
);
assert.equal(
resolveClaudeCodeCompatibleEffort({ output_config: { effort: "unexpected" } }),
"high"

View File

@@ -10,6 +10,7 @@ import {
getModelsByProviderId,
getProviderModels,
isValidModel,
supportsClaudeMaxEffort,
supportsXHighEffort,
} from "../../open-sse/config/providerModels.ts";
@@ -92,6 +93,19 @@ test("Kiro registry exposes the current CLI model lineup with context windows",
assert.equal(byId.has("claude-haiku-4-5"), false);
});
test("Claude max effort support excludes Haiku family and non-Claude IDs", () => {
assert.equal(supportsClaudeMaxEffort("claude-opus-4-7"), true);
assert.equal(supportsClaudeMaxEffort("claude-opus-4-6"), true);
assert.equal(supportsClaudeMaxEffort("claude-sonnet-4-6"), true);
assert.equal(supportsClaudeMaxEffort("claude-sonnet-4-5-20250929"), true);
assert.equal(supportsClaudeMaxEffort("claude-haiku-4-5-20251001"), false);
assert.equal(supportsClaudeMaxEffort("claude-3-5-haiku-20241022"), false);
assert.equal(supportsClaudeMaxEffort("anthropic/claude-haiku-4.5"), false);
assert.equal(supportsClaudeMaxEffort("vendor/haiku-compatible-claude-sonnet-4-6"), true);
assert.equal(supportsClaudeMaxEffort("gpt-5"), false);
assert.equal(supportsClaudeMaxEffort("claude-future-5-0"), true);
});
test("Claude xhigh effort support defaults on for new models and opts out legacy models", () => {
const claudeModels = new Set(getModelsByProviderId("claude").map((model) => model.id));

View File

@@ -307,6 +307,32 @@ test("OpenAI -> Claude preserves xhigh only for Claude models that expose it", (
assert.equal(downgraded.max_tokens, 128000);
});
test("OpenAI -> Claude preserves max effort except for Haiku models", () => {
const preserved = openaiToClaudeRequest(
"claude-sonnet-4-6",
{
messages: [{ role: "user", content: "Think at max" }],
reasoning_effort: "max",
},
false
);
const haiku = openaiToClaudeRequest(
"claude-haiku-4-5-20251001",
{
messages: [{ role: "user", content: "Think at max" }],
max_tokens: 10,
reasoning_effort: "max",
},
false
);
assert.deepEqual(preserved.thinking, { type: "adaptive" });
assert.deepEqual(preserved.output_config, { effort: "max" });
assert.equal(haiku.output_config, undefined);
assert.deepEqual(haiku.thinking, { type: "enabled", budget_tokens: 62976 });
assert.equal(haiku.max_tokens, 64000);
});
test("OpenAI -> Claude fits thinking budget within Opus 4.7 output cap (regression)", () => {
// Real-world OpenCode scenario: caller asks for max_tokens=32000 with high effort.
// High effort maps to budget=131072. The previous naive