diff --git a/src/anthropic-proxy.ts b/src/anthropic-proxy.ts index 147e7b0..932cf27 100644 --- a/src/anthropic-proxy.ts +++ b/src/anthropic-proxy.ts @@ -15,18 +15,35 @@ import { providerizeSchema } from "./json-schema"; export type CreateAnthropicProxyOptions = { providers: Record; port?: number; + host?: string; // default: 127.0.0.1 + authToken?: string; // if set, require matching custom header + authHeaderName?: string; // if authToken is set, required header name (default: X-AnyClaude-Token) }; // createAnthropicProxy creates a proxy server that accepts // Anthropic Message API requests and proxies them through // the appropriate provider - converting the results back // to the Anthropic Message API format. -export const createAnthropicProxy = ({ +export const createAnthropicProxy = async ({ port, + host = "127.0.0.1", providers, -}: CreateAnthropicProxyOptions): string => { - const proxy = http - .createServer((req, res) => { + authToken, + authHeaderName = "X-AnyClaude-Token", +}: CreateAnthropicProxyOptions): Promise => { + const proxy = http.createServer((req, res) => { + // Basic auth gate: require matching token via a custom header + if (authToken) { + const key = authHeaderName.toLowerCase(); + const headerVal = req.headers[key]; + const provided = Array.isArray(headerVal) ? headerVal[0] : headerVal; + if (!provided || provided !== authToken) { + res.writeHead(401, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "Unauthorized" })); + return; + } + } + if (!req.url) { res.writeHead(400, { "Content-Type": "application/json", @@ -221,8 +238,18 @@ export const createAnthropicProxy = ({ }) ); }); - }) - .listen(port ?? 0); + }); + + // Start listening using a random ephemeral port by default (0) and only + // resolve once the server has successfully bound, avoiding address() races. + const listenPort = typeof port === "number" ? port : 0; + await new Promise((resolve, reject) => { + proxy.once("error", reject); + proxy.listen(listenPort, host, () => { + proxy.off("error", reject); + resolve(); + }); + }); const address = proxy.address(); if (!address) { @@ -231,5 +258,5 @@ export const createAnthropicProxy = ({ if (typeof address === "string") { return address; } - return `http://localhost:${address.port}`; + return `http://${host === "127.0.0.1" ? "localhost" : host}:${address.port}`; }; diff --git a/src/main.ts b/src/main.ts index 9df7873..f8dfb08 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,6 +6,7 @@ import { createGoogleGenerativeAI } from "@ai-sdk/google"; import { createOpenAI } from "@ai-sdk/openai"; import { createXai } from "@ai-sdk/xai"; import { spawn } from "child_process"; +import { randomUUID } from "crypto"; import { createAnthropicProxy, type CreateAnthropicProxyOptions, @@ -42,28 +43,60 @@ if (process.env.ANTHROPIC_API_KEY) { }); } -const proxyURL = createAnthropicProxy({ - providers, -}); +// Authentication token for the proxy. If the user already provided an +// ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY, reuse it so Claude sends it as +// Authorization: Bearer / X-Api-Key. Otherwise generate an ephemeral token and +// inject it only into the spawned Claude process. +const proxyAuthToken = + process.env.ANTHROPIC_AUTH_TOKEN ?? + process.env.ANTHROPIC_API_KEY ?? + randomUUID(); + +const proxyAuthHeaderName = process.env.ANYCLAUDE_AUTH_HEADER ?? "X-AnyClaude-Token"; + +// Allow overriding host/port via environment variables for flexibility. If +// port is not provided, we bind to an ephemeral random port for parallel safety. +const proxyHost = process.env.ANYCLAUDE_HOST || "127.0.0.1"; +const proxyPort = process.env.ANYCLAUDE_PORT + ? Number(process.env.ANYCLAUDE_PORT) + : undefined; + +(async () => { + const proxyURL = await createAnthropicProxy({ + providers, + host: proxyHost, + port: proxyPort, + authToken: proxyAuthToken, + authHeaderName: proxyAuthHeaderName, + }); + + if (process.env.PROXY_ONLY === "true") { + console.log("Proxy only mode: " + proxyURL); + return; + } -if (process.env.PROXY_ONLY === "true") { - console.log("Proxy only mode: "+proxyURL); -} else { const claudeArgs = process.argv.slice(2); const proc = spawn("claude", claudeArgs, { env: { ...process.env, ANTHROPIC_BASE_URL: proxyURL, + // Ensure the Claude CLI includes our custom header via ANTHROPIC_CUSTOM_HEADERS + ANTHROPIC_CUSTOM_HEADERS: (() => { + const existing = process.env.ANTHROPIC_CUSTOM_HEADERS; + const line = `${proxyAuthHeaderName}: ${proxyAuthToken}`; + return existing ? `${existing}\n${line}` : line; + })(), }, stdio: "inherit", }); proc.on("exit", (code) => { if (claudeArgs[0] === "-h" || claudeArgs[0] === "--help") { - console.log("\nCustom Models:") - console.log(" --model / e.g. openai/o3"); + console.log("\nCustom Models:"); + console.log(" --model / e.g. openai/gpt-5-mini"); } - - process.exit(code); + process.exit(code ?? 0); }); -} - +})().catch((err) => { + console.error(err); + process.exit(1); +});