opencode2api
从 0 到 1:用纯 Go 标准库写一个高可用的 AI 协议网关(opencode2api)
项目地址:github.com/jasonxu114514/opencode2api
一个 Go 编写的 OpenCode Zen / Zen Go 协议代理,对外提供标准 OpenAI 与 Anthropic API,自动添加 OpenCode 客户端请求头。
零第三方依赖、纯标准库、约 3900 行、Docker Compose 一键部署。
一、为什么写这个项目
AI 生态里协议是分裂的。OpenAI 有 Chat Completions 和 Responses 两套 API,Anthropic 有自己的 Messages API,而 OpenCode 客户端走的是自定义的 zen / zen/go 协议,鉴权方式、请求头、甚至会话语义都和别人不一样。
这意味着:
- Claude Code、Cline、Continue 等一堆 OpenAI/Anthropic 兼容工具没法直接连上 OpenCode 的模型服务;
- OpenCode 的 Zen 与 Zen Go 是两个不同的「档位」,模型、key、价格体系各异,用起来要自己分;
- 海外代理、多 key 分发、故障容错,这些本来不该让使用方操心。
所以我想做一个胶水层:对外暴露最通用的 OpenAI / Anthropic API,对内负责把协议翻译成 OpenCode 听得懂的语言,并顺手把多 key、多代理、故障转移这些脏活接过去。
于是 opencode2api 诞生了。整件事我从第一个 commit 到最后一版修复,花了大约 19 个小时。
如果你也被这个问题困扰过,欢迎到 GitHub 点个 Star、提 issue。
二、架构一览
1 | ┌─────────────┐ OpenAI Chat/Responses / Anthropic Messages |
关键设计
| 关注点 | 实现 |
|---|---|
| 协议 | chat / responses / anthropic 双向互转,内部统一走 bridge 中间表示 |
| 模型路由 | inferProtocol 按模型名前缀推断协议,models.protocols 可手动覆盖;双池模型按 prefer 选档 |
| 高可用 | 多 key 轮询 + 多代理绑定,故障自动迁移,冷却指数退避,15 分钟巡检 |
| 会话 | FNV 哈希做会话亲和,同一对话稳定绑定同一上游 key |
| 兼容 | thinking/reasoning 历史按目标协议规范化(针对 DeepSeek/Kimi/MiMo 兼容端点) |
| 流式 | SSE 逐帧转码,支持工具调用的增量参数、reasoning signature |
| 运维 | /healthz 健康检查,Docker Compose 部署,启动时自动构建缓存二进制 |
三、核心难点:三协议互转的「桥」
三种协议虽然都在讲「消息 + 工具 + 流式」,但字段语义差异很大:
- Chat:
messages[]+delta.tool_calls(分片、靠index归并),reasoning 走非标准的reasoning_content; - Responses:
input[]是一个顺序数组,function_call/function_call_output互相配对,工具参数是流式arguments; - Anthropic:
content是一组块(block),tool_use/tool_result靠block id关联,多一个thinking块和signature字段。
关键决策是不写两两转换成 6 个方向,而是建一个内部的「桥」中间表示:
1 | bridgeRequest / bridgeResponse / bridgeBlock |
1 | // convert.go 中统一的请求准备路径:同协议走 clone,跨协议走 bridge, |
这样对外暴露什么协议、对内走什么协议,解耦成两件独立的事。4d10eac refactor: rewrite protocol forwarding 这次重构就是把它从「两套分支」合并成这一条统一路径——这是我在开发中踩坑后得出的最重要的一条架构经验,详情见第五节。
四、开发历程(Git 时间线)
整个项目从首提交到修复收官,历时约 19 小时,11 个提交。节奏大概是:先把骨架搭出来跑通 → 疯狂补高可用 → 大重构 → 磨兼容性。
1 | 2026-08-14 08:33 f32c4a0 feat: add OpenCode Zen API compatibility proxy ← 首版,~2500 行 |
技术栈上,go.mod 里只有两行——module opencode2api 和 go 1.24。整个项目零第三方依赖,HTTP 服务器、SSE 解析、JSON 编解码、SHA/FNV 哈希、socks5 代理全部用标准库。好处是编译快、二进制干净、docker-entrypoint.sh 里甚至在容器内 go build 都很轻量;坏处是标准库不带代理协议实现,socks5 我用了 Go 自带支持(http.Transport.Proxy = http.ProxyURL),够用。
五、踩过的坑(最有价值的部分)
5.1 代理故障的误判让「健康检查」形同虚设
一开始我把代理可用性交给定时健康检查,结果发现两个问题:误伤和反应慢。
- 请求 401/403/429 甚至 5xx,不代表代理死了,但早期逻辑可能把 key 一并拉黑、把代理标记宕机;
- 定时检查是滞后信号,等检测到已经损失了一些请求。
最终收敛成一套组合拳(pool.go / gateway.go):
- 极窄的故障定义:
isProxyFailure只认超时和连接拒绝(ECONNREFUSED/DeadlineExceeded),其他 HTTP 状态一律不算代理故障:
1 | func isProxyFailure(err error) bool { |
- 用真实流量当反馈源:每次上游请求结束都会
syncProxyResult,成功就把代理标记恢复; - 异步复核:真实请求报错后,用 Cloudflare 的
https://cloudflare.com/cdn-cgi/trace独立复核代理连通性(带CompareAndSwap防重入),每 15 分钟巡检一次未恢复的代理; - 原子换绑:代理宕机时,把绑定在它上面的 key 按「最少负载优先」迁移到健康代理,恢复后原路归还(
RestoreProxy),迁移过程对调用方透明。
5.2 流式响应中途切 key = 拼接两份不同的回答
多 key 重试原本的逻辑是「失败就换下一个 key 重来」。对流式响应这是灾难——如果已经向客户端吐出了几百字节,中途换 key 重跑,客户端收到的就是两个模型拼出来的怪东西。
处理原则写得很明确(gateway.go):
流式响应一旦已经向客户端输出数据,就不会切换节点重新生成,避免拼接两个不同的响应。
所以在 doUpstream 里,只要成功开始返回 2xx 并进入流式拷贝/转码,就一条路走到黑;只有未输出任何字节的情况下才允许重试。这是流式网关最容易忽略的一条红线。
5.3 Thinking 历史:DeepSeek/Kimi/MiMo 兼容端点的「显式要求」
这是最隐蔽的坑,花了两个 commit 才收干净。
工具调用的多轮对话里,assistant 的消息要连同它的推理过程一起回放给模型。但不同客户端丢的东西不一样:
- 有些客户端丢掉了
reasoning_content但保留tool_calls,导致下一次 thinking 模式的请求不合法(Chat 协议); - Anthropic 协议的 thinking 块合法形态包括带
signature、redacted_thinking、甚至空 thinking,但 DeepSeek/Kimi/Moonshot/MiMo 的兼容端点对包含 tool_use 的 assistant 轮一律拒绝这些合法形态。
所以:
- Chat 侧:给每个带
tool_calls的 assistant 轮补齐reasoning_content,有历史 reasoning 则提升复用,否则兜底占位; - Anthropic 侧:保留有效 thinking 文本、剥离
signature、把redacted_thinking转回thinking、缺失则插入占位,并插入thinking块; - 激活条件要克制:只有模型名/上游 URL 命中推理厂商标识,或请求显式开启了 reasoning 时才做规范化,普通请求一条历史都不动——避免给不需要的厂商加戏导致行为漂移。
最新 commit e68a0b9 fix: normalize tool reasoning history across protocols 把规范化从客户端协议解耦,统一到「目标协议」上,让从哪个口进来不再影响发给上游的请求形态。
5.4 会话亲和:让多轮对话「说得上话」
模型(尤其 thinking 模型)的上下文状态往往和上游的某个 key 有黏性。如果同一段对话每次请求都随机落到不同的 key / 代理上,轻则体验割裂,重则直接报错。
做法(ids.go + pool.go):
- 会话 ID 优先级:
x-opencode-session→x-session-id→conversation-id→ body 里的conversation_id→ 第一条用户消息的稳定哈希; - 上游 key 的选择用
CursorFor(affinity):对会话 ID 做 FNV-1a 哈希得到稳定起点,从起点开始轮询,同时保留冷却感知的故障转移能力; - 每个请求的
x-opencode-request每次随机、重试保持不变,让上游能正确识别「这是同一请求的重试」。
1 | func (p *nodePool) CursorFor(affinity string) nodeCursor { |
5.5 重试策略:别拿所有 key 为一个确定性错误陪葬
retry.max_attempts 控制重试上限,但不是所有错误都值得换 key:
- 网络错误、401/403 认证失败、429 限流、5xx → 轮换节点重试;
- 其他 4xx 是确定性的请求错误(比如参数非法),换多少 key 都是同一个结果 → 直接返回错给客户端;
Retry-After响应头会被解析并纳入冷却时长;- 冷却时间按失败次数指数退避,以
1 << min(failures-1, 3)封顶到 8 倍基础冷却。
5.6 没有文档:好的代理永远是个「中间人」
匿名背后还有个更根本的问题:OpenCode 的 zen / zen/go 协议没有公开的完整文档。你甚至不能先按 doc 实现,再对拍验证——第一次跑通基本只能靠「对着客户端的真实行为逆向」。
这个项目里你能看出一套典型的逆向痕迹(ids.go):
User-Agent直接固化成opencode/1.18.18 (...),等于在说「你在跟 1.18.18 版的客户端对话」;x-opencode-session/x-opencode-request/x-opencode-client: cli/x-opencode-project这组头,语义是从真实客户端的请求里一步步拼出来的;- 会话 ID 的推导优先级(
x-opencode-session→x-session-id→ … → 第一条用户消息哈希)是在一次一次 4xx 试错中补全的「保底链」。
开发节奏上也存在诡异的留白:66e1ef1(11:04,Docker 部署可用)到 efbdb6e(16:16)之间有约五个小时的提交空白,之后连续五个 commit 全部是 tool/reasoning 历史、thinking 规范化、「健康检查」类修复。合理推测:项目能部署、能对外提供服务之后,真实使用把一批只在长对话里才会暴露的怪问题冲刷了出来——多轮工具调用 + thinking 回放、代理误伤、流式拼接。没有线上反馈的磨炼,这些东西几乎不可能自己提前想到。
如果你的目标也是「兼容某个未公开协议的商业服务」,建议把客户端真实请求抓包 → 内部桥模型 → 用假上游做回归测试这条流水线尽早立起来。
5.7 健康检查端点本身也不能「大嘴巴」
/healthz 暴露的是给 Docker healthcheck、监控用的,所以:
- 模型目录未刷新 / 过期 / 无模型 / 无健康代理时返回 503,否则 200,语义严格对应;
- 状态里只有数量(key 数、代理健康数、模型数),绝不泄漏 key 或代理地址;
- 代码里大量使用
redactURL脱敏代理 URL 中的用户名密码,日志也从不打印完整 key 和请求正文。
安全上还有两处细节:server_keys 鉴权用 subtle.ConstantTimeCompare 做常量时间比较防时序侧信道;请求体限制 32MB,响应体限制 64MB。
六、运维与部署
直接编译(需要 Go 1.24+)
1 | go build -o opencode2api ./ |
Docker Compose 一把梭
1 | git clone https://github.com/jasonxu114514/opencode2api.git |
容器入口脚本做了一个很聪明的缓存:对源码目录做 SHA-256 指纹,指纹没变就直接跑缓存二进制,变了才重新构建,并把构建产物存进持久化 volume。所以改 *.go 重启镜像,Go 的 Docker 构建缓存基本秒级命中,迭代体验很好。
修改宿主机端口:
1 | OPENCODE2API_PORT=18080 docker compose up -d |
支持的最小配置
1 | { |
代理支持 direct、http://、https://、socks5://、socks5h://,可配账号密码,比如 socks5://user:pass@127.0.0.1:1080。
七、GitHub 地址与下一步
项目在这里 👉 https://github.com/jasonxu114514/opencode2api
如果你正在用 Cli 类工具、Agent 框架,或者维护自己的 API 中转服务,希望多档位模型池、多代理容错和协议兼容这些能力开箱即用,欢迎:
- ⭐ Star 支持一下,
- 🐛 报 bug / 提需求,
- 🛠 亲手来加
models.protocols覆盖、新协议适配。
下一步可能的方向:Prometheus 指标、流式 token 配额统计、多租户 key 限流、更多上游协议(比如兼容 gpt-* 别名模型的鉴权透传)…… 想法很多,欢迎一起来。
