Skip to content

Commit 0ce51b5

Browse files
committed
Add authentication and host configuration to Anthropic proxy
Change-Id: If6f97c548b00d040beccf17e207c389bcaef3be8 Signed-off-by: Thomas Kosiewski <[email protected]>
1 parent 4050bf6 commit 0ce51b5

File tree

2 files changed

+79
-19
lines changed

2 files changed

+79
-19
lines changed

src/anthropic-proxy.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,35 @@ import { providerizeSchema } from "./json-schema";
1515
export type CreateAnthropicProxyOptions = {
1616
providers: Record<string, ProviderV2>;
1717
port?: number;
18+
host?: string; // default: 127.0.0.1
19+
authToken?: string; // if set, require matching custom header
20+
authHeaderName?: string; // if authToken is set, required header name (default: X-AnyClaude-Token)
1821
};
1922

2023
// createAnthropicProxy creates a proxy server that accepts
2124
// Anthropic Message API requests and proxies them through
2225
// the appropriate provider - converting the results back
2326
// to the Anthropic Message API format.
24-
export const createAnthropicProxy = ({
27+
export const createAnthropicProxy = async ({
2528
port,
29+
host = "127.0.0.1",
2630
providers,
27-
}: CreateAnthropicProxyOptions): string => {
28-
const proxy = http
29-
.createServer((req, res) => {
31+
authToken,
32+
authHeaderName = "X-AnyClaude-Token",
33+
}: CreateAnthropicProxyOptions): Promise<string> => {
34+
const proxy = http.createServer((req, res) => {
35+
// Basic auth gate: require matching token via a custom header
36+
if (authToken) {
37+
const key = authHeaderName.toLowerCase();
38+
const headerVal = req.headers[key];
39+
const provided = Array.isArray(headerVal) ? headerVal[0] : headerVal;
40+
if (!provided || provided !== authToken) {
41+
res.writeHead(401, { "Content-Type": "application/json" });
42+
res.end(JSON.stringify({ error: "Unauthorized" }));
43+
return;
44+
}
45+
}
46+
3047
if (!req.url) {
3148
res.writeHead(400, {
3249
"Content-Type": "application/json",
@@ -221,8 +238,18 @@ export const createAnthropicProxy = ({
221238
})
222239
);
223240
});
224-
})
225-
.listen(port ?? 0);
241+
});
242+
243+
// Start listening using a random ephemeral port by default (0) and only
244+
// resolve once the server has successfully bound, avoiding address() races.
245+
const listenPort = typeof port === "number" ? port : 0;
246+
await new Promise<void>((resolve, reject) => {
247+
proxy.once("error", reject);
248+
proxy.listen(listenPort, host, () => {
249+
proxy.off("error", reject);
250+
resolve();
251+
});
252+
});
226253

227254
const address = proxy.address();
228255
if (!address) {
@@ -231,5 +258,5 @@ export const createAnthropicProxy = ({
231258
if (typeof address === "string") {
232259
return address;
233260
}
234-
return `http://localhost:${address.port}`;
261+
return `http://${host === "127.0.0.1" ? "localhost" : host}:${address.port}`;
235262
};

src/main.ts

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { createGoogleGenerativeAI } from "@ai-sdk/google";
66
import { createOpenAI } from "@ai-sdk/openai";
77
import { createXai } from "@ai-sdk/xai";
88
import { spawn } from "child_process";
9+
import { randomUUID } from "crypto";
910
import {
1011
createAnthropicProxy,
1112
type CreateAnthropicProxyOptions,
@@ -42,28 +43,60 @@ if (process.env.ANTHROPIC_API_KEY) {
4243
});
4344
}
4445

45-
const proxyURL = createAnthropicProxy({
46-
providers,
47-
});
46+
// Authentication token for the proxy. If the user already provided an
47+
// ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY, reuse it so Claude sends it as
48+
// Authorization: Bearer / X-Api-Key. Otherwise generate an ephemeral token and
49+
// inject it only into the spawned Claude process.
50+
const proxyAuthToken =
51+
process.env.ANTHROPIC_AUTH_TOKEN ??
52+
process.env.ANTHROPIC_API_KEY ??
53+
randomUUID();
54+
55+
const proxyAuthHeaderName = process.env.ANYCLAUDE_AUTH_HEADER ?? "X-AnyClaude-Token";
56+
57+
// Allow overriding host/port via environment variables for flexibility. If
58+
// port is not provided, we bind to an ephemeral random port for parallel safety.
59+
const proxyHost = process.env.ANYCLAUDE_HOST || "127.0.0.1";
60+
const proxyPort = process.env.ANYCLAUDE_PORT
61+
? Number(process.env.ANYCLAUDE_PORT)
62+
: undefined;
63+
64+
(async () => {
65+
const proxyURL = await createAnthropicProxy({
66+
providers,
67+
host: proxyHost,
68+
port: proxyPort,
69+
authToken: proxyAuthToken,
70+
authHeaderName: proxyAuthHeaderName,
71+
});
72+
73+
if (process.env.PROXY_ONLY === "true") {
74+
console.log("Proxy only mode: " + proxyURL);
75+
return;
76+
}
4877

49-
if (process.env.PROXY_ONLY === "true") {
50-
console.log("Proxy only mode: "+proxyURL);
51-
} else {
5278
const claudeArgs = process.argv.slice(2);
5379
const proc = spawn("claude", claudeArgs, {
5480
env: {
5581
...process.env,
5682
ANTHROPIC_BASE_URL: proxyURL,
83+
// Ensure the Claude CLI includes our custom header via ANTHROPIC_CUSTOM_HEADERS
84+
ANTHROPIC_CUSTOM_HEADERS: (() => {
85+
const existing = process.env.ANTHROPIC_CUSTOM_HEADERS;
86+
const line = `${proxyAuthHeaderName}: ${proxyAuthToken}`;
87+
return existing ? `${existing}\n${line}` : line;
88+
})(),
5789
},
5890
stdio: "inherit",
5991
});
6092
proc.on("exit", (code) => {
6193
if (claudeArgs[0] === "-h" || claudeArgs[0] === "--help") {
62-
console.log("\nCustom Models:")
63-
console.log(" --model <provider>/<model> e.g. openai/o3");
94+
console.log("\nCustom Models:");
95+
console.log(" --model <provider>/<model> e.g. openai/gpt-5-mini");
6496
}
65-
66-
process.exit(code);
97+
process.exit(code ?? 0);
6798
});
68-
}
69-
99+
})().catch((err) => {
100+
console.error(err);
101+
process.exit(1);
102+
});

0 commit comments

Comments
 (0)