feat: Support xhigh only on Claude models that expose it

Closes #1356
This commit is contained in:
diegosouzapw
2026-04-17 16:58:27 -03:00
parent 5ead25829f
commit 792a1cb2ab
7 changed files with 148 additions and 24 deletions

View File

@@ -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;
}

View File

@@ -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<string, RegistryEntry> = {
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,
},
],
},

View File

@@ -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<string, unknown> | null,
normalizedBody?: Record<string, unknown> | 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";

View File

@@ -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, unknown> | string;
thinking?: Record<string, unknown>;
output_config?: Record<string, unknown>;
_toolNameMap?: Map<string, string>;
} = {
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<string, number> = {
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<string, number> = {
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;
}
}
}
}

View File

@@ -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: {

View File

@@ -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"

View File

@@ -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: [