mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 00:02:20 +03:00
fix: repair cc compatible v1 route handling
This commit is contained in:
@@ -5,7 +5,7 @@ import { getRotatingApiKey } from "../services/apiKeyRotator.ts";
|
||||
import {
|
||||
buildClaudeCodeCompatibleHeaders,
|
||||
CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH,
|
||||
joinBaseUrlAndPath,
|
||||
joinClaudeCodeCompatibleUrl,
|
||||
} from "../services/claudeCodeCompatible.ts";
|
||||
import { isClaudeCodeCompatible } from "../services/provider.ts";
|
||||
|
||||
@@ -32,7 +32,10 @@ export class DefaultExecutor extends BaseExecutor {
|
||||
const baseUrl = psd?.baseUrl || "https://api.anthropic.com/v1";
|
||||
const customPath = typeof psd?.chatPath === "string" && psd.chatPath ? psd.chatPath : null;
|
||||
if (isClaudeCodeCompatible(this.provider)) {
|
||||
return joinBaseUrlAndPath(baseUrl, customPath || CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH);
|
||||
return joinClaudeCodeCompatibleUrl(
|
||||
baseUrl,
|
||||
customPath || CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH
|
||||
);
|
||||
}
|
||||
const normalized = baseUrl.replace(/\/$/, "");
|
||||
return `${normalized}${customPath || "/messages"}`;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
|
||||
export const CLAUDE_CODE_COMPATIBLE_PREFIX = "anthropic-compatible-cc-";
|
||||
export const CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH = "/messages?beta=true";
|
||||
export const CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH = "/v1/messages?beta=true";
|
||||
export const CLAUDE_CODE_COMPATIBLE_DEFAULT_MODELS_PATH = "/models";
|
||||
export const CLAUDE_CODE_COMPATIBLE_DEFAULT_MAX_TOKENS = 8092;
|
||||
export const CLAUDE_CODE_COMPATIBLE_ANTHROPIC_VERSION = "2023-06-01";
|
||||
@@ -45,14 +45,39 @@ export function stripAnthropicMessagesSuffix(baseUrl: string | null | undefined)
|
||||
return normalized.replace(/\/messages(?:\?[^#]*)?$/i, "");
|
||||
}
|
||||
|
||||
export function joinBaseUrlAndPath(baseUrl: string, path: string): string {
|
||||
const normalizedBase = stripAnthropicMessagesSuffix(baseUrl).replace(/\/$/, "");
|
||||
export function stripClaudeCodeCompatibleEndpointSuffix(
|
||||
baseUrl: string | null | undefined
|
||||
): string {
|
||||
const normalized = String(baseUrl || "")
|
||||
.trim()
|
||||
.replace(/\/$/, "");
|
||||
if (!normalized) return "";
|
||||
return normalized.replace(/\/(?:v\d+\/)?messages(?:\?[^#]*)?$/i, "");
|
||||
}
|
||||
|
||||
function joinNormalizedBaseUrlAndPath(baseUrl: string, path: string): string {
|
||||
const normalizedBase = String(baseUrl || "").replace(/\/$/, "");
|
||||
const normalizedPath = String(path || "").startsWith("/")
|
||||
? String(path)
|
||||
: `/${String(path || "")}`;
|
||||
const versionMatch = normalizedBase.match(/(\/v\d+)$/i);
|
||||
if (
|
||||
versionMatch &&
|
||||
normalizedPath.toLowerCase().startsWith(`${versionMatch[1].toLowerCase()}/`)
|
||||
) {
|
||||
return `${normalizedBase}${normalizedPath.slice(versionMatch[1].length)}`;
|
||||
}
|
||||
return `${normalizedBase}${normalizedPath}`;
|
||||
}
|
||||
|
||||
export function joinBaseUrlAndPath(baseUrl: string, path: string): string {
|
||||
return joinNormalizedBaseUrlAndPath(stripAnthropicMessagesSuffix(baseUrl), path);
|
||||
}
|
||||
|
||||
export function joinClaudeCodeCompatibleUrl(baseUrl: string, path: string): string {
|
||||
return joinNormalizedBaseUrlAndPath(stripClaudeCodeCompatibleEndpointSuffix(baseUrl), path);
|
||||
}
|
||||
|
||||
export function buildClaudeCodeCompatibleHeaders(
|
||||
apiKey: string,
|
||||
stream = false,
|
||||
@@ -340,14 +365,12 @@ function convertClaudeCodeCompatibleTool(tool: unknown) {
|
||||
const rawTool = readRecord(tool);
|
||||
if (!rawTool) return null;
|
||||
|
||||
const toolData =
|
||||
rawTool.type === "function" ? readRecord(rawTool.function) || rawTool : rawTool;
|
||||
const toolData = rawTool.type === "function" ? readRecord(rawTool.function) || rawTool : rawTool;
|
||||
|
||||
const name = toNonEmptyString(toolData.name);
|
||||
if (!name) return null;
|
||||
|
||||
const rawSchema =
|
||||
readRecord(toolData.parameters) ||
|
||||
const rawSchema = readRecord(toolData.parameters) ||
|
||||
readRecord(toolData.input_schema) || { type: "object", properties: {}, required: [] };
|
||||
const inputSchema =
|
||||
rawSchema.type === "object" && !readRecord(rawSchema.properties)
|
||||
|
||||
@@ -3,8 +3,7 @@ import { getRegistryEntry } from "../config/providerRegistry.ts";
|
||||
import {
|
||||
buildClaudeCodeCompatibleHeaders,
|
||||
CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH,
|
||||
joinBaseUrlAndPath,
|
||||
stripAnthropicMessagesSuffix,
|
||||
joinClaudeCodeCompatibleUrl,
|
||||
} from "./claudeCodeCompatible.ts";
|
||||
|
||||
const OPENAI_COMPATIBLE_PREFIX = "openai-compatible-";
|
||||
@@ -205,7 +204,7 @@ export function buildProviderUrl(
|
||||
if (isAnthropicCompatible(provider)) {
|
||||
const baseUrl = options?.baseUrl || ANTHROPIC_COMPATIBLE_DEFAULTS.baseUrl;
|
||||
if (isClaudeCodeCompatible(provider)) {
|
||||
return joinBaseUrlAndPath(baseUrl, CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH);
|
||||
return joinClaudeCodeCompatibleUrl(baseUrl, CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH);
|
||||
}
|
||||
return buildAnthropicCompatibleUrl(baseUrl);
|
||||
}
|
||||
|
||||
@@ -470,7 +470,7 @@ interface EditCompatibleNodeModalProps {
|
||||
|
||||
const CC_COMPATIBLE_LABEL = "CC Compatible";
|
||||
const CC_COMPATIBLE_DETAILS_TITLE = "CC Compatible Details";
|
||||
const CC_COMPATIBLE_DEFAULT_CHAT_PATH = "/messages?beta=true";
|
||||
const CC_COMPATIBLE_DEFAULT_CHAT_PATH = "/v1/messages?beta=true";
|
||||
const CC_COMPATIBLE_DEFAULT_MODELS_PATH = "/models";
|
||||
|
||||
function normalizeCodexLimitPolicy(policy: unknown): { use5h: boolean; useWeekly: boolean } {
|
||||
@@ -5104,7 +5104,11 @@ function EditCompatibleNodeModal({
|
||||
apiType: node.apiType || "chat",
|
||||
baseUrl:
|
||||
node.baseUrl ||
|
||||
(isAnthropic ? "https://api.anthropic.com/v1" : "https://api.openai.com/v1"),
|
||||
(isCcCompatible
|
||||
? "https://api.anthropic.com"
|
||||
: isAnthropic
|
||||
? "https://api.anthropic.com/v1"
|
||||
: "https://api.openai.com/v1"),
|
||||
chatPath: node.chatPath || (isCcCompatible ? CC_COMPATIBLE_DEFAULT_CHAT_PATH : ""),
|
||||
modelsPath: node.modelsPath || (isCcCompatible ? CC_COMPATIBLE_DEFAULT_MODELS_PATH : ""),
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ import { useTranslations } from "next-intl";
|
||||
|
||||
const CC_COMPATIBLE_LABEL = "CC Compatible";
|
||||
const ADD_CC_COMPATIBLE_LABEL = "Add CC Compatible";
|
||||
const CC_COMPATIBLE_DEFAULT_CHAT_PATH = "/messages?beta=true";
|
||||
const CC_COMPATIBLE_DEFAULT_CHAT_PATH = "/v1/messages?beta=true";
|
||||
const CC_COMPATIBLE_DEFAULT_MODELS_PATH = "/models";
|
||||
|
||||
// Shared helper function to avoid code duplication between ProviderCard and ApiKeyProviderCard
|
||||
@@ -1295,7 +1295,7 @@ function AddCcCompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
prefix: "",
|
||||
baseUrl: "https://api.anthropic.com/v1",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
chatPath: CC_COMPATIBLE_DEFAULT_CHAT_PATH,
|
||||
modelsPath: CC_COMPATIBLE_DEFAULT_MODELS_PATH,
|
||||
});
|
||||
@@ -1335,7 +1335,7 @@ function AddCcCompatibleModal({ isOpen, onClose, onCreated }) {
|
||||
setFormData({
|
||||
name: "",
|
||||
prefix: "",
|
||||
baseUrl: "https://api.anthropic.com/v1",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
chatPath: CC_COMPATIBLE_DEFAULT_CHAT_PATH,
|
||||
modelsPath: CC_COMPATIBLE_DEFAULT_MODELS_PATH,
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
updateProviderConnection,
|
||||
updateProviderNode,
|
||||
} from "@/models";
|
||||
import { isClaudeCodeCompatibleProvider } from "@/shared/constants/providers";
|
||||
import { updateProviderNodeSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
|
||||
@@ -16,6 +17,20 @@ function asRecord(value: unknown): JsonRecord {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
|
||||
}
|
||||
|
||||
function sanitizeAnthropicBaseUrl(baseUrl: string) {
|
||||
return (baseUrl || "")
|
||||
.trim()
|
||||
.replace(/\/$/, "")
|
||||
.replace(/\/messages(?:\?[^#]*)?$/i, "");
|
||||
}
|
||||
|
||||
function sanitizeClaudeCodeCompatibleBaseUrl(baseUrl: string) {
|
||||
return (baseUrl || "")
|
||||
.trim()
|
||||
.replace(/\/$/, "")
|
||||
.replace(/\/(?:v\d+\/)?messages(?:\?[^#]*)?$/i, "");
|
||||
}
|
||||
|
||||
// PUT /api/provider-nodes/[id] - Update provider node
|
||||
export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
let rawBody;
|
||||
@@ -58,8 +73,9 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
|
||||
|
||||
// Sanitize Base URL for Anthropic Compatible
|
||||
if (node.type === "anthropic-compatible") {
|
||||
sanitizedBaseUrl = sanitizedBaseUrl.replace(/\/$/, "");
|
||||
sanitizedBaseUrl = sanitizedBaseUrl.replace(/\/messages(?:\?[^#]*)?$/i, "");
|
||||
sanitizedBaseUrl = isClaudeCodeCompatibleProvider(id)
|
||||
? sanitizeClaudeCodeCompatibleBaseUrl(sanitizedBaseUrl)
|
||||
: sanitizeAnthropicBaseUrl(sanitizedBaseUrl);
|
||||
}
|
||||
|
||||
const updates: Record<string, unknown> = {
|
||||
|
||||
@@ -25,6 +25,13 @@ function sanitizeAnthropicBaseUrl(baseUrl: string) {
|
||||
.replace(/\/messages(?:\?[^#]*)?$/i, "");
|
||||
}
|
||||
|
||||
function sanitizeClaudeCodeCompatibleBaseUrl(baseUrl: string) {
|
||||
return (baseUrl || "")
|
||||
.trim()
|
||||
.replace(/\/$/, "")
|
||||
.replace(/\/(?:v\d+\/)?messages(?:\?[^#]*)?$/i, "");
|
||||
}
|
||||
|
||||
// GET /api/provider-nodes - List all provider nodes
|
||||
export async function GET() {
|
||||
try {
|
||||
@@ -86,9 +93,11 @@ export async function POST(request) {
|
||||
return NextResponse.json({ error: "CC Compatible provider is disabled" }, { status: 403 });
|
||||
}
|
||||
|
||||
const sanitizedBaseUrl = sanitizeAnthropicBaseUrl(
|
||||
baseUrl || ANTHROPIC_COMPATIBLE_DEFAULTS.baseUrl
|
||||
);
|
||||
const rawBaseUrl = baseUrl || ANTHROPIC_COMPATIBLE_DEFAULTS.baseUrl;
|
||||
const sanitizedBaseUrl =
|
||||
compatMode === "cc"
|
||||
? sanitizeClaudeCodeCompatibleBaseUrl(rawBaseUrl)
|
||||
: sanitizeAnthropicBaseUrl(rawBaseUrl);
|
||||
|
||||
const node = await createProviderNode({
|
||||
id:
|
||||
|
||||
@@ -11,6 +11,13 @@ function sanitizeAnthropicBaseUrl(baseUrl: string) {
|
||||
.replace(/\/messages(?:\?[^#]*)?$/i, "");
|
||||
}
|
||||
|
||||
function sanitizeClaudeCodeCompatibleBaseUrl(baseUrl: string) {
|
||||
return (baseUrl || "")
|
||||
.trim()
|
||||
.replace(/\/$/, "")
|
||||
.replace(/\/(?:v\d+\/)?messages(?:\?[^#]*)?$/i, "");
|
||||
}
|
||||
|
||||
// POST /api/provider-nodes/validate - Validate API key against base URL
|
||||
export async function POST(request) {
|
||||
let rawBody;
|
||||
@@ -48,7 +55,7 @@ export async function POST(request) {
|
||||
const result = await validateClaudeCodeCompatibleProvider({
|
||||
apiKey,
|
||||
providerSpecificData: {
|
||||
baseUrl: sanitizeAnthropicBaseUrl(baseUrl),
|
||||
baseUrl: sanitizeClaudeCodeCompatibleBaseUrl(baseUrl),
|
||||
chatPath: chatPath || undefined,
|
||||
modelsPath: modelsPath || undefined,
|
||||
},
|
||||
|
||||
@@ -4,7 +4,9 @@ import {
|
||||
buildClaudeCodeCompatibleValidationPayload,
|
||||
CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH,
|
||||
CLAUDE_CODE_COMPATIBLE_DEFAULT_MODELS_PATH,
|
||||
joinClaudeCodeCompatibleUrl,
|
||||
joinBaseUrlAndPath,
|
||||
stripClaudeCodeCompatibleEndpointSuffix,
|
||||
stripAnthropicMessagesSuffix,
|
||||
} from "@omniroute/open-sse/services/claudeCodeCompatible.ts";
|
||||
import {
|
||||
@@ -24,6 +26,10 @@ function normalizeAnthropicBaseUrl(baseUrl: string) {
|
||||
return stripAnthropicMessagesSuffix(baseUrl || "");
|
||||
}
|
||||
|
||||
function normalizeClaudeCodeCompatibleBaseUrl(baseUrl: string) {
|
||||
return stripClaudeCodeCompatibleEndpointSuffix(baseUrl || "");
|
||||
}
|
||||
|
||||
function addModelsSuffix(baseUrl: string) {
|
||||
const normalized = normalizeBaseUrl(baseUrl);
|
||||
if (!normalized) return "";
|
||||
@@ -576,7 +582,7 @@ export async function validateClaudeCodeCompatibleProvider({
|
||||
apiKey,
|
||||
providerSpecificData = {},
|
||||
}: any) {
|
||||
const baseUrl = normalizeAnthropicBaseUrl(providerSpecificData.baseUrl);
|
||||
const baseUrl = normalizeClaudeCodeCompatibleBaseUrl(providerSpecificData.baseUrl);
|
||||
if (!baseUrl) {
|
||||
return { valid: false, error: "No base URL configured for CC Compatible provider" };
|
||||
}
|
||||
@@ -586,7 +592,7 @@ export async function validateClaudeCodeCompatibleProvider({
|
||||
const defaultHeaders = buildClaudeCodeCompatibleHeaders(apiKey, false);
|
||||
|
||||
try {
|
||||
const modelsRes = await fetch(joinBaseUrlAndPath(baseUrl, modelsPath), {
|
||||
const modelsRes = await fetch(joinClaudeCodeCompatibleUrl(baseUrl, modelsPath), {
|
||||
method: "GET",
|
||||
headers: defaultHeaders,
|
||||
});
|
||||
@@ -608,7 +614,7 @@ export async function validateClaudeCodeCompatibleProvider({
|
||||
const sessionId = JSON.parse(payload.metadata.user_id).session_id;
|
||||
|
||||
try {
|
||||
const messagesRes = await fetch(joinBaseUrlAndPath(baseUrl, chatPath), {
|
||||
const messagesRes = await fetch(joinClaudeCodeCompatibleUrl(baseUrl, chatPath), {
|
||||
method: "POST",
|
||||
headers: buildClaudeCodeCompatibleHeaders(apiKey, false, sessionId),
|
||||
body: JSON.stringify(payload),
|
||||
|
||||
@@ -14,6 +14,7 @@ const {
|
||||
CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH,
|
||||
CLAUDE_CODE_COMPATIBLE_DEFAULT_MAX_TOKENS,
|
||||
CLAUDE_CODE_COMPATIBLE_DEFAULT_MODELS_PATH,
|
||||
joinClaudeCodeCompatibleUrl,
|
||||
} = await import("../../open-sse/services/claudeCodeCompatible.ts");
|
||||
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
|
||||
const providerNodesRoute = await import("../../src/app/api/provider-nodes/route.ts");
|
||||
@@ -159,6 +160,30 @@ test("buildClaudeCodeCompatibleRequest omits auto tool_choice while preserving t
|
||||
assert.equal(payload.tool_choice, undefined);
|
||||
});
|
||||
|
||||
test("joinClaudeCodeCompatibleUrl preserves a single /v1 segment for CC paths", () => {
|
||||
assert.equal(
|
||||
joinClaudeCodeCompatibleUrl(
|
||||
"https://proxy.example.com",
|
||||
CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH
|
||||
),
|
||||
"https://proxy.example.com/v1/messages?beta=true"
|
||||
);
|
||||
assert.equal(
|
||||
joinClaudeCodeCompatibleUrl(
|
||||
"https://proxy.example.com/v1",
|
||||
CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH
|
||||
),
|
||||
"https://proxy.example.com/v1/messages?beta=true"
|
||||
);
|
||||
assert.equal(
|
||||
joinClaudeCodeCompatibleUrl(
|
||||
"https://proxy.example.com/v1/messages?beta=true",
|
||||
CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH
|
||||
),
|
||||
"https://proxy.example.com/v1/messages?beta=true"
|
||||
);
|
||||
});
|
||||
|
||||
test("DefaultExecutor uses CC-compatible path and headers", () => {
|
||||
const executor = new DefaultExecutor("anthropic-compatible-cc-test");
|
||||
const credentials = {
|
||||
@@ -172,7 +197,7 @@ test("DefaultExecutor uses CC-compatible path and headers", () => {
|
||||
|
||||
assert.equal(
|
||||
executor.buildUrl("claude-sonnet-4-6", true, 0, credentials),
|
||||
`https://proxy.example.com/v1${CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH}`
|
||||
"https://proxy.example.com/v1/messages?beta=true"
|
||||
);
|
||||
|
||||
const headers = executor.buildHeaders(credentials, true);
|
||||
@@ -213,10 +238,7 @@ test("validateProviderApiKey uses CC skeleton request after /models fallback", a
|
||||
assert.match(result.warning, /reached upstream/i);
|
||||
assert.deepEqual(
|
||||
calls.map((call) => `${call.method} ${call.url}`),
|
||||
[
|
||||
"GET https://proxy.example.com/v1/models",
|
||||
"POST https://proxy.example.com/v1/messages?beta=true",
|
||||
]
|
||||
["GET https://proxy.example.com/models", "POST https://proxy.example.com/v1/messages?beta=true"]
|
||||
);
|
||||
assert.equal(calls[1].body.model, "claude-sonnet-4-6");
|
||||
assert.equal(calls[1].body.messages[0].role, "user");
|
||||
@@ -265,7 +287,7 @@ test("provider-nodes create route creates CC node with dedicated prefix when ena
|
||||
assert.equal(response.status, 201);
|
||||
const data = await response.json();
|
||||
assert.match(data.node.id, /^anthropic-compatible-cc-/);
|
||||
assert.equal(data.node.baseUrl, "https://proxy.example.com/v1");
|
||||
assert.equal(data.node.baseUrl, "https://proxy.example.com");
|
||||
assert.equal(data.node.chatPath, CLAUDE_CODE_COMPATIBLE_DEFAULT_CHAT_PATH);
|
||||
assert.equal(data.node.modelsPath, CLAUDE_CODE_COMPATIBLE_DEFAULT_MODELS_PATH);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user