Hello World

吞风吻雨葬落日 欺山赶海踏雪径

0%

claude-proxy 技术方案总结

claude-proxy 技术方案

项目概述

claude-proxy 是一个通用的 API 代理服务,实现了 Anthropic Messages APIOpenAI 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
客户端请求 → 入站验证 → 协议转换 → 上游转发 → 响应转换 → 返回客户端
  1. 入站请求处理 (handleMessages)

    • 接收 Anthropic Messages API 格式请求
    • 可选的入站身份验证(通过 ak 配置)
    • 解码请求体并验证模型 ID
  2. 协议转换 (convertAnthropicToOpenAI)

    • 将 Anthropic 消息格式转换为 OpenAI 格式
    • 处理系统消息、用户/助手消息、工具调用和图像
    • 映射内容块到 OpenAI 消息部分
  3. 上游请求 (doUpstreamJSONproxyStream)

    • 构建上游 URL: {base_url}/v1/chat/completions
    • 添加 Authorization: Bearer <api_key>
    • 非流式:读取完整响应
    • 流式:实时代理 SSE 事件
  4. 响应转换 (convertOpenAIToAnthropic)

    • 将 OpenAI 响应转回 Anthropic 格式
    • 映射结束原因:stopend_turn, lengthmax_tokens, tool_callstool_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 {

// 设置 SSE 响应头
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")

// 发送 message_start 事件
encoder("message_start", map[string]any{
"type": "message_start",
"message": map[string]any{
"id": messageID,
"role": "assistant",
"content": []any{},
},
})

// 读取 OpenAI SSE 流
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)

// 转换并发送 Anthropic 事件
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},
})
}
}
}

// 发送 message_stop 事件
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 {
// 支持 Authorization: Bearer <token>
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
}

// 支持 x-api-key: <token>
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 {
// 隐藏 base64 图像数据
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"
}

部署方案

环境要求

  • Go 1.x
  • 无外部依赖

本地运行

1
2
3
4
5
# 使用默认配置 config.json
go run .

# 指定配置文件路径
CONFIG_PATH=/path/to/config.json go run .

编译

标准编译:

1
go build -o claude-proxy .

跨平台编译:

1
2
3
4
5
6
7
8
# Linux AMD64
GOOS=linux GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o dist/claude-proxy_linux_amd64 .

# Windows AMD64
GOOS=windows GOARCH=amd64 go build -trimpath -ldflags "-s -w" -o dist/claude-proxy_windows_amd64.exe .

# macOS ARM64
GOOS=darwin GOARCH=arm64 go build -trimpath -ldflags "-s -w" -o dist/claude-proxy_darwin_arm64 .

环境变量

  • CONFIG_PATH - 配置文件路径(默认:config.json

生产部署

  1. 编辑 config.json 配置上游服务和 API 密钥
  2. 编译对应平台的二进制文件
  3. 运行服务:./claude-proxy
  4. 使用 systemd 或 supervisor 管理进程(可选)

使用说明

与 Claude Code CLI 集成

1
2
3
4
5
6
7
8
9
# 设置环境变量
export ANTHROPIC_BASE_URL=http://localhost:8888
export ANTHROPIC_AUTH_TOKEN=your-proxy-api-key
export ANTHROPIC_DEFAULT_HAIKU_MODEL=glm
export ANTHROPIC_DEFAULT_SONNET_MODEL=glm
export ANTHROPIC_DEFAULT_OPUS_MODEL=glm

# 启动 Claude Code
claude

配置说明

字段说明:

  • 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 密钥提交到版本控制系统。

总结

项目优势

  1. 零依赖: 仅使用 Go 标准库,编译后无外部依赖,部署简单
  2. 轻量级: 单文件实现,代码清晰易懂
  3. 高性能: 原生 HTTP 实现,支持 HTTP/2 和连接复用
  4. 灵活性: 支持多提供商、模型映射、流式响应
  5. 安全性: 使用常量时间比较防止时序攻击,支持密钥脱敏日志

适用场景

  • 将 Anthropic API 客户端连接到 OpenAI 兼容服务
  • 统一多个上游服务的 API 接口
  • 模型 ID 映射和转换
  • 本地开发和测试环境

已知限制

  • 流式转换仅支持文本 delta 和工具调用 delta
  • 其他 Anthropic 内容块类型(如 thinking blocks)未完全实现
  • /v1/models 端点返回配置中的静态列表,不从上游服务获取
  • 请求/响应体会被记录,需注意日志中可能包含敏感信息