Skip to content
Closed
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
11 changes: 11 additions & 0 deletions open-sse/config/providerModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
21 changes: 15 additions & 6 deletions open-sse/config/providerRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface RegistryModel {
toolCalling?: boolean;
supportsReasoning?: boolean;
supportsVision?: boolean;
supportsXHighEffort?: boolean;
targetFormat?: string;
unsupportedParams?: readonly string[];
/** Maximum context window in tokens */
Expand Down Expand Up @@ -277,12 +278,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,
},
],
},

Expand Down
13 changes: 10 additions & 3 deletions open-sse/services/claudeCodeCompatible.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createHash, randomUUID } from "node:crypto";

import { getStainlessTimeoutSeconds } from "@/shared/utils/runtimeTimeouts";
import { supportsXHighEffort } from "../config/providerModels.ts";
import { prepareClaudeRequest } from "../translator/helpers/claudeHelper.ts";
import { signRequestBody } from "./claudeCodeCCH.ts";
import { computeFingerprint, extractFirstUserMessageText } from "./claudeCodeFingerprint.ts";
Expand Down Expand Up @@ -71,6 +72,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);
}
Expand Down Expand Up @@ -331,7 +336,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"]) ||
Expand All @@ -342,14 +347,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";
Expand Down
44 changes: 30 additions & 14 deletions open-sse/translator/request/openai-to-claude.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
};
result.output_config = {
...(result.output_config || {}),
effort: "xhigh",
};
} else {
const effortBudgetMap: Record<string, number> = {
low: 1024,
medium: 10240,
high: 131072,
max: 131072,
};
// Claude requires max_tokens > budget_tokens
if (result.max_tokens <= budget) {
result.max_tokens = budget + 8192;
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;
}
}
}
}
Expand Down
22 changes: 22 additions & 0 deletions tests/unit/cc-compatible-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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: {
Expand Down
24 changes: 23 additions & 1 deletion tests/unit/claude-code-compatible-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(""), "");
Expand All @@ -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"
Expand Down
37 changes: 37 additions & 0 deletions tests/unit/translator-openai-to-claude.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -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: [
Expand Down