import { randomUUID, timingSafeEqual } from "node:crypto"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import { FunctionCallingConfigMode, GoogleGenAI, createPartFromFunctionResponse } from "@google/genai"; import { config as loadEnv } from "dotenv"; import { formatOpenAIAuthHint, resolveOpenAIAuth } from "../shared/openaiAuth.js"; import { openAICompatibleMessagesToResponsesInput, openAICompatibleToolsToResponsesTools, parseResponsesSseToResult, sliceResponsesInputToLatestToolTurn, } from "../shared/openaiResponsesCompat.js"; import { providerLabel, providerModelCatalog } from "../shared/providerModels.js"; loadEnv({ quiet: true }); type CompatProvider = "openai" | "gemini" | "groq" | "openrouter" | "ollama" | "copilot" | "zai"; type AnthropicMessageRequest = { model?: string; max_tokens?: number; stream?: boolean; system?: string | Array<{ type?: string; text?: string }>; messages?: Array<{ role: "user" | "assistant"; content: string | Array>; }>; tools?: Array>; tool_choice?: { type?: "auto" | "any" | "none" | "tool"; name?: string; }; }; type AnthropicTextBlock = { type: "text"; text: string; citations: null; }; type AnthropicToolUseBlock = { type: "tool_use"; id: string; name: string; input: Record; }; type AnthropicContentBlock = AnthropicTextBlock | AnthropicToolUseBlock; type AnthropicMessageResponse = { id: string; type: "message"; role: "assistant"; model: string; content: AnthropicContentBlock[]; stop_reason: "end_turn" | "tool_use"; stop_sequence: null; usage: { input_tokens: number; cache_creation_input_tokens: number | null; cache_read_input_tokens: number | null; output_tokens: number; server_tool_use: null; }; }; type GeminiContent = { role: "user" | "model"; parts: Array>; }; const PROVIDER = resolveProvider(); const PORT = Number(process.env.ANTHROPIC_COMPAT_PORT ?? process.env.GEMINI_PROXY_PORT ?? defaultPortFor(PROVIDER)); const ACTIVE_MODEL = resolveModel(PROVIDER); const OLLAMA_BASE_URL = normalizeBaseUrl(process.env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434"); const OLLAMA_API_KEY = process.env.OLLAMA_API_KEY?.trim(); const OLLAMA_KEEP_ALIVE = process.env.OLLAMA_KEEP_ALIVE?.trim(); const OLLAMA_NUM_CTX = parseOptionalInt(process.env.OLLAMA_NUM_CTX); const OLLAMA_NUM_PREDICT = parseOptionalInt(process.env.OLLAMA_NUM_PREDICT); const GEMINI_API_KEY = readConfiguredSecret(process.env.GEMINI_API_KEY); const GROQ_API_KEY = readConfiguredSecret(process.env.GROQ_API_KEY); const OPENROUTER_API_KEY = readConfiguredSecret(process.env.OPENROUTER_API_KEY); const OPENROUTER_BASE_URL = normalizeBaseUrl(process.env.OPENROUTER_BASE_URL ?? "https://openrouter.ai/api/v1"); const OPENROUTER_SITE_URL = process.env.OPENROUTER_SITE_URL?.trim(); const OPENROUTER_APP_NAME = process.env.OPENROUTER_APP_NAME?.trim(); const COPILOT_TOKEN = readConfiguredSecret(process.env.COPILOT_TOKEN) || readConfiguredSecret(process.env.GITHUB_MODELS_TOKEN) || readConfiguredSecret(process.env.GITHUB_TOKEN) || readConfiguredSecret(process.env.GH_TOKEN); const ZAI_API_KEY = readConfiguredSecret(process.env.ZAI_API_KEY); const LOCAL_PROXY_AUTH_TOKEN = readConfiguredSecret(process.env.ANTHROPIC_AUTH_TOKEN); const geminiClient = GEMINI_API_KEY ? new GoogleGenAI({ apiKey: GEMINI_API_KEY }) : null; const OPENAI_AUTH = PROVIDER === "openai" ? resolveOpenAIAuth({ env: process.env }) : null; class CompatProxyError extends Error { statusCode: number; errorType: string; constructor(message: string, statusCode = 500, errorType = "api_error") { super(message); this.name = "CompatProxyError"; this.statusCode = statusCode; this.errorType = errorType; } } assertProviderConfiguration(); const server = createServer(async (req, res) => { try { const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`); if (req.method === "GET" && (url.pathname === "/health" || url.pathname === "/")) { return sendJson(res, 200, { ok: true, provider: PROVIDER, model: ACTIVE_MODEL, }); } assertLocalProxyAuth(req); if (req.method === "GET" && url.pathname === "/v1/models") { return sendJson(res, 200, buildModelsPage()); } if (req.method === "GET" && url.pathname.startsWith("/v1/models/")) { const id = decodeURIComponent(url.pathname.slice("/v1/models/".length)); return sendJson(res, 200, buildModelInfo(id)); } if (req.method === "POST" && url.pathname === "/v1/messages/count_tokens") { const body = (await readJson(req)) as AnthropicMessageRequest; return sendJson(res, 200, { input_tokens: estimateInputTokens(body), }); } if (req.method === "POST" && url.pathname === "/v1/messages") { const body = (await readJson(req)) as AnthropicMessageRequest; const result = await handleMessages(body); if (body.stream) { return sendMessageStream(res, result); } return sendJson(res, 200, result.message); } return sendJson(res, 404, { type: "error", error: { type: "not_found_error", message: `Unsupported endpoint: ${req.method ?? "GET"} ${url.pathname}`, }, }); } catch (error) { const statusCode = error instanceof CompatProxyError ? error.statusCode : 500; const errorType = error instanceof CompatProxyError ? error.errorType : "api_error"; return sendJson(res, statusCode, { type: "error", error: { type: errorType, message: error instanceof Error ? error.message : String(error), }, }); } }); server.listen(PORT, "127.0.0.1", () => { process.stdout.write( `Claw Dev Anthropic-compatible proxy listening on http://127.0.0.1:${PORT} (${PROVIDER}:${ACTIVE_MODEL})\n`, ); }); function resolveProvider(): CompatProvider { const raw = ( process.env.ANTHROPIC_COMPAT_PROVIDER ?? process.env.CLAW_PROVIDER ?? "openai" ) .trim() .toLowerCase(); if (raw === "grok") { return "groq"; } if (raw === "github" || raw === "github-models") { return "copilot"; } if (raw === "router") { return "openrouter"; } if (raw === "z.ai") { return "zai"; } if (raw === "chatgpt") { return "openai"; } if ( raw === "openai" || raw === "gemini" || raw === "groq" || raw === "openrouter" || raw === "ollama" || raw === "copilot" || raw === "zai" ) { return raw; } throw new Error(`Unsupported ANTHROPIC_COMPAT_PROVIDER: ${raw}`); } function defaultPortFor(provider: CompatProvider): string { switch (provider) { case "openai": return "8787"; case "gemini": return "8788"; case "groq": return "8789"; case "openrouter": return "8793"; case "ollama": return "8792"; case "copilot": return "8790"; case "zai": return "8791"; } } function resolveModel(provider: CompatProvider): string { switch (provider) { case "openai": return process.env.OPENAI_MODEL?.trim() || "gpt-5-mini"; case "gemini": return process.env.GEMINI_MODEL?.trim() || "gemini-2.5-flash"; case "groq": return process.env.GROQ_MODEL?.trim() || "openai/gpt-oss-20b"; case "openrouter": return process.env.OPENROUTER_MODEL?.trim() || "anthropic/claude-sonnet-4"; case "ollama": return process.env.OLLAMA_MODEL?.trim() || "qwen3"; case "copilot": return process.env.COPILOT_MODEL?.trim() || "openai/gpt-4.1-mini"; case "zai": return process.env.ZAI_MODEL?.trim() || "glm-5"; } } function assertProviderConfiguration(): void { if (PROVIDER === "openai") { const auth = OPENAI_AUTH ?? { status: "missing" as const, authType: "none" as const, authPath: "~/.codex/auth.json", reason: "auth-not-initialized", }; if (auth.status !== "ok") { throw new CompatProxyError(formatOpenAIAuthHint(auth), 400, "authentication_error"); } } if (PROVIDER === "gemini" && !GEMINI_API_KEY) { throw new CompatProxyError( "Gemini is not configured. Set GEMINI_API_KEY, then restart Claw Dev and choose Gemini again.", 400, "authentication_error", ); } if (PROVIDER === "groq" && !GROQ_API_KEY) { throw new CompatProxyError( "Groq is not configured. Set GROQ_API_KEY, then restart Claw Dev and choose Groq again.", 400, "authentication_error", ); } if (PROVIDER === "openrouter" && !OPENROUTER_API_KEY) { throw new CompatProxyError( "OpenRouter is not configured. Set OPENROUTER_API_KEY, then restart Claw Dev and choose OpenRouter again.", 400, "authentication_error", ); } if (PROVIDER === "copilot" && !COPILOT_TOKEN) { throw new CompatProxyError( "Copilot is not configured. Set COPILOT_TOKEN or GITHUB_MODELS_TOKEN before using the GitHub Models path.", 400, "authentication_error", ); } if (PROVIDER === "zai" && !ZAI_API_KEY) { throw new CompatProxyError( "z.ai is not configured. Set ZAI_API_KEY, then restart Claw Dev and choose z.ai again.", 400, "authentication_error", ); } } function assertLocalProxyAuth(req: IncomingMessage): void { if (!LOCAL_PROXY_AUTH_TOKEN) { return; } const bearerHeader = req.headers.authorization?.trim() ?? ""; const apiKeyHeader = req.headers["x-api-key"]; const bearerToken = bearerHeader.toLowerCase().startsWith("bearer ") ? bearerHeader.slice("bearer ".length).trim() : ""; const apiKey = typeof apiKeyHeader === "string" ? apiKeyHeader.trim() : Array.isArray(apiKeyHeader) ? apiKeyHeader[0]?.trim() ?? "" : ""; if (secureTokenMatch(bearerToken, LOCAL_PROXY_AUTH_TOKEN) || secureTokenMatch(apiKey, LOCAL_PROXY_AUTH_TOKEN)) { return; } throw new CompatProxyError( "Local compatibility proxy rejected the request because the session auth token was missing or invalid. Restart Claw Dev and try again.", 401, "authentication_error", ); } function secureTokenMatch(actual: string, expected: string): boolean { if (!actual || !expected) { return false; } const actualBuffer = Buffer.from(actual); const expectedBuffer = Buffer.from(expected); if (actualBuffer.length !== expectedBuffer.length) { return false; } return timingSafeEqual(actualBuffer, expectedBuffer); } function normalizeBaseUrl(value: string): string { return value.endsWith("/") ? value.slice(0, -1) : value; } function parseOptionalInt(value: string | undefined): number | undefined { if (!value) { return undefined; } const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) ? parsed : undefined; } function readConfiguredSecret(value: string | undefined): string | undefined { const trimmed = value?.trim(); if (!trimmed) { return undefined; } const normalized = trimmed.toLowerCase(); if ( normalized === "changeme" || normalized === "replace-me" || normalized === "your_api_key_here" || (normalized.startsWith("your_") && normalized.endsWith("_here")) || normalized.includes("placeholder") || normalized.includes("example") ) { return undefined; } return trimmed; } function buildOllamaRuntimeConfig(): Record { const options: Record = {}; if (OLLAMA_NUM_CTX !== undefined) { options.num_ctx = OLLAMA_NUM_CTX; } if (OLLAMA_NUM_PREDICT !== undefined) { options.num_predict = OLLAMA_NUM_PREDICT; } return { ...(Object.keys(options).length > 0 ? { options } : {}), ...(OLLAMA_KEEP_ALIVE ? { keep_alive: OLLAMA_KEEP_ALIVE } : {}), }; } function buildModelsPage() { const ids = providerModelCatalog(PROVIDER); return { data: ids.map((id) => buildModelInfo(id)), first_id: ids[0], has_more: false, last_id: ids.at(-1), }; } function buildModelInfo(id: string) { return { type: "model", id, display_name: `${id} (${providerLabel(PROVIDER)} via Claw Dev proxy)`, created_at: "2026-03-31T00:00:00Z", }; } function resolveRequestModel(body: AnthropicMessageRequest): string { const requested = body.model?.trim(); if (!requested) { return ACTIVE_MODEL; } const catalog = providerModelCatalog(PROVIDER); if (catalog.includes(requested)) { return requested; } const slotMapped = resolveCompatRequestedModel(requested); if (slotMapped) { return slotMapped; } // Allow custom model ids for user-managed providers even if they are not in the default catalog. if ( PROVIDER === "ollama" || PROVIDER === "zai" || PROVIDER === "groq" || PROVIDER === "openrouter" || PROVIDER === "openai" || PROVIDER === "gemini" ) { return requested; } return ACTIVE_MODEL; } function resolveCompatRequestedModel(requested: string): string | null { const normalized = requested.trim().toLowerCase(); const prefix = PROVIDER.toUpperCase(); const readSlot = (slot: "HAIKU" | "SONNET" | "OPUS"): string | null => process.env[`${prefix}_MODEL_${slot}`]?.trim() || null; if (normalized.includes("opus")) { return readSlot("OPUS"); } if (normalized.includes("haiku")) { return readSlot("HAIKU"); } if (normalized.includes("sonnet") || normalized.startsWith("claude-")) { return readSlot("SONNET"); } return null; } async function handleMessages(body: AnthropicMessageRequest) { const requestId = `req_${randomUUID()}`; const providerModel = resolveRequestModel(body); const responseModel = body.model?.trim() || providerModel; let content: AnthropicContentBlock[]; switch (PROVIDER) { case "openai": content = await runOpenAI(body, providerModel); break; case "gemini": content = await runGemini(body, providerModel); break; case "groq": content = await runGroq(body, providerModel); break; case "openrouter": content = await runOpenRouter(body, providerModel); break; case "ollama": content = await runOllama(body, providerModel); break; case "copilot": content = await runCopilot(body, providerModel); break; case "zai": content = await runZai(body, providerModel); break; } const message: AnthropicMessageResponse = { id: `msg_${randomUUID()}`, type: "message", role: "assistant", model: responseModel, content, stop_reason: content.some((block) => block.type === "tool_use") ? "tool_use" : "end_turn", stop_sequence: null, usage: { input_tokens: estimateInputTokens(body), cache_creation_input_tokens: null, cache_read_input_tokens: null, output_tokens: estimateOutputTokens(content), server_tool_use: null, }, }; return { requestId, message }; } async function runOpenAI(body: AnthropicMessageRequest, model: string): Promise { if (!OPENAI_AUTH || OPENAI_AUTH.status !== "ok") { throw new Error( formatOpenAIAuthHint( OPENAI_AUTH ?? { status: "missing", authType: "none", authPath: "~/.codex/auth.json", reason: "auth-not-initialized", }, ), ); } const toolNameById = new Map(); const systemInstruction = normalizeSystemPrompt(body.system); const rawMessages = anthropicMessagesToOpenAICompatible( body.messages ?? [], toolNameById, systemInstruction, { includeToolNameOnToolMessages: false }, ); const rawTools = anthropicToolsToOpenAITools(body.tools ?? []); const { messages, tools } = compactOpenAICompatibleRequest(systemInstruction, rawMessages, rawTools, "openai"); if (OPENAI_AUTH.authType === "oauth") { const instructions = systemInstruction || "You are a helpful assistant."; const rawInput = sliceResponsesInputToLatestToolTurn(openAICompatibleMessagesToResponsesInput(messages)); const responseTools = openAICompatibleToolsToResponsesTools(tools); const compactRequest = compactOpenAIResponsesRequest(instructions, rawInput, responseTools); let response: Response; try { response = await fetch("https://chatgpt.com/backend-api/codex/responses", { method: "POST", headers: { "Content-Type": "application/json", Accept: "text/event-stream", Authorization: `Bearer ${OPENAI_AUTH.bearerToken}`, ...(OPENAI_AUTH.accountId ? { "ChatGPT-Account-Id": OPENAI_AUTH.accountId } : {}), "User-Agent": "codex-cli/0.117.0", originator: "codex_cli_rs", "x-client-request-id": randomUUID(), }, body: JSON.stringify({ model, instructions: compactRequest.instructions, input: compactRequest.input, tools: compactRequest.tools, tool_choice: buildResponsesToolChoice(body.tool_choice, compactRequest.tools), parallel_tool_calls: true, reasoning: { effort: "none" }, store: false, stream: true, include: [], text: { format: { type: "text" }, verbosity: "medium", }, }), }); } catch (error) { throw networkErrorForProvider("openai", model, "https://chatgpt.com/backend-api/codex/responses", error); } const sseText = await response.text(); if (!response.ok) { throw providerHttpError("openai", response.status, model, sseText); } return parseResponsesSseToResult(sseText).content as AnthropicContentBlock[]; } const json = await requestOpenAICompatibleJson({ provider: "openai", url: "https://api.openai.com/v1/chat/completions", token: OPENAI_AUTH.bearerToken, model, payload: { model, messages, ...(tools.length > 0 ? { tools } : {}), ...(tools.length > 0 ? { tool_choice: buildOpenAIToolChoice(body.tool_choice) } : {}), stream: false, }, }); return openAIMessageToAnthropicContent(extractOpenAIMessage(json), true); } async function runGemini(body: AnthropicMessageRequest, model: string): Promise { if (!geminiClient) { throw new CompatProxyError( "Gemini client is not configured. Set GEMINI_API_KEY and restart Claw Dev.", 400, "authentication_error", ); } const toolNameById = new Map(); const systemInstruction = normalizeSystemPrompt(body.system); const contents = anthropicMessagesToGemini(body.messages ?? [], toolNameById, systemInstruction, model); const toolDeclarations = anthropicToolsToGemini(body.tools ?? []); const useInlineSystemPrompt = shouldInlineSystemPrompt(model); let response; try { response = await geminiClient.models.generateContent({ model, contents, config: { ...(!useInlineSystemPrompt && systemInstruction ? { systemInstruction } : {}), ...(toolDeclarations.length > 0 ? { tools: [{ functionDeclarations: toolDeclarations }], toolConfig: { functionCallingConfig: buildGeminiFunctionCallingConfig( body.tool_choice, toolDeclarations.map((tool) => tool.name), ), }, } : {}), }, }); } catch (error) { const reason = error instanceof Error ? error.message : String(error); throw providerHttpError("gemini", 400, model, reason); } const candidateParts = (response.candidates?.[0]?.content?.parts ?? []) as Array>; return geminiPartsToAnthropicContent(candidateParts); } async function runGroq(body: AnthropicMessageRequest, model: string): Promise { const toolNameById = new Map(); const systemInstruction = normalizeSystemPrompt(body.system); const rawMessages = anthropicMessagesToOpenAICompatible( body.messages ?? [], toolNameById, systemInstruction, { includeToolNameOnToolMessages: false }, ); const rawTools = anthropicToolsToOpenAITools(body.tools ?? []); const { messages, tools } = compactOpenAICompatibleRequest(systemInstruction, rawMessages, rawTools, "groq"); const json = await requestOpenAICompatibleJson({ provider: "groq", url: "https://api.groq.com/openai/v1/chat/completions", token: GROQ_API_KEY, model, payload: { model, messages, ...(tools.length > 0 ? { tools } : {}), ...(tools.length > 0 ? { tool_choice: buildOpenAIToolChoice(body.tool_choice) } : {}), stream: false, }, }); return openAIMessageToAnthropicContent(extractOpenAIMessage(json), true); } async function runOpenRouter(body: AnthropicMessageRequest, model: string): Promise { const toolNameById = new Map(); const systemInstruction = normalizeSystemPrompt(body.system); const rawMessages = anthropicMessagesToOpenAICompatible( body.messages ?? [], toolNameById, systemInstruction, { includeToolNameOnToolMessages: false }, ); const rawTools = anthropicToolsToOpenAITools(body.tools ?? []); const { messages, tools } = compactOpenAICompatibleRequest(systemInstruction, rawMessages, rawTools, "openrouter"); const json = await requestOpenAICompatibleJson({ provider: "openrouter", url: `${OPENROUTER_BASE_URL}/chat/completions`, token: OPENROUTER_API_KEY, model, extraHeaders: { ...(OPENROUTER_SITE_URL ? { "HTTP-Referer": OPENROUTER_SITE_URL } : {}), ...(OPENROUTER_APP_NAME ? { "X-Title": OPENROUTER_APP_NAME } : {}), }, payload: { model, messages, ...(tools.length > 0 ? { tools } : {}), ...(tools.length > 0 ? { tool_choice: buildOpenAIToolChoice(body.tool_choice) } : {}), stream: false, }, }); return openAIMessageToAnthropicContent(extractOpenAIMessage(json), true); } async function runOllama(body: AnthropicMessageRequest, model: string): Promise { const toolNameById = new Map(); const systemInstruction = normalizeSystemPrompt(body.system); const messages = anthropicMessagesToOpenAICompatible( body.messages ?? [], toolNameById, systemInstruction, { includeToolNameOnToolMessages: true }, ); const tools = anthropicToolsToOpenAITools(body.tools ?? []); let response: Response; try { response = await fetch(`${OLLAMA_BASE_URL}/api/chat`, { method: "POST", headers: { "Content-Type": "application/json", ...(OLLAMA_API_KEY ? { Authorization: `Bearer ${OLLAMA_API_KEY}` } : {}), }, body: JSON.stringify({ model, messages, ...(tools.length > 0 ? { tools } : {}), ...buildOllamaRuntimeConfig(), stream: false, }), }); } catch (error) { throw networkErrorForProvider("ollama", model, `${OLLAMA_BASE_URL}/api/chat`, error); } if (!response.ok) { throw providerHttpError("ollama", response.status, model, await response.text()); } const json = (await response.json()) as Record; return ollamaMessageToAnthropicContent(json.message); } async function runCopilot(body: AnthropicMessageRequest, model: string): Promise { const toolNameById = new Map(); const systemInstruction = normalizeSystemPrompt(body.system); const rawMessages = anthropicMessagesToOpenAICompatible( body.messages ?? [], toolNameById, systemInstruction, { includeToolNameOnToolMessages: false }, ); const rawTools = anthropicToolsToOpenAITools(body.tools ?? []); const { messages, tools } = compactOpenAICompatibleRequest(systemInstruction, rawMessages, rawTools, "copilot"); const json = await requestOpenAICompatibleJson({ provider: "copilot", url: "https://models.github.ai/inference/chat/completions", token: COPILOT_TOKEN, model, extraHeaders: { "X-GitHub-Api-Version": "2022-11-28", }, payload: { model, messages, ...(tools.length > 0 ? { tools } : {}), ...(tools.length > 0 ? { tool_choice: buildOpenAIToolChoice(body.tool_choice) } : {}), stream: false, }, }); return openAIMessageToAnthropicContent(extractOpenAIMessage(json), true); } async function runZai(body: AnthropicMessageRequest, model: string): Promise { const toolNameById = new Map(); const systemInstruction = normalizeSystemPrompt(body.system); const rawMessages = anthropicMessagesToOpenAICompatible( body.messages ?? [], toolNameById, systemInstruction, { includeToolNameOnToolMessages: false }, ); const rawTools = anthropicToolsToOpenAITools(body.tools ?? []); const { messages, tools } = compactOpenAICompatibleRequest(systemInstruction, rawMessages, rawTools, "zai"); const json = await requestOpenAICompatibleJson({ provider: "zai", url: "https://api.z.ai/api/paas/v4/chat/completions", token: ZAI_API_KEY, model, payload: { model, messages, ...(tools.length > 0 ? { tools } : {}), ...(tools.length > 0 ? { tool_choice: buildOpenAIToolChoice(body.tool_choice) } : {}), stream: false, }, }); return openAIMessageToAnthropicContent(extractOpenAIMessage(json), true); } async function requestOpenAICompatibleJson({ provider, url, token, model, payload, extraHeaders = {}, }: { provider: "openai" | "groq" | "openrouter" | "copilot" | "zai"; url: string; token: string | undefined; model: string; payload: Record; extraHeaders?: Record; }): Promise> { if (!token) { throw new CompatProxyError( `${providerLabel(provider)} is missing a usable API token. Check your environment variables and restart Claw Dev.`, 400, "authentication_error", ); } let response: Response; try { response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, ...extraHeaders, }, body: JSON.stringify(payload), }); } catch (error) { throw networkErrorForProvider(provider, model, url, error); } if (!response.ok) { const text = await response.text(); throw providerHttpError(provider, response.status, model, text); } return (await response.json()) as Record; } function providerHttpError(provider: CompatProvider, status: number, model: string, rawText: string): CompatProxyError { const details = parseProviderErrorBody(rawText); const message = extractProviderErrorMessage(details, rawText); const normalized = message.toLowerCase(); const prefix = `${providerLabel(provider)} request failed for model "${model}".`; if (status === 401 || status === 403) { return new CompatProxyError( `${prefix} Authentication was rejected.\nCheck the API key for ${providerLabel(provider)} and confirm the account still has access to this model.\nProvider message: ${message}`, status, "authentication_error", ); } if (status === 404 || normalized.includes("not found") || normalized.includes("unknown model")) { const modelHint = provider === "openrouter" ? 'OpenRouter model ids usually look like "anthropic/claude-sonnet-4" or "google/gemini-2.5-pro".' : "Check the exact model id for this provider and try again."; return new CompatProxyError( `${prefix} The selected model was not found.\n${modelHint}\nProvider message: ${message}`, status, "invalid_request_error", ); } if ( status === 429 || normalized.includes("rate limit") || normalized.includes("quota") || normalized.includes("resource_exhausted") ) { return new CompatProxyError( `${prefix} The provider hit a rate limit or quota cap.\nTry again later, switch to a smaller model, or use a provider with a higher limit.\nProvider message: ${message}`, 429, "rate_limit_error", ); } if ( normalized.includes("context") || normalized.includes("too large") || normalized.includes("token limit") || normalized.includes("max size") || normalized.includes("maximum context") ) { return new CompatProxyError( `${prefix} The request was larger than this model allows.\nChoose a higher-context model, shorten the conversation, or reduce tool-heavy provider modes.\nProvider message: ${message}`, 400, "invalid_request_error", ); } return new CompatProxyError(`${prefix}\nProvider message: ${message}`, status >= 400 && status < 600 ? status : 500, "api_error"); } function networkErrorForProvider( provider: CompatProvider, model: string, url: string, error: unknown, ): CompatProxyError { const reason = error instanceof Error ? error.message : String(error); if (provider === "ollama") { return new CompatProxyError( `Ollama could not be reached at ${OLLAMA_BASE_URL} for model "${model}".\nStart Ollama, confirm the base URL, and make sure the model is already pulled.\nNetwork error: ${reason}`, 502, "api_error", ); } return new CompatProxyError( `${providerLabel(provider)} could not be reached for model "${model}".\nCheck your internet connection, proxy settings, or provider base URL.\nRequest target: ${url}\nNetwork error: ${reason}`, 502, "api_error", ); } function parseProviderErrorBody(rawText: string): unknown { try { const parsed = JSON.parse(rawText); if (isRecord(parsed) && typeof parsed.message === "string") { const nestedMessage = parsed.message.trim(); if (nestedMessage.startsWith("{") || nestedMessage.startsWith("[")) { try { const reparsed = JSON.parse(nestedMessage); return reparsed; } catch { return parsed; } } } return parsed; } catch { return rawText; } } function extractProviderErrorMessage(details: unknown, rawText: string): string { if (typeof details === "string") { return details.trim() || rawText.trim() || "Unknown provider error"; } if (isRecord(details)) { const nested = details.error; if (typeof details.message === "string" && details.message.trim()) { return details.message.trim(); } if (isRecord(nested) && typeof nested.message === "string" && nested.message.trim()) { return nested.message.trim(); } if (typeof details.detail === "string" && details.detail.trim()) { return details.detail.trim(); } } return rawText.trim() || "Unknown provider error"; } function anthropicMessagesToGemini( messages: AnthropicMessageRequest["messages"], toolNameById: Map, systemInstruction: string, modelName: string, ): GeminiContent[] { const contents: GeminiContent[] = []; if (shouldInlineSystemPrompt(modelName) && systemInstruction) { contents.push({ role: "user", parts: [{ text: `\n${systemInstruction}\n` }], }); } for (const message of messages ?? []) { const parts = normalizeAnthropicContent(message.content); const geminiParts: Array> = []; for (const part of parts) { const type = typeof part.type === "string" ? part.type : ""; if (type === "text" && typeof part.text === "string") { geminiParts.push({ text: part.text }); continue; } if (type === "tool_use") { const id = typeof part.id === "string" ? part.id : `toolu_${randomUUID()}`; const name = typeof part.name === "string" ? part.name : "tool"; const input = isRecord(part.input) ? part.input : {}; toolNameById.set(id, name); geminiParts.push({ functionCall: { id, name, args: input, }, }); continue; } if (type === "tool_result") { const toolUseId = typeof part.tool_use_id === "string" ? part.tool_use_id : `toolu_${randomUUID()}`; const name = toolNameById.get(toolUseId) ?? "tool"; geminiParts.push( createPartFromFunctionResponse(toolUseId, name, { content: extractToolResultText(part.content), is_error: Boolean(part.is_error), }) as Record, ); } } if (geminiParts.length > 0) { contents.push({ role: message.role === "assistant" ? "model" : "user", parts: geminiParts, }); } } return contents; } function anthropicMessagesToOpenAICompatible( messages: AnthropicMessageRequest["messages"], toolNameById: Map, systemInstruction: string, options: { includeToolNameOnToolMessages: boolean }, ) { const result: Array> = []; if (systemInstruction) { result.push({ role: "system", content: systemInstruction, }); } for (const message of messages ?? []) { const parts = normalizeAnthropicContent(message.content); const textParts: string[] = []; const toolMessages: Array> = []; const toolCalls: Array> = []; for (const part of parts) { const type = typeof part.type === "string" ? part.type : ""; if (type === "text" && typeof part.text === "string") { textParts.push(part.text); continue; } if (type === "tool_use") { const id = typeof part.id === "string" ? part.id : `toolu_${randomUUID()}`; const name = typeof part.name === "string" ? part.name : "tool"; const input = isRecord(part.input) ? part.input : {}; toolNameById.set(id, name); toolCalls.push({ id, type: "function", function: { name, arguments: JSON.stringify(input), }, }); continue; } if (type === "tool_result") { const toolUseId = typeof part.tool_use_id === "string" ? part.tool_use_id : `toolu_${randomUUID()}`; const toolName = toolNameById.get(toolUseId) ?? "tool"; toolMessages.push({ role: "tool", tool_call_id: toolUseId, ...(options.includeToolNameOnToolMessages ? { tool_name: toolName } : {}), content: extractToolResultText(part.content), }); } } if (message.role === "assistant") { if (textParts.length > 0 || toolCalls.length > 0) { result.push({ role: "assistant", content: textParts.join("\n") || "", ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}), }); } continue; } if (textParts.length > 0) { result.push({ role: "user", content: textParts.join("\n"), }); } result.push(...toolMessages); } return result; } function anthropicToolsToGemini(tools: Array>) { return tools .map((tool) => { const name = typeof tool.name === "string" ? tool.name : null; if (!name) { return null; } return { name, description: typeof tool.description === "string" ? tool.description : "", parametersJsonSchema: isRecord(tool.input_schema) ? tool.input_schema : { type: "object", properties: {} }, }; }) .filter((tool): tool is { name: string; description: string; parametersJsonSchema: Record } => tool !== null); } function anthropicToolsToOpenAITools(tools: Array>) { return tools .map((tool) => { const name = typeof tool.name === "string" ? tool.name : null; if (!name) { return null; } return { type: "function", function: { name, description: typeof tool.description === "string" ? tool.description : "", parameters: isRecord(tool.input_schema) ? tool.input_schema : { type: "object", properties: {} }, }, }; }) .filter( ( tool, ): tool is { type: "function"; function: { name: string; description: string; parameters: Record }; } => tool !== null, ); } function compactOpenAICompatibleRequest( systemInstruction: string, messages: Array>, tools: Array<{ type: "function"; function: { name: string; description: string; parameters: Record }; }>, provider: "openai" | "groq" | "openrouter" | "copilot" | "zai", ) { const budget = provider === "copilot" ? 6000 : provider === "openai" ? 12000 : provider === "openrouter" ? 18000 : 20000; let compactMessages = trimOpenAICompatibleMessages(messages, provider); let compactTools = compactOpenAICompatibleTools(tools, provider, "normal"); while (estimateOpenAICompatiblePayload(systemInstruction, compactMessages, compactTools) > budget && compactMessages.length > 4) { compactMessages = compactMessages.slice(1); } if (estimateOpenAICompatiblePayload(systemInstruction, compactMessages, compactTools) > budget && compactTools.length > 12) { compactTools = compactTools.slice(0, 12); } if (estimateOpenAICompatiblePayload(systemInstruction, compactMessages, compactTools) > budget && compactTools.length > 6) { compactTools = compactTools.slice(0, 6); } if (provider === "copilot" && estimateOpenAICompatiblePayload(systemInstruction, compactMessages, compactTools) > budget) { compactTools = compactOpenAICompatibleTools(compactTools, provider, "minimal"); } while (provider === "copilot" && estimateOpenAICompatiblePayload(systemInstruction, compactMessages, compactTools) > budget && compactMessages.length > 2) { compactMessages = compactMessages.slice(1); } while (provider === "copilot" && estimateOpenAICompatiblePayload(systemInstruction, compactMessages, compactTools) > budget && compactTools.length > 2) { compactTools = compactTools.slice(0, Math.max(2, compactTools.length - 2)); } return { messages: compactMessages, tools: compactTools, }; } function compactOpenAIResponsesRequest( instructions: string, input: Array>, tools: Array>, ) { const budget = 12000; let compactInput = [...input]; let compactTools = [...tools]; while (estimateOpenAIResponsesPayload(instructions, compactInput, compactTools) > budget && compactInput.length > 4) { compactInput = compactInput.slice(1); } if (estimateOpenAIResponsesPayload(instructions, compactInput, compactTools) > budget && compactTools.length > 12) { compactTools = compactTools.slice(0, 12); } if (estimateOpenAIResponsesPayload(instructions, compactInput, compactTools) > budget && compactTools.length > 6) { compactTools = compactTools.slice(0, 6); } return { instructions, input: compactInput, tools: compactTools, }; } function trimOpenAICompatibleMessages( messages: Array>, provider: "openai" | "groq" | "openrouter" | "copilot" | "zai", ): Array> { if (provider !== "copilot") { return [...messages]; } return messages .slice(-4) .map((message) => { const role = typeof message.role === "string" ? message.role : "user"; const content = typeof message.content === "string" ? message.content : ""; const trimmedContent = role === "system" ? content.slice(-700) : role === "tool" ? content.slice(-400) : content.slice(-900); return { ...message, ...(typeof message.content === "string" ? { content: trimmedContent } : {}), }; }); } function compactOpenAICompatibleTools( tools: Array<{ type: "function"; function: { name: string; description: string; parameters: Record }; }>, provider: "openai" | "groq" | "openrouter" | "copilot" | "zai", mode: "normal" | "minimal", ) { return tools.map((tool) => ({ ...tool, function: { ...tool.function, description: mode === "minimal" ? "" : tool.function.description.slice(0, provider === "copilot" ? 48 : 160), parameters: provider === "copilot" && mode === "minimal" ? { type: "object", properties: {}, required: [], additionalProperties: true, } : normalizeJsonSchemaForOpenAI(tool.function.parameters), }, })); } function normalizeJsonSchemaForOpenAI(schema: Record): Record { const copy = structuredClone(schema); return normalizeJsonSchemaNode(copy); } function normalizeJsonSchemaNode(value: Record): Record { if (value.type === "object") { if (!isRecord(value.properties)) { value.properties = {}; } const properties = value.properties as Record; for (const [key, child] of Object.entries(properties)) { if (isRecord(child)) { properties[key] = normalizeJsonSchemaNode(child); } } } if (value.type === "array" && isRecord(value.items)) { value.items = normalizeJsonSchemaNode(value.items); } for (const key of ["anyOf", "oneOf", "allOf"]) { const variants = value[key]; if (Array.isArray(variants)) { value[key] = variants.map((entry) => (isRecord(entry) ? normalizeJsonSchemaNode(entry) : entry)); } } return value; } function estimateOpenAICompatiblePayload( systemInstruction: string, messages: Array>, tools: Array<{ type: "function"; function: { name: string; description: string; parameters: Record }; }>, ): number { return Math.ceil( JSON.stringify({ systemInstruction, messages, tools, }).length / 4, ); } function estimateOpenAIResponsesPayload( instructions: string, input: Array>, tools: Array>, ): number { return Math.ceil( JSON.stringify({ instructions, input, tools, }).length / 4, ); } function buildGeminiFunctionCallingConfig( toolChoice: AnthropicMessageRequest["tool_choice"], toolNames: string[], ) { if (!toolChoice || toolChoice.type === "auto") { return { mode: FunctionCallingConfigMode.AUTO }; } if (toolChoice.type === "none") { return { mode: FunctionCallingConfigMode.NONE }; } if (toolChoice.type === "tool" && toolChoice.name) { return { mode: FunctionCallingConfigMode.ANY, allowedFunctionNames: [toolChoice.name], }; } return { mode: FunctionCallingConfigMode.ANY, ...(toolNames.length > 0 ? { allowedFunctionNames: toolNames } : {}), }; } function buildOpenAIToolChoice(toolChoice: AnthropicMessageRequest["tool_choice"]) { if (!toolChoice || toolChoice.type === "auto") { return "auto"; } if (toolChoice.type === "none") { return "none"; } if (toolChoice.type === "tool" && toolChoice.name) { return { type: "function", function: { name: toolChoice.name, }, }; } return "required"; } function buildResponsesToolChoice( toolChoice: AnthropicMessageRequest["tool_choice"], tools: Array>, ) { if (!toolChoice || toolChoice.type === "auto") { return "auto"; } if (toolChoice.type === "none" || tools.length === 0) { return "none"; } if (toolChoice.type === "tool" && toolChoice.name) { return "required"; } return "required"; } function normalizeSystemPrompt(system: AnthropicMessageRequest["system"]): string { if (typeof system === "string") { return system; } if (!Array.isArray(system)) { return ""; } return system .map((block) => (typeof block.text === "string" ? block.text : "")) .filter((text) => text.length > 0) .join("\n\n"); } function shouldInlineSystemPrompt(modelName: string): boolean { return modelName.trim().toLowerCase().startsWith("gemma"); } function normalizeAnthropicContent(content: string | Array>): Array> { if (typeof content === "string") { return [{ type: "text", text: content }]; } return Array.isArray(content) ? content : []; } function extractToolResultText(content: unknown): string { if (typeof content === "string") { return content; } if (Array.isArray(content)) { return content .map((item) => (isRecord(item) && typeof item.text === "string" ? item.text : "")) .filter((text) => text.length > 0) .join("\n"); } if (isRecord(content)) { return JSON.stringify(content); } return ""; } function geminiPartsToAnthropicContent(parts: Array>): AnthropicContentBlock[] { const blocks: AnthropicContentBlock[] = []; for (const part of parts) { if (typeof part.text === "string" && part.text.length > 0) { blocks.push({ type: "text", text: part.text, citations: null, }); } if (isRecord(part.functionCall)) { const id = typeof part.functionCall.id === "string" ? part.functionCall.id : `toolu_${randomUUID()}`; const name = typeof part.functionCall.name === "string" ? part.functionCall.name : "tool"; const input = isRecord(part.functionCall.args) ? part.functionCall.args : {}; blocks.push({ type: "tool_use", id, name, input, }); } } return blocks.length > 0 ? blocks : [{ type: "text", text: "", citations: null }]; } function openAIMessageToAnthropicContent(message: Record | null, parseJsonArguments: boolean): AnthropicContentBlock[] { if (!message) { return [{ type: "text", text: "", citations: null }]; } const blocks: AnthropicContentBlock[] = []; const content = typeof message.content === "string" ? message.content : ""; if (content.length > 0) { blocks.push({ type: "text", text: content, citations: null, }); } const rawToolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : []; for (const rawToolCall of rawToolCalls) { if (!isRecord(rawToolCall)) { continue; } const id = typeof rawToolCall.id === "string" ? rawToolCall.id : `toolu_${randomUUID()}`; const fn = isRecord(rawToolCall.function) ? rawToolCall.function : {}; const name = typeof fn.name === "string" ? fn.name : "tool"; let input: Record = {}; if (parseJsonArguments && typeof fn.arguments === "string" && fn.arguments.trim().length > 0) { try { const parsed = JSON.parse(fn.arguments); input = isRecord(parsed) ? parsed : {}; } catch { input = {}; } } blocks.push({ type: "tool_use", id, name, input, }); } return blocks.length > 0 ? blocks : [{ type: "text", text: "", citations: null }]; } function extractOpenAIMessage(payload: Record): Record | null { const choices = Array.isArray(payload.choices) ? payload.choices : []; const first = choices[0]; if (!isRecord(first)) { return null; } return isRecord(first.message) ? first.message : null; } function ollamaMessageToAnthropicContent(message: unknown): AnthropicContentBlock[] { if (!isRecord(message)) { return [{ type: "text", text: "", citations: null }]; } const blocks: AnthropicContentBlock[] = []; if (typeof message.content === "string" && message.content.length > 0) { blocks.push({ type: "text", text: message.content, citations: null, }); } const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : []; for (const rawCall of toolCalls) { if (!isRecord(rawCall) || !isRecord(rawCall.function)) { continue; } const name = typeof rawCall.function.name === "string" ? rawCall.function.name : "tool"; const input = isRecord(rawCall.function.arguments) ? rawCall.function.arguments : {}; blocks.push({ type: "tool_use", id: `toolu_${randomUUID()}`, name, input, }); } return blocks.length > 0 ? blocks : [{ type: "text", text: "", citations: null }]; } function estimateInputTokens(body: AnthropicMessageRequest): number { const raw = JSON.stringify(body.messages ?? []).length + normalizeSystemPrompt(body.system).length; return Math.max(1, Math.ceil(raw / 4)); } function estimateOutputTokens(content: AnthropicContentBlock[]): number { const raw = content .map((block) => (block.type === "text" ? block.text : JSON.stringify(block.input))) .join("\n").length; return Math.max(1, Math.ceil(raw / 4)); } function sendMessageStream( res: ServerResponse, payload: { requestId: string; message: AnthropicMessageResponse }, ) { res.statusCode = 200; res.setHeader("Content-Type", "text/event-stream; charset=utf-8"); res.setHeader("Cache-Control", "no-cache, no-transform"); res.setHeader("Connection", "keep-alive"); res.setHeader("request-id", payload.requestId); const startMessage: AnthropicMessageResponse = { ...payload.message, content: [], stop_reason: null as never, stop_sequence: null, usage: { ...payload.message.usage, output_tokens: 0, }, }; writeSse(res, "message_start", { type: "message_start", message: startMessage, }); payload.message.content.forEach((block, index) => { if (block.type === "text") { writeSse(res, "content_block_start", { type: "content_block_start", index, content_block: { type: "text", text: "", citations: null, }, }); writeSse(res, "content_block_delta", { type: "content_block_delta", index, delta: { type: "text_delta", text: block.text, }, }); writeSse(res, "content_block_stop", { type: "content_block_stop", index, }); return; } writeSse(res, "content_block_start", { type: "content_block_start", index, content_block: { type: "tool_use", id: block.id, name: block.name, input: {}, }, }); writeSse(res, "content_block_delta", { type: "content_block_delta", index, delta: { type: "input_json_delta", partial_json: JSON.stringify(block.input), }, }); writeSse(res, "content_block_stop", { type: "content_block_stop", index, }); }); writeSse(res, "message_delta", { type: "message_delta", delta: { stop_reason: payload.message.stop_reason, stop_sequence: null, }, usage: { input_tokens: payload.message.usage.input_tokens, cache_creation_input_tokens: null, cache_read_input_tokens: null, output_tokens: payload.message.usage.output_tokens, server_tool_use: null, }, }); writeSse(res, "message_stop", { type: "message_stop", }); res.end(); } function writeSse(res: ServerResponse, event: string, data: unknown) { res.write(`event: ${event}\n`); res.write(`data: ${JSON.stringify(data)}\n\n`); } async function readJson(req: IncomingMessage): Promise { const chunks: Buffer[] = []; for await (const chunk of req) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } const text = Buffer.concat(chunks).toString("utf8"); return text.length > 0 ? JSON.parse(text) : null; } function sendJson(res: ServerResponse, statusCode: number, body: unknown) { res.statusCode = statusCode; res.setHeader("Content-Type", "application/json; charset=utf-8"); res.end(JSON.stringify(body)); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); }