Claude Code代理
·
模型服务
白山智算
智谱 AI apiKey
miniMax
阿里百炼
字节火山引擎
aistudio.google
美团 longcat
packyapi
模型腾讯文档
代理服务器
这里用的模型是白山智算
白云智算是openai风格, 代理服务器的作用是将其转为Anthropic 风格
const http = require("http");
const https = require("https");
const { URL } = require("url");
// ===================== 配置区 =====================
const CONFIG = {
PORT: 3456,
// 上游 OpenAI 兼容 API
UPSTREAM_BASE_URL: "https://api.edgefn.net/v1",
UPSTREAM_API_KEY: "sk-s", // 替换成你的 Key
// 模型映射:Claude Code 请求的模型 → 上游模型名
// Claude Code 默认会用 claude-sonnet-4-5 / claude-opus-4 等名称
MODEL_MAP: {
// Sonnet 系列 → GLM-4.7(均衡,推荐日常编码)
"claude-sonnet-4-5-20250929": "GLM-4.7",
"claude-sonnet-4-20250514": "GLM-4.7",
"claude-3-5-sonnet-20241022": "GLM-4.7",
"claude-3-5-sonnet-20240620": "GLM-4.7",
// Haiku 系列 → GLM-4.7(轻量快速任务)
"claude-haiku-4-5-20251001": "GLM-4.7",
"claude-3-5-haiku-20241022": "GLM-4.7",
"claude-3-haiku-20240307": "GLM-4.7",
// Opus 系列 → MiniMax-M2.5(复杂推理/长上下文)
"claude-opus-4-5-20251101": "MiniMax-M2.5",
"claude-opus-4-20250514": "MiniMax-M2.5",
"claude-3-opus-20240229": "MiniMax-M2.5",
// 直接指定模型名(透传)
"GLM-4.7": "GLM-4.7",
"MiniMax-M2.5": "MiniMax-M2.5",
// 默认兜底
"default": "GLM-4.7",
},
// 请求超时(毫秒)
TIMEOUT_MS: 600_000,
// 开启详细日志
VERBOSE: true,
};
// ==================================================
function log(...args) {
if (CONFIG.VERBOSE) console.log(new Date().toISOString(), ...args);
}
function mapModel(claudeModel) {
return CONFIG.MODEL_MAP[claudeModel] ?? CONFIG.MODEL_MAP["default"];
}
// ── Anthropic Messages → OpenAI Chat Completions ──────────────────────────────
function convertMessagesToOpenAI(anthropicBody) {
const messages = [];
// system prompt
if (anthropicBody.system) {
const systemText =
typeof anthropicBody.system === "string"
? anthropicBody.system
: anthropicBody.system
.filter((b) => b.type === "text")
.map((b) => b.text)
.join("\n");
messages.push({ role: "system", content: systemText });
}
// user / assistant turns
for (const msg of anthropicBody.messages ?? []) {
if (typeof msg.content === "string") {
messages.push({ role: msg.role, content: msg.content });
continue;
}
// content 是数组(含 text / tool_use / tool_result / image 等块)
const parts = [];
for (const block of msg.content) {
if (block.type === "text") {
parts.push({ type: "text", text: block.text });
} else if (block.type === "tool_result") {
// 把 tool_result 的内容展开成文本,简单处理
const resultText =
typeof block.content === "string"
? block.content
: (block.content ?? [])
.filter((b) => b.type === "text")
.map((b) => b.text)
.join("\n");
parts.push({ type: "text", text: `[Tool Result: ${block.tool_use_id}]\n${resultText}` });
} else if (block.type === "image") {
// OpenAI vision 格式
if (block.source?.type === "base64") {
parts.push({
type: "image_url",
image_url: {
url: `data:${block.source.media_type};base64,${block.source.data}`,
},
});
}
}
// tool_use blocks from assistant: 转成文本摘要,保持上下文连贯
else if (block.type === "tool_use") {
parts.push({
type: "text",
text: `[Tool Call: ${block.name}]\n${JSON.stringify(block.input, null, 2)}`,
});
}
}
messages.push({
role: msg.role,
content: parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts,
});
}
return messages;
}
function buildOpenAIPayload(anthropicBody) {
const model = mapModel(anthropicBody.model ?? "");
const messages = convertMessagesToOpenAI(anthropicBody);
const payload = {
model,
messages,
stream: true,
max_tokens: anthropicBody.max_tokens ?? 8192,
};
if (anthropicBody.temperature != null) payload.temperature = anthropicBody.temperature;
if (anthropicBody.top_p != null) payload.top_p = anthropicBody.top_p;
if (anthropicBody.stop_sequences) payload.stop = anthropicBody.stop_sequences;
return payload;
}
// ── OpenAI SSE → Anthropic SSE ────────────────────────────────────────────────
function makeAnthropicSSE(event, data) {
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
}
/**
* 将 OpenAI 的 SSE 流(delta chunks)转成 Anthropic SSE 流写到 res。
*/
async function pipeOpenAIStreamToAnthropic(openAIRes, res, msgId) {
const inputTokens = 0; // 上游不一定给 prompt tokens,用 0 占位
// Anthropic 事件序列:message_start → content_block_start → ping → deltas → content_block_stop → message_delta → message_stop
res.write(
makeAnthropicSSE("message_start", {
type: "message_start",
message: {
id: msgId,
type: "message",
role: "assistant",
content: [],
model: "claude-sonnet-4-5-20250929",
stop_reason: null,
stop_sequence: null,
usage: { input_tokens: inputTokens, output_tokens: 1 },
},
})
);
res.write(
makeAnthropicSSE("content_block_start", {
type: "content_block_start",
index: 0,
content_block: { type: "text", text: "" },
})
);
res.write(makeAnthropicSSE("ping", { type: "ping" }));
let outputTokens = 0;
let stopReason = "end_turn";
let buffer = "";
await new Promise((resolve, reject) => {
openAIRes.on("data", (chunk) => {
buffer += chunk.toString("utf8");
const lines = buffer.split("\n");
buffer = lines.pop(); // 最后一行可能不完整,留在 buffer
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const raw = line.slice(6).trim();
if (raw === "[DONE]") continue;
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
continue;
}
const choice = parsed.choices?.[0];
if (!choice) continue;
const delta = choice.delta?.content;
if (delta) {
outputTokens += 1; // 粗略估算
res.write(
makeAnthropicSSE("content_block_delta", {
type: "content_block_delta",
index: 0,
delta: { type: "text_delta", text: delta },
})
);
}
if (choice.finish_reason) {
stopReason =
choice.finish_reason === "stop"
? "end_turn"
: choice.finish_reason === "length"
? "max_tokens"
: "end_turn";
}
// 有些上游会在最后一个 chunk 带 usage
if (parsed.usage?.completion_tokens) {
outputTokens = parsed.usage.completion_tokens;
}
}
});
openAIRes.on("end", resolve);
openAIRes.on("error", reject);
});
res.write(
makeAnthropicSSE("content_block_stop", { type: "content_block_stop", index: 0 })
);
res.write(
makeAnthropicSSE("message_delta", {
type: "message_delta",
delta: { stop_reason: stopReason, stop_sequence: null },
usage: { output_tokens: outputTokens },
})
);
res.write(makeAnthropicSSE("message_stop", { type: "message_stop" }));
res.end();
}
// ── HTTP 请求工具 ─────────────────────────────────────────────────────────────
function httpsRequest(url, options, body) {
return new Promise((resolve, reject) => {
const parsed = new URL(url);
const isHttps = parsed.protocol === "https:";
const lib = isHttps ? https : http;
const req = lib.request(
{
hostname: parsed.hostname,
port: parsed.port || (isHttps ? 443 : 80),
path: parsed.pathname + (parsed.search || ""),
method: options.method || "POST",
headers: options.headers || {},
timeout: CONFIG.TIMEOUT_MS,
},
resolve
);
req.on("error", reject);
req.on("timeout", () => {
req.destroy(new Error("Request timed out"));
});
if (body) req.write(body);
req.end();
});
}
// ── 请求处理器 ────────────────────────────────────────────────────────────────
async function handleMessages(req, res) {
// 读取请求体
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const bodyText = Buffer.concat(chunks).toString("utf8");
let anthropicBody;
try {
anthropicBody = JSON.parse(bodyText);
} catch {
res.writeHead(400, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ error: "Invalid JSON" }));
}
log(`→ model=${anthropicBody.model} msgs=${anthropicBody.messages?.length}`);
const openAIPayload = buildOpenAIPayload(anthropicBody);
log(` mapped to upstream model: ${openAIPayload.model}`);
const payloadStr = JSON.stringify(openAIPayload);
const upstreamURL = `${CONFIG.UPSTREAM_BASE_URL}/chat/completions`;
let upstreamRes;
try {
upstreamRes = await httpsRequest(
upstreamURL,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(payloadStr),
Authorization: `Bearer ${CONFIG.UPSTREAM_API_KEY}`,
Accept: "text/event-stream",
},
},
payloadStr
);
} catch (err) {
log("Upstream request failed:", err.message);
res.writeHead(502, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ error: "upstream_error", message: err.message }));
}
if (upstreamRes.statusCode !== 200) {
const errChunks = [];
upstreamRes.on("data", (c) => errChunks.push(c));
await new Promise((r) => upstreamRes.on("end", r));
const errBody = Buffer.concat(errChunks).toString("utf8");
log("Upstream error:", upstreamRes.statusCode, errBody);
res.writeHead(upstreamRes.statusCode, { "Content-Type": "application/json" });
return res.end(errBody);
}
// 设置 Anthropic SSE 响应头
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"X-Accel-Buffering": "no",
});
const msgId = "msg_" + Date.now().toString(36);
try {
await pipeOpenAIStreamToAnthropic(upstreamRes, res, msgId);
log(`← done [${msgId}]`);
} catch (err) {
log("Stream error:", err.message);
if (!res.writableEnded) res.end();
}
}
function handleHealthCheck(res) {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok", proxy: "claude-code-proxy" }));
}
// ── 主服务器 ──────────────────────────────────────────────────────────────────
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://localhost`);
// 统一允许跨域
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, x-api-key, anthropic-version, anthropic-beta");
if (req.method === "OPTIONS") {
res.writeHead(204);
return res.end();
}
// GET / → 健康检查
if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/health")) {
return handleHealthCheck(res);
}
// POST /v1/messages → 核心代理
if (req.method === "POST" && url.pathname === "/v1/messages") {
return handleMessages(req, res).catch((err) => {
log("Unhandled error:", err);
if (!res.headersSent) res.writeHead(500);
if (!res.writableEnded) res.end(JSON.stringify({ error: "internal_error" }));
});
}
// 其他路径返回 404
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "not_found" }));
});
server.listen(CONFIG.PORT, () => {
console.log("┌─────────────────────────────────────────────┐");
console.log(`│ Claude Code Proxy → http://localhost:${CONFIG.PORT} │`);
console.log("├─────────────────────────────────────────────┤");
console.log(`│ 上游: ${CONFIG.UPSTREAM_BASE_URL.padEnd(37)}│`);
console.log("├─────────────────────────────────────────────┤");
console.log("│ 使用方式: │");
console.log(`│ export ANTHROPIC_BASE_URL=http://localhost:${CONFIG.PORT}│`);
console.log("│ export ANTHROPIC_AUTH_TOKEN=any-dummy-value│");
console.log("│ claude │");
console.log("└─────────────────────────────────────────────┘");
});
Claude Code 到代理服务器
配置路径 C:\Users\minglie.claude\settings.json
{
"env": {
"ANTHROPIC_BASE_URL": "http://localhost:3456",
"ANTHROPIC_AUTH_TOKEN": "any-dummy-value",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "GLM-4.7",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "GLM-4.7",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "MiniMax-M2.5",
"API_TIMEOUT_MS": "600000",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
}
}
更多推荐

所有评论(0)