mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user