From 792a1cb2ab9906985580cedd684d04806b5efd95 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Fri, 17 Apr 2026 16:58:27 -0300 Subject: [PATCH] feat: Support xhigh only on Claude models that expose it Closes #1356 --- open-sse/config/providerModels.ts | 11 +++++ open-sse/config/providerRegistry.ts | 21 ++++++--- open-sse/services/claudeCodeCompatible.ts | 13 ++++-- .../translator/request/openai-to-claude.ts | 44 +++++++++++++------ tests/unit/cc-compatible-provider.test.ts | 22 ++++++++++ .../claude-code-compatible-request.test.ts | 24 +++++++++- .../unit/translator-openai-to-claude.test.ts | 37 ++++++++++++++++ 7 files changed, 148 insertions(+), 24 deletions(-) diff --git a/open-sse/config/providerModels.ts b/open-sse/config/providerModels.ts index 63cd97ad91..a1c37e4d87 100644 --- a/open-sse/config/providerModels.ts +++ b/open-sse/config/providerModels.ts @@ -16,6 +16,12 @@ export function getDefaultModel(aliasOrId: string): string | null { return models?.[0]?.id || null; } +export function getProviderModel(aliasOrId: string, modelId: string): RegistryModel | undefined { + const models = PROVIDER_MODELS[aliasOrId]; + if (!models) return undefined; + return models.find((model) => model.id === modelId); +} + export function isValidModel( aliasOrId: string, modelId: string, @@ -45,3 +51,8 @@ export function getModelsByProviderId(providerId: string): RegistryModel[] { const alias = PROVIDER_ID_TO_ALIAS[providerId] || providerId; return PROVIDER_MODELS[alias] || []; } + +export function supportsXHighEffort(aliasOrId: string, modelId: string): boolean { + const alias = PROVIDER_ID_TO_ALIAS[aliasOrId] || aliasOrId; + return getProviderModel(alias, modelId)?.supportsXHighEffort === true; +} diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index 2340954fd7..bfddee6860 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -43,6 +43,7 @@ export interface RegistryModel { toolCalling?: boolean; supportsReasoning?: boolean; supportsVision?: boolean; + supportsXHighEffort?: boolean; targetFormat?: string; unsupportedParams?: readonly string[]; /** Maximum context window in tokens */ @@ -293,12 +294,20 @@ export const REGISTRY: Record = { tokenUrl: "https://console.anthropic.com/v1/oauth/token", }, models: [ - { id: "claude-opus-4-7", name: "Claude Opus 4.7" }, - { id: "claude-opus-4-6", name: "Claude Opus 4.6" }, - { id: "claude-sonnet-4-6", name: "Claude 4.6 Sonnet" }, - { id: "claude-opus-4-5-20251101", name: "Claude 4.5 Opus" }, - { id: "claude-sonnet-4-5-20250929", name: "Claude 4.5 Sonnet" }, - { id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" }, + { id: "claude-opus-4-7", name: "Claude Opus 4.7", supportsXHighEffort: true }, + { id: "claude-opus-4-6", name: "Claude Opus 4.6", supportsXHighEffort: false }, + { id: "claude-sonnet-4-6", name: "Claude 4.6 Sonnet", supportsXHighEffort: false }, + { id: "claude-opus-4-5-20251101", name: "Claude 4.5 Opus", supportsXHighEffort: false }, + { + id: "claude-sonnet-4-5-20250929", + name: "Claude 4.5 Sonnet", + supportsXHighEffort: false, + }, + { + id: "claude-haiku-4-5-20251001", + name: "Claude 4.5 Haiku", + supportsXHighEffort: false, + }, ], }, diff --git a/open-sse/services/claudeCodeCompatible.ts b/open-sse/services/claudeCodeCompatible.ts index 065840108a..b0246cce1e 100644 --- a/open-sse/services/claudeCodeCompatible.ts +++ b/open-sse/services/claudeCodeCompatible.ts @@ -9,6 +9,7 @@ import { CLAUDE_CLI_USER_AGENT, CLAUDE_CLI_VERSION, } from "../config/anthropicHeaders.ts"; +import { supportsXHighEffort } from "../config/providerModels.ts"; import { prepareClaudeRequest } from "../translator/helpers/claudeHelper.ts"; import { signRequestBody } from "./claudeCodeCCH.ts"; import { computeFingerprint, extractFirstUserMessageText } from "./claudeCodeFingerprint.ts"; @@ -78,6 +79,10 @@ type BuildRequestOptions = { preserveCacheControl?: boolean; }; +function supportsClaudeXHighEffort(model: string | null | undefined): boolean { + return typeof model === "string" && supportsXHighEffort("claude", model); +} + export function isClaudeCodeCompatibleProvider(provider: string | null | undefined): boolean { return typeof provider === "string" && provider.startsWith(CLAUDE_CODE_COMPATIBLE_PREFIX); } @@ -338,7 +343,7 @@ export function resolveClaudeCodeCompatibleEffort( sourceBody?: Record | null, normalizedBody?: Record | null, model?: string | null -): "low" | "medium" | "high" { +): "low" | "medium" | "high" | "xhigh" { const raw = readNestedString(sourceBody, ["output_config", "effort"]) || readNestedString(sourceBody, ["reasoning", "effort"]) || @@ -349,14 +354,16 @@ export function resolveClaudeCodeCompatibleEffort( ""; const normalizedEffort = raw.toLowerCase(); - void model; if (!normalizedEffort) return "high"; if (normalizedEffort === "low") return "low"; if (normalizedEffort === "medium") return "medium"; if (normalizedEffort === "high") return "high"; if (normalizedEffort === "none" || normalizedEffort === "disabled") return "low"; - if (normalizedEffort === "max" || normalizedEffort === "xhigh") { + if (normalizedEffort === "xhigh") { + return supportsClaudeXHighEffort(model) ? "xhigh" : "high"; + } + if (normalizedEffort === "max") { return "high"; } return "high"; diff --git a/open-sse/translator/request/openai-to-claude.ts b/open-sse/translator/request/openai-to-claude.ts index 47aeac8fb8..3b09912672 100644 --- a/open-sse/translator/request/openai-to-claude.ts +++ b/open-sse/translator/request/openai-to-claude.ts @@ -1,6 +1,7 @@ import { register } from "../registry.ts"; import { FORMATS } from "../formats.ts"; import { CLAUDE_SYSTEM_PROMPT } from "../../config/constants.ts"; +import { 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"; @@ -99,6 +100,7 @@ export function openaiToClaudeRequest(model, body, stream) { tools?: ClaudeTool[]; tool_choice?: Record | string; thinking?: Record; + output_config?: Record; _toolNameMap?: Map; } = { model: model, @@ -333,22 +335,36 @@ export function openaiToClaudeRequest(model, body, stream) { } else if (body.reasoning_effort) { // Convert OpenAI reasoning_effort to Claude thinking format (#627) // Clients like OpenCode send reasoning_effort via @ai-sdk/openai-compatible - const effortBudgetMap: Record = { - low: 1024, - medium: 10240, - high: 131072, - max: 131072, - }; - const effort = String(body.reasoning_effort).toLowerCase(); - const budget = effortBudgetMap[effort]; - if (budget !== undefined && budget > 0) { + const requestedEffort = String(body.reasoning_effort).toLowerCase(); + const normalizedEffort = + requestedEffort === "xhigh" && !supportsXHighEffort("claude", model) + ? "high" + : requestedEffort; + if (normalizedEffort === "xhigh") { result.thinking = { - type: "enabled", - budget_tokens: budget, + type: "adaptive", }; - // Claude requires max_tokens > budget_tokens - if (result.max_tokens <= budget) { - result.max_tokens = budget + 8192; + result.output_config = { + ...(result.output_config || {}), + effort: "xhigh", + }; + } else { + const effortBudgetMap: Record = { + low: 1024, + medium: 10240, + high: 131072, + max: 131072, + }; + const budget = effortBudgetMap[normalizedEffort]; + if (budget !== undefined && budget > 0) { + result.thinking = { + type: "enabled", + budget_tokens: budget, + }; + // Claude requires max_tokens > budget_tokens + if (result.max_tokens <= budget) { + result.max_tokens = budget + 8192; + } } } } diff --git a/tests/unit/cc-compatible-provider.test.ts b/tests/unit/cc-compatible-provider.test.ts index cbdb5c0164..6f3ced1708 100644 --- a/tests/unit/cc-compatible-provider.test.ts +++ b/tests/unit/cc-compatible-provider.test.ts @@ -18,6 +18,7 @@ const { CLAUDE_CODE_COMPATIBLE_DEFAULT_MODELS_PATH, joinClaudeCodeCompatibleUrl, } = await import("../../open-sse/services/claudeCodeCompatible.ts"); +const { getModelsByProviderId } = await import("../../open-sse/config/providerModels.ts"); const { handleChatCore } = await import("../../open-sse/handlers/chatCore.ts"); const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts"); const providerNodesRoute = await import("../../src/app/api/provider-nodes/route.ts"); @@ -142,6 +143,27 @@ test("buildClaudeCodeCompatibleRequest keeps prior role history while dropping t assert.equal(JSON.parse(payload.metadata.user_id).session_id, "session-1"); }); +test("buildClaudeCodeCompatibleRequest preserves xhigh for Claude models that support it", () => { + const xhighModel = getModelsByProviderId("claude").find( + (model) => model.supportsXHighEffort === true + ); + assert.ok(xhighModel, "expected at least one Claude model with xhigh support"); + const payload = buildClaudeCodeCompatibleRequest({ + sourceBody: { + reasoning_effort: "xhigh", + }, + normalizedBody: { + messages: [{ role: "user", content: "u1" }], + }, + model: xhighModel.id, + cwd: "/tmp/work", + now: new Date("2026-04-01T12:00:00.000Z"), + }); + + assert.equal(payload.output_config.effort, "xhigh"); + assert.equal(payload.thinking.type, "adaptive"); +}); + test("buildClaudeCodeCompatibleRequest preserves Claude cache markers when requested", () => { const payload = buildClaudeCodeCompatibleRequest({ sourceBody: { diff --git a/tests/unit/claude-code-compatible-request.test.ts b/tests/unit/claude-code-compatible-request.test.ts index 3e97d937c2..441002d955 100644 --- a/tests/unit/claude-code-compatible-request.test.ts +++ b/tests/unit/claude-code-compatible-request.test.ts @@ -11,6 +11,16 @@ const { resolveClaudeCodeCompatibleMaxTokens, buildClaudeCodeCompatibleRequest, } = await import("../../open-sse/services/claudeCodeCompatible.ts"); +const { getModelsByProviderId } = await import("../../open-sse/config/providerModels.ts"); + +function getClaudeEffortFixtures() { + const claudeModels = getModelsByProviderId("claude"); + const xhighModel = claudeModels.find((model) => model.supportsXHighEffort === true); + const standardModel = claudeModels.find((model) => model.supportsXHighEffort === false); + assert.ok(xhighModel, "expected at least one Claude model with xhigh support"); + assert.ok(standardModel, "expected at least one Claude model without xhigh support"); + return { xhighModel, standardModel }; +} test("Claude Code compatible URL helpers cover empty values, version trimming and legacy session headers", () => { assert.equal(stripClaudeCodeCompatibleEndpointSuffix(""), ""); @@ -33,9 +43,21 @@ test("Claude Code compatible URL helpers cover empty values, version trimming an }); test("Claude Code compatible effort and max token helpers cover priority fallbacks", () => { + const { xhighModel, standardModel } = getClaudeEffortFixtures(); assert.equal(resolveClaudeCodeCompatibleEffort({ reasoning_effort: "medium" }), "medium"); assert.equal(resolveClaudeCodeCompatibleEffort({ reasoning: { effort: "none" } }), "low"); - assert.equal(resolveClaudeCodeCompatibleEffort({ output_config: { effort: "xhigh" } }), "high"); + assert.equal( + resolveClaudeCodeCompatibleEffort({ output_config: { effort: "xhigh" } }, null, xhighModel.id), + "xhigh" + ); + assert.equal( + resolveClaudeCodeCompatibleEffort( + { output_config: { effort: "xhigh" } }, + null, + standardModel.id + ), + "high" + ); assert.equal( resolveClaudeCodeCompatibleEffort({ output_config: { effort: "unexpected" } }), "high" diff --git a/tests/unit/translator-openai-to-claude.test.ts b/tests/unit/translator-openai-to-claude.test.ts index 268974549c..04c773bee0 100644 --- a/tests/unit/translator-openai-to-claude.test.ts +++ b/tests/unit/translator-openai-to-claude.test.ts @@ -11,6 +11,16 @@ const { const { CLAUDE_SYSTEM_PROMPT } = await import("../../open-sse/config/constants.ts"); const { DEFAULT_THINKING_CLAUDE_SIGNATURE } = await import("../../open-sse/config/defaultThinkingSignature.ts"); +const { getModelsByProviderId } = await import("../../open-sse/config/providerModels.ts"); + +function getClaudeEffortFixtures() { + const claudeModels = getModelsByProviderId("claude"); + const xhighModel = claudeModels.find((model) => model.supportsXHighEffort === true); + const standardModel = claudeModels.find((model) => model.supportsXHighEffort === false); + assert.ok(xhighModel, "expected at least one Claude model with xhigh support"); + assert.ok(standardModel, "expected at least one Claude model without xhigh support"); + return { xhighModel, standardModel }; +} test("OpenAI -> Claude helpers normalize array content and strip empty nested text blocks", () => { const normalized = normalizeContentToString([ @@ -259,6 +269,33 @@ test("OpenAI -> Claude turns reasoning settings into thinking budgets and expand assert.equal(explicitThinkingResult.max_tokens, 10192); }); +test("OpenAI -> Claude preserves xhigh only for Claude models that expose it", () => { + const { xhighModel, standardModel } = getClaudeEffortFixtures(); + const preserved = openaiToClaudeRequest( + xhighModel.id, + { + messages: [{ role: "user", content: "Think harder" }], + reasoning_effort: "xhigh", + }, + false + ); + const downgraded = openaiToClaudeRequest( + standardModel.id, + { + messages: [{ role: "user", content: "Think harder" }], + max_tokens: 10, + reasoning_effort: "xhigh", + }, + false + ); + + assert.deepEqual(preserved.thinking, { type: "adaptive" }); + assert.deepEqual(preserved.output_config, { effort: "xhigh" }); + assert.deepEqual(downgraded.thinking, { type: "enabled", budget_tokens: 131072 }); + assert.equal(downgraded.output_config, undefined); + assert.equal(downgraded.max_tokens, 139264); +}); + test("OpenAI -> Claude can disable OAuth prefixes and Antigravity strips Claude-only prompting", () => { const baseBody = { messages: [