构建 opencode-go-2api:一个支持双协议转发的 AI API 代理

记录从测试 opencode.ai 各模型端点,到发现 DeepSeek 的 Anthropic 兼容性问题,最终用 Go 实现一个同时支持 OpenAI 和 Anthropic 协议转换的代理服务的过程。

背景

opencode.ai 提供了多个国内大模型的统一接入端点,包括 DeepSeek V4、GLM-5、Kimi K2.6、MiMo、MiniMax、Qwen 等。平台在文档中为不同模型标注了不同的 SDK 兼容性:部分模型标注为 @ai-sdk/openai-compatible,部分标注为 @ai-sdk/anthropic

我起初想直接用 curl 测试 DeepSeek V4 Pro 的流式输出,按照文档使用了 Anthropic 的 /v1/messages 端点,却得到了如下错误:

{"error":{"message":"Error from provider (DeepSeek): Empty input messages"}}

批量测试所有模型

为了搞清楚哪些模型能走哪个端点,我对平台提供的 14 个模型做了批量测试。测试脚本的核心逻辑是:对标注为 OpenAI 兼容的模型调用 /v1/chat/completions,对标注为 Anthropic 的模型调用 /v1/messages

测试结果如下:

模型 标注端点 结果
GLM-5.1 / GLM-5 OpenAI 正常
Kimi K2.5 / K2.6 OpenAI 正常
DeepSeek V4 Pro / Flash Anthropic 失败
MiMo V2 / V2.5 全系 OpenAI 正常
MiniMax M2.7 / M2.5 Anthropic 正常
Qwen3.6 Plus / 3.5 Plus OpenAI 正常

结论很明确:DeepSeek V4 Pro 和 DeepSeek V4 Flash 实际需要使用 OpenAI 兼容端点 /v1/chat/completions 才能正常工作,但官方文档却将其标注为 Anthropic 端点。

设计协议转换器

既然平台本身存在端点标注与实际行为不一致的情况,写一个代理层来屏蔽这种差异是合理的做法。需求如下:

  • 读取 config.toml 配置(端口、API Key)
  • 同时暴露 OpenAI 格式的 /v1/chat/completions 和 Anthropic 格式的 /v1/messages
  • 暴露 /v1/models,根据请求头自动返回对应格式的模型列表
  • 对 Anthropic 请求,自动判断模型应该直转 Anthropic 端点,还是转换为 OpenAI 请求后转发
  • 将 OpenAI 的流式/非流式响应再转换回 Anthropic 格式

项目结构

opencode-go-2api/
├── config.toml
├── go.mod
├── main.go
└── internal/
    ├── config.go              # TOML 配置读取
    ├── models.go              # 14 个模型注册表
    ├── proxy.go               # 上游 HTTP 代理
    ├── convert_request.go     # Anthropic -> OpenAI 请求转换
    ├── convert_response.go    # OpenAI -> Anthropic 响应转换(含 SSE)
    ├── handler_openai.go      # OpenAI 端点 Handler
    └── handler_anthropic.go   # Anthropic 端点 Handler

核心转换逻辑

请求转换(Anthropic -> OpenAI)

当客户端以 Anthropic 格式发送请求时,代理执行以下转换:

  • messages 中的结构化 content 数组被扁平化为字符串
  • system 字段被提取为一条 role: system 的消息
  • stop_sequences 映射为 OpenAI 的 stop
  • tools 被重新包装为 type: function 格式
  • thinking 字段原样透传(DeepSeek 上游支持该字段)

响应转换(OpenAI -> Anthropic)

非流式响应的转换要点:

  • message.content 映射为 Anthropic 的 text content block
  • message.reasoning_content 映射为 thinking content block
  • message.tool_calls 被解析为 tool_use content blocks
  • finish_reason: tool_calls 被映射为 stop_reason: tool_use

流式响应的转换更为复杂。OpenAI 的 SSE 格式是每个 chunk 携带 delta.contentdelta.reasoning_content 的增量。代理需要将其重组为 Anthropic 的事件序列:

event: content_block_start    (type: thinking / text / tool_use)
event: content_block_delta     (type: thinking_delta / text_delta / input_json_delta)
event: content_block_stop
event: message_delta
event: message_stop

每个 content block 拥有独立的 index,代理需要动态分配和维护这些 index 的状态。

Tool Call 支持

请求侧:Anthropic 的 tools[].input_schema 映射为 OpenAI 的 tools[].function.parameters

响应侧:OpenAI 的 tool_calls[].function.arguments(JSON 字符串)被反序列化为 Anthropic tool_use.input 对象。流式场景下,input_json_deltapartial_json 字段承载增量参数片段。

Thinking / Reasoning 支持

DeepSeek V4 的流式输出中会先返回一段 reasoning_content(思维链),再返回正式回复 content。代理已将 reasoning_content 完整映射为 Anthropic 的 thinking content block。

此外,opencode.ai 的 DeepSeek 端点也接受 thinkingreasoning_effort 字段:

  • thinking: {"type": "enabled"} 会启用更长的推理过程
  • reasoning_effort: "high" / "low" 对推理长度有一定影响

测试表明,默认情况下 DeepSeek 已经会返回 reasoning 内容,显式设置 thinking 后 reasoning tokens 会明显增加。

启动方式

cd opencode-go-2api
# 编辑 config.toml 填入 api_key
go build -o opencode-go-2api .
./opencode-go-2api --config config.toml

代理监听配置的端口,对 OpenAI SDK 和 Anthropic SDK 均透明可用。

总结

这个项目的价值在于:用一个本地代理层屏蔽了上游平台在协议兼容性上的不一致性。用户不需要关心某个模型到底该走 OpenAI 端点还是 Anthropic 端点,代理会根据模型注册表中的 AnthropicOK 标记自动选择正确的转发路径。对于需要同时使用 DeepSeek 和 MiniMax 的 Anthropic SDK 用户来说,这个代理是必需的中间层。

Interaction

读完之后

分享海报
Interaction

评论区