Skip to content

Add authentication and host configuration to Anthropic proxy #6

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
41 changes: 34 additions & 7 deletions src/anthropic-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,35 @@ import { providerizeSchema } from "./json-schema";
export type CreateAnthropicProxyOptions = {
providers: Record<string, ProviderV2>;
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<string> => {
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",
Expand Down Expand Up @@ -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<void>((resolve, reject) => {
proxy.once("error", reject);
proxy.listen(listenPort, host, () => {
proxy.off("error", reject);
resolve();
});
});

const address = proxy.address();
if (!address) {
Expand All @@ -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}`;
};
57 changes: 45 additions & 12 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 <provider>/<model> e.g. openai/o3");
console.log("\nCustom Models:");
console.log(" --model <provider>/<model> e.g. openai/gpt-5-mini");
}

process.exit(code);
process.exit(code ?? 0);
});
}

})().catch((err) => {
console.error(err);
process.exit(1);
});