Claude Code配置

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"
  }
}
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐