claude-proxy 技术方案 项目概述 claude-proxy 是一个通用的 API 代理服务,实现了 Anthropic Messages API 与 OpenAI Chat Completions API 之间的双向协议转换。该代理允许使用 Anthropic API 格式的客户端(如 Claude Code CLI)无缝对接任何 OpenAI 兼容的上游服务。
核心特性
双向协议转换 : Anthropic Messages API ↔ OpenAI Chat Completions API
多提供商支持 : 可配置多个上游服务,每个独立的 API 密钥和模型映射
模型 ID 映射 : 客户端模型 ID(id)映射到上游远程 ID(remote_id)
流式响应支持 : 实时 SSE 事件转换
工具调用支持 : 双向函数调用转换
零依赖 : 仅使用 Go 标准库,无外部依赖
技术栈
Go 1.x - 编程语言
net/http - HTTP 服务器和客户端
encoding/json - JSON 编解码
crypto/subtle - 安全的密钥比较
项目架构 整体设计 项目采用单文件架构(main.go),所有功能模块集中在一个文件中,便于部署和维护:
1 2 3 4 5 claude-proxy/ ├── main.go # 主程序文件 ├── config.json # 配置文件 ├── README.md # 项目文档 └── CLAUDE.md # Claude Code 指导文档
请求处理流程 1 客户端请求 → 入站验证 → 协议转换 → 上游转发 → 响应转换 → 返回客户端
入站请求处理 (handleMessages)
接收 Anthropic Messages API 格式请求
可选的入站身份验证(通过 ak 配置)
解码请求体并验证模型 ID
协议转换 (convertAnthropicToOpenAI)
将 Anthropic 消息格式转换为 OpenAI 格式
处理系统消息、用户/助手消息、工具调用和图像
映射内容块到 OpenAI 消息部分
上游请求 (doUpstreamJSON 或 proxyStream)
构建上游 URL: {base_url}/v1/chat/completions
添加 Authorization: Bearer <api_key> 头
非流式:读取完整响应
流式:实时代理 SSE 事件
响应转换 (convertOpenAIToAnthropic)
将 OpenAI 响应转回 Anthropic 格式
映射结束原因:stop → end_turn, length → max_tokens, tool_calls → tool_use
核心模块 配置管理 (loadConfig) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 type fileConfig struct { AK string `json:"ak"` Port int `json:"port"` UpstreamTimeoutSeconds int `json:"upstream_timeout_seconds"` LogBodyMaxChars int `json:"log_body_max_chars"` LogStreamTextPreviewChars int `json:"log_stream_text_preview_chars"` DefaultModelID string `json:"default_model_id"` LogFile string `json:"logfile"` Providers []providerConfig `json:"providers"` } type providerConfig struct { BaseURL string `json:"base_url"` APIKey string `json:"api_key"` Models []modelConfig `json:"models"` } type modelConfig struct { ID string `json:"id"` DisplayName string `json:"display_name"` RemoteID string `json:"remote_id"` }
配置示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "ak" : "your-proxy-api-key" , "port" : 8888 , "upstream_timeout_seconds" : 300 , "log_body_max_chars" : 4096 , "log_stream_text_preview_chars" : 256 , "providers" : [ { "base_url" : "https://api.example.com" , "api_key" : "your-upstream-api-key" , "models" : [ { "id" : "glm" , "display_name" : "glm4.7" , "remote_id" : "deepseek-chat" } ] } ] }
模型映射 启动时构建 modelMap,将客户端请求的模型 ID 映射到上游服务的远程 ID:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type modelMapping struct { ProviderIndex int RemoteID string DisplayName string } modelMap := make (map [string ]modelMapping) for i, p := range fc.Providers { for _, m := range p.Models { remoteID := m.RemoteID if remoteID == "" { remoteID = m.ID } modelMap[m.ID] = modelMapping{ ProviderIndex: i, RemoteID: remoteID, DisplayName: m.DisplayName, } } }
协议转换模块 Anthropic → OpenAI 请求转换 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 func convertAnthropicToOpenAI (req *anthropicMessageRequest) (openaiChatCompletionRequest, error ) { var messages []any if sys := strings.TrimSpace(extractSystemText(req.System)); sys != "" { messages = append (messages, map [string ]any{ "role" : "system" , "content" : sys, }) } for _, m := range req.Messages { switch m.Role { case "user" : userMsgs, err := convertAnthropicUserBlocksToOpenAIMessages(blocks) messages = append (messages, userMsgs...) case "assistant" : assistantMsg, err := convertAnthropicAssistantBlocksToOpenAIMessage(blocks) messages = append (messages, assistantMsg) } } return openaiChatCompletionRequest{ Model: req.Model, Messages: messages, MaxTokens: req.MaxTokens, Temperature: req.Temperature, Stream: req.Stream, Tools: convertTools(req.Tools), ToolChoice: convertToolChoice(req.ToolChoice), }, nil }
OpenAI → Anthropic 响应转换 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 func convertOpenAIToAnthropic (resp openaiChatCompletionResponse) anthropicMessageResponse { content := make ([]any, 0 ) if len (resp.Choices) > 0 { ch := resp.Choices[0 ] if ch.Message.Content != nil && *ch.Message.Content != "" { content = append (content, map [string ]any{ "type" : "text" , "text" : *ch.Message.Content, }) } for _, tc := range ch.Message.ToolCalls { content = append (content, map [string ]any{ "type" : "tool_use" , "id" : tc.ID, "name" : tc.Function.Name, "input" : parseArguments(tc.Function.Arguments), }) } } return anthropicMessageResponse{ ID: resp.ID, Type: "message" , Role: "assistant" , Model: resp.Model, Content: content, StopReason: mapFinishReason(finishReason), Usage: buildUsage(resp.Usage), } }
流式响应处理 (proxyStream) 流式处理实时转换 OpenAI SSE 事件到 Anthropic 格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 func proxyStream (w http.ResponseWriter, r *http.Request, cfg *serverConfig, reqID string , openaiReq openaiChatCompletionRequest, upstreamURL string , apiKey string ) error { w.Header().Set("Content-Type" , "text/event-stream" ) w.Header().Set("Cache-Control" , "no-cache" ) w.Header().Set("Connection" , "keep-alive" ) encoder("message_start" , map [string ]any{ "type" : "message_start" , "message" : map [string ]any{ "id" : messageID, "role" : "assistant" , "content" : []any{}, }, }) reader := bufio.NewReader(upResp.Body) for { line, err := reader.ReadString('\n' ) if strings.HasPrefix(line, "data:" ) { data := strings.TrimPrefix(line, "data:" ) if data == "[DONE]" { break } var chunk openaiChatCompletionChunk json.Unmarshal([]byte (data), &chunk) if delta.Content != nil { encoder("content_block_delta" , map [string ]any{ "type" : "content_block_delta" , "delta" : map [string ]any{"type" : "text_delta" , "text" : *delta.Content}, }) } } } encoder("message_stop" , map [string ]any{"type" : "message_stop" }) return nil }
认证模块 入站认证 (checkInboundAuth) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func checkInboundAuth (r *http.Request, expected string ) bool { auth := strings.TrimSpace(r.Header.Get("Authorization" )) if strings.HasPrefix(strings.ToLower(auth), "bearer " ) { got := strings.TrimSpace(auth[len ("bearer " ):]) return subtle.ConstantTimeCompare([]byte (got), []byte (expected)) == 1 } if got := strings.TrimSpace(r.Header.Get("x-api-key" )); got != "" { return subtle.ConstantTimeCompare([]byte (got), []byte (expected)) == 1 } return false }
上游认证 始终使用配置的 api_key 发送 Authorization: Bearer <api_key> 头到上游服务。
日志和清理 1 2 3 4 5 6 7 8 9 10 11 12 13 func sanitizeOpenAIRequest (req openaiChatCompletionRequest) openaiChatCompletionRequest { if strings.HasPrefix(url, "data:" ) { iu["url" ] = "data:<redacted>" } return req } func logForwardedRequest (reqID string , cfg *serverConfig, anthropicReq, openaiReq, upstreamURL) { log.Printf("[%s] inbound summary=%s" , reqID, mustJSONTrunc(inSummary, cfg.logBodyMax)) log.Printf("[%s] forward body=%s" , reqID, mustJSONTrunc(out, cfg.logBodyMax)) }
API 端点 GET /v1/models 返回配置文件中定义的可用模型列表(Anthropic 格式)。
请求示例 :
1 2 curl -sS http://127.0.0.1:8888/v1/models \ -H 'Authorization: Bearer your-proxy-api-key'
响应格式 :
1 2 3 4 5 6 7 8 9 10 11 { "object" : "list" , "data" : [ { "id" : "glm" , "object" : "model" , "created" : 1234567890 , "display_name" : "glm4.7" } ] }
POST /v1/messages 发送消息到上游服务。
非流式请求示例 :
1 2 3 4 5 6 7 8 curl -sS http://127.0.0.1:8888/v1/messages \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer your-proxy-api-key' \ -d '{ "model": "glm", "max_tokens": 256, "messages": [{"role": "user", "content": "hello"}] }'
流式请求示例 :
1 2 3 4 5 6 7 8 9 curl -N http://127.0.0.1:8888/v1/messages \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer your-proxy-api-key' \ -d '{ "model": "glm", "max_tokens": 256, "stream": true, "messages": [{"role": "user", "content": "hello"}] }'
GET /status 健康检查端点。
响应 :
1 2 3 4 { "message" : "claude-proxy" , "health" : "ok" }
部署方案 环境要求
本地运行 1 2 3 4 5 go run . CONFIG_PATH=/path/to/config.json go run .
编译 标准编译:
1 go build -o claude-proxy .
跨平台编译:
1 2 3 4 5 6 7 8 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o dist/claude-proxy_linux_amd64 . GOOS=windows GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o dist/claude-proxy_windows_amd64.exe . GOOS=darwin GOARCH=arm64 go build -trimpath -ldflags "-s -w" -o dist/claude-proxy_darwin_arm64 .
环境变量
CONFIG_PATH - 配置文件路径(默认:config.json)
生产部署
编辑 config.json 配置上游服务和 API 密钥
编译对应平台的二进制文件
运行服务:./claude-proxy
使用 systemd 或 supervisor 管理进程(可选)
使用说明 与 Claude Code CLI 集成 1 2 3 4 5 6 7 8 9 export ANTHROPIC_BASE_URL=http://localhost:8888export ANTHROPIC_AUTH_TOKEN=your-proxy-api-keyexport ANTHROPIC_DEFAULT_HAIKU_MODEL=glmexport ANTHROPIC_DEFAULT_SONNET_MODEL=glmexport ANTHROPIC_DEFAULT_OPUS_MODEL=glmclaude
配置说明 字段说明 :
ak (可选): 入站认证密钥
port (可选): 服务器端口,默认 8888
default_model_id (可选): 默认模型 ID
upstream_timeout_seconds (可选): 上游请求超时时间,默认 300
log_body_max_chars (可选): 日志最大字符数,默认 4096(设为 0 禁用)
log_stream_text_preview_chars (可选): 流式响应预览字符数,默认 256(设为 0 禁用)
logfile (可选): 日志文件路径
providers (必需): 上游服务提供商数组
base_url (必需): 上游服务基础 URL
api_key (必需): 上游 API 密钥
models (必需): 模型配置数组
id (必需): 客户端使用的模型标识符
display_name (可选): 显示名称
remote_id (可选): 发送到上游的模型 ID(默认与 id 相同)
重要 : 请勿将真实的 API 密钥提交到版本控制系统。
总结 项目优势
零依赖 : 仅使用 Go 标准库,编译后无外部依赖,部署简单
轻量级 : 单文件实现,代码清晰易懂
高性能 : 原生 HTTP 实现,支持 HTTP/2 和连接复用
灵活性 : 支持多提供商、模型映射、流式响应
安全性 : 使用常量时间比较防止时序攻击,支持密钥脱敏日志
适用场景
将 Anthropic API 客户端连接到 OpenAI 兼容服务
统一多个上游服务的 API 接口
模型 ID 映射和转换
本地开发和测试环境
已知限制
流式转换仅支持文本 delta 和工具调用 delta
其他 Anthropic 内容块类型(如 thinking blocks)未完全实现
/v1/models 端点返回配置中的静态列表,不从上游服务获取
请求/响应体会被记录,需注意日志中可能包含敏感信息