从零开始,解决国内无法直连 Anthropic 的问题,并为你的自建代理添加 Claude Code 支持。
Claude Code 是 Anthropic 推出的强大 AI 编程助手,但在国内网络环境下,直接运行 claude 命令会遇到:
Unable to connect to Anthropic services Failed to connect to api.anthropic.com: ERR_BAD_REQUEST
这是因为 Claude Code 启动时需要连接官方 API 进行验证。好在 Claude Code 支持通过环境变量配置第三方 API,我们可以让它在本地或自建代理上运行。
🚀 配套开源项目:本教程使用的自建代理正是 LLM-Proxy-Gateway
一个高性能、多 Key 池、支持优先级与自动故障转移的 LLM 智能代理网关,已内置 Claude Code 适配。
本教程将带你:
/v1/messages 端点)最终效果:Claude Code 愉快地跑在你的代理上,就像在使用官方 Claude 一样。
Claude Code 首次启动时会尝试连接 Anthropic 服务器,我们需要让它“误以为”已经完成过引导。
找到配置文件(Windows 路径):
C:\Users\<你的用户名>\.claude.json
用文本编辑器打开,添加或修改:
json{
"hasCompletedOnboarding": true
}
如果没有该文件,可以手动创建。保存后,重新运行 claude,应该会直接进入主题选择界面,而不再报网络错误。
如果你已经有第三方 OpenAI 兼容的 API(例如 DeepSeek、自建 vLLM 等),只需配置 Claude Code 指向它。
编辑配置文件 ~/.claude/settings.json(Windows 路径 %USERPROFILE%\.claude\settings.json):
json{
"env": {
"ANTHROPIC_BASE_URL": "https://api.deepseek.com",
"ANTHROPIC_AUTH_TOKEN": "sk-你的真实密钥",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "deepseek-chat"
},
"theme": "dark"
}
ANTHROPIC_BASE_URL:你的 API 地址,不要加 /v1 后缀。ANTHROPIC_AUTH_TOKEN:API 密钥(Bearer token)。ANTHROPIC_DEFAULT_SONNET_MODEL:覆盖 Claude Code 默认的 Sonnet 模型,填入你代理中实际可用的模型名。配置完成后,重启 Claude Code,应该可以直接对话。
有时我们需要完全自托管代理,比如集成多个 API 源、负载均衡、Token 统计等。本教程使用我开源的 LLM-Proxy-Gateway(项目内 proxy.py),它提供:
/v1/chat/completions 标准 OpenAI 端点该代理默认监听 http://127.0.0.1:8800,返回的模型列表中有一个名为 auto 的模型(代表智能选择)。
bashcurl http://127.0.0.1:8800/v1/models
返回示例:
json{"object":"list","data":[{"id":"auto","object":"model","created":1779947583,"owned_by":"proxy"}]}
说明代理已运行。
/v1/messages 适配Claude Code 使用的是 Anthropic 协议(端点 /v1/messages),而我们的代理仅支持 OpenAI 格式。需要增加一个适配层,做请求/响应的双向转换。
在 proxy.py 中(handle_chat_request 函数之前)添加以下代码:
pythonimport json
import time
def anthropic_to_openai(anthropic_body: dict) -> dict:
"""将 Anthropic 请求转换为 OpenAI 格式"""
openai_messages = []
system_prompt = None
# 处理 system 字段
if "system" in anthropic_body:
sys_content = anthropic_body["system"]
if isinstance(sys_content, list):
texts = [item.get("text", "") for item in sys_content if item.get("type") == "text"]
system_prompt = "\n".join(texts)
else:
system_prompt = sys_content
# 处理 messages
for msg in anthropic_body.get("messages", []):
role = msg["role"]
content = msg["content"]
if isinstance(content, list):
# 提取纯文本
text_parts = [block.get("text", "") for block in content if block.get("type") == "text"]
content = " ".join(text_parts) if text_parts else ""
openai_messages.append({"role": role, "content": content})
# 构建 OpenAI payload
openai_payload = {
"model": anthropic_body.get("model", "auto"),
"messages": openai_messages,
"max_tokens": anthropic_body.get("max_tokens", 1024),
"temperature": anthropic_body.get("temperature", 1.0),
"stream": anthropic_body.get("stream", False),
}
if system_prompt:
openai_payload["system"] = system_prompt
# 处理工具调用(可选)
if "tools" in anthropic_body:
openai_tools = []
for tool in anthropic_body["tools"]:
openai_tools.append({
"type": "function",
"function": {
"name": tool["name"],
"description": tool.get("description", ""),
"parameters": tool.get("input_schema", {})
}
})
openai_payload["tools"] = openai_tools
if anthropic_body.get("tool_choice"):
openai_payload["tool_choice"] = anthropic_body["tool_choice"]
return openai_payload
def openai_to_anthropic(openai_response: dict, original_model: str) -> dict:
"""将 OpenAI 响应转换为 Anthropic 格式"""
choice = openai_response.get("choices", [{}])[0]
message = choice.get("message", {})
content = message.get("content", "")
finish_reason = choice.get("finish_reason", "")
stop_reason_map = {
"stop": "end_turn",
"length": "max_tokens",
"tool_calls": "tool_use",
"content_filter": "content_filter"
}
stop_reason = stop_reason_map.get(finish_reason, "end_turn")
# 构建 content 块
content_blocks = []
if "tool_calls" in message and message["tool_calls"]:
for tc in message["tool_calls"]:
content_blocks.append({
"type": "tool_use",
"id": tc.get("id", f"toolu_{hash(tc['function']['name'])}"),
"name": tc["function"]["name"],
"input": json.loads(tc["function"]["arguments"])
})
if content:
content_blocks.append({"type": "text", "text": content})
if not content_blocks:
content_blocks = [{"type": "text", "text": ""}]
usage = openai_response.get("usage", {})
return {
"id": openai_response.get("id", f"msg_{int(time.time())}"),
"type": "message",
"role": "assistant",
"model": original_model,
"content": content_blocks,
"stop_reason": stop_reason,
"stop_sequence": None,
"usage": {
"input_tokens": usage.get("prompt_tokens", 0),
"output_tokens": usage.get("completion_tokens", 0)
}
}
def openai_to_anthropic_stream_chunk(openai_chunk: dict, model: str):
"""将 OpenAI 流式 chunk 转换为 Anthropic SSE 事件"""
choices = openai_chunk.get("choices")
if not choices or not isinstance(choices, list) or len(choices) == 0:
return None
choice = choices[0]
delta = choice.get("delta", {})
content = delta.get("content", "")
finish_reason = choice.get("finish_reason", None)
index = choice.get("index", 0)
if content:
return {
"type": "content_block_delta",
"index": index,
"delta": {"type": "text_delta", "text": content}
}
if finish_reason:
return {"type": "message_stop"}
return None
/v1/messages 路由在 app.route 定义区域(例如 /v1/chat/completions 之后)添加:
python@app.route('/v1/messages', methods=['POST'])
def anthropic_messages():
if not check_api_token():
return jsonify({"error": "Unauthorized"}), 401
try:
anthropic_body = request.get_json()
if not anthropic_body:
return jsonify({"error": "Invalid JSON body"}), 400
except:
return jsonify({"error": "Invalid JSON body"}), 400
openai_payload = anthropic_to_openai(anthropic_body)
is_stream = anthropic_body.get("stream", False)
headers = {"Content-Type": "application/json"}
auth_header = request.headers.get("Authorization")
if auth_header:
headers["Authorization"] = auth_header
local_url = f"http://127.0.0.1:{PROXY_PORT}/v1/chat/completions"
try:
upstream_resp = requests.post(
local_url,
headers=headers,
json=openai_payload,
timeout=REQUEST_TIMEOUT,
stream=is_stream
)
if upstream_resp.status_code != 200:
return Response(upstream_resp.text, status=upstream_resp.status_code, content_type="application/json")
if is_stream:
def generate():
for line in upstream_resp.iter_lines():
if not line:
continue
line = line.decode('utf-8')
if line.startswith('data: '):
data_str = line[6:]
if data_str == '[DONE]':
yield 'data: {"type": "message_stop"}\n\n'
break
try:
openai_chunk = json.loads(data_str)
anth_chunk = openai_to_anthropic_stream_chunk(openai_chunk, anthropic_body.get("model", "auto"))
if anth_chunk:
yield f'data: {json.dumps(anth_chunk)}\n\n'
except Exception as e:
add_log(logging.ERROR, f"Stream conversion error: {e}")
continue
response = Response(generate(), status=200)
response.headers['Content-Type'] = 'text/event-stream'
response.headers['Cache-Control'] = 'no-cache'
return response
else:
openai_response = upstream_resp.json()
anthropic_response = openai_to_anthropic(openai_response, anthropic_body.get("model", "auto"))
return jsonify(anthropic_response)
except Exception as e:
add_log(logging.ERROR, f"/v1/messages error: {e}")
return jsonify({"error": str(e)}), 500
注意:PROXY_PORT 需与你的监听端口一致(本例为 8800)。
bashpython proxy.py
现在代理同时支持:
POST /v1/chat/completions(OpenAI 格式)POST /v1/messages(Anthropic 格式)编辑 ~/.claude/settings.json:
json{
"env": {
"ANTHROPIC_BASE_URL": "http://127.0.0.1:8800",
"ANTHROPIC_AUTH_TOKEN": "123"
},
"theme": "dark"
}
如果你的代理启用了 API 鉴权(
ENABLE_API_AUTH=true),则需要将ANTHROPIC_AUTH_TOKEN设置为API_ACCESS_TOKEN的值。
启动 Claude Code:
bashclaude
进入后,运行 /model 选择 auto 模型(或你代理中实际支持的模型名),然后随便问一句话。
selected model (auto) may not exist/v1/models 返回的 id 与 ANTHROPIC_DEFAULT_SONNET_MODEL 设置一致。/model 选择正确的模型。list index out of rangemimo-v2-omni)返回的流式 chunk 可能不标准,更新 openai_to_anthropic_stream_chunk 函数增加防御性判断(上面已提供完整版本)。anthropic_to_openai 中注释掉 tools 相关代码,强制禁用工具调用。C:\Users\<用户名>\.claude\settings.jsonC:\Users\<用户名>\.claude.json本教程的代理代码已全部开源,欢迎使用、反馈和贡献:
👉 GitHub 仓库:https://github.com/yhyh0000/LLM-Proxy-Gateway
如果觉得有用,别忘了点亮 ⭐ Star 支持一下!
成功运行后,你会看到类似这样的界面:
❯ 你好 ● 你好!有什么我可以帮你的吗?
同时代理日志会显示:
INFO:proxy:流式请求成功 供应商: 小米mimo 模型: mimo-v2-omni
恭喜!你现在已经让 Claude Code 愉快地运行在自建代理上了。
本教程涵盖了:
通过这种方式,你可以灵活地将 Claude Code 对接任何 OpenAI 兼容的后端(包括本地 vLLM、Ollama、DeepSeek 等),享受 AI 编程助手的便利,同时保持对数据和费用的完全控制。
如果你有其他自建代理或特殊需求,修改适配函数即可。Happy coding!
本文作者:苏皓明
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!