做 Claude 到 OpenAI 兼容代理时,最费时间的是消息结构、工具调用、流式事件、模型映射和鉴权这些细节。
做 API 兼容代理时,最容易低估的一件事是:它看起来像一个“字段翻译器”。
表面上,它只是把 A 家请求改成 B 家请求,再把响应改回去。
接到代码工具链后,问题会具体很多:工具调用、流式输出、取消请求、鉴权、模型选择和错误处理都会影响客户端行为。
普通消息只是第一层
最基础的转换是消息结构。
比如一边可能把系统指令单独放在 system 字段里,另一边可能希望它成为第一条 system message。用户消息可能是字符串,也可能是多模态块;助手消息可能同时包含文本和工具调用。
如果只处理最简单的:
user text -> user textassistant text -> assistant text那 demo 很容易跑通。
但真实工具链里,消息往往更复杂:
- system 可能是字符串,也可能是内容块列表。
- user content 可能包含文本和图片。
- assistant content 可能包含文本和 tool use。
- tool result 可能作为下一条 user message 回来。
- stop sequence、top_p、temperature、最大输出长度都要有合理映射。
这时代理要按两边协议重新组装消息,而不是只搬 JSON 字段。
模型映射是产品策略
模型映射看起来只是配置项,其实是产品策略。
一个兼容代理常见做法是把上游请求里的模型名映射到本地配置的几类模型:
haiku -> small modelsonnet -> middle modelopus -> big modelunknown -> fallback model这能让旧客户端继续用熟悉的模型名,同时让服务端接到不同提供商。
但这里有两个坑。
第一,不能把所有未知模型都静默映射到最贵模型后就不管。这样虽然“能用”,但成本和延迟会失控。至少要在文档里写清 fallback 行为。
第二,如果某些提供商自己的模型名已经是目标协议格式,就不该再强行改名。代理要知道哪些模型应该直通,哪些模型应该映射。
模型映射会直接影响成本、能力和用户预期,不能当成普通字符串替换。
工具调用是兼容代理的分水岭
普通聊天能跑,不代表代理可用。
工具调用通常是第一个暴露问题的地方。
工具调用需要至少处理三段转换:
- 把客户端工具定义转换成目标模型的工具定义。
- 把模型返回的工具调用转换回客户端能识别的 tool use。
- 把客户端下一轮传回来的 tool result 再转换成目标模型能接收的 tool message。
这里最容易出错的是 ID 和参数。
工具调用 ID 必须能在多轮之间对应起来。参数一般是 JSON,但流式输出时参数可能是分片到达的。代理不能假设每个 chunk 都是完整 JSON,也不能把半截参数当成最后结果。
所以工具调用支持不是“加一个 tools 字段”。它要求代理记录 tool call 的 ID、参数分片和回合关系,并按客户端期待的格式还原。
流式响应不是逐字转发
流式响应也很容易被低估。
如果目标提供商返回的是 OpenAI 风格 SSE,客户端期待的是另一种事件序列,那么代理必须重组事件,而不是简单转发文本。
一个完整的流式过程通常需要:
- 先发 message start。
- 再发 content block start。
- 中间持续发 delta。
- 工具调用时发 tool use block。
- 最后发 block stop、message delta、message stop。
还要处理 ping、finish reason、usage、JSON 参数分片和异常事件。
如果这些事件顺序不对,前端或 CLI 可能表现为卡住、工具调用丢失、输出结束不了,或者明明模型已经完成但客户端还在等待。
流式兼容要保证事件顺序和结束信号完整,不只是让文本逐段显示。
取消请求要进入设计
代码工具链里的请求经常会被取消。
用户按下中断,客户端断开连接,或者上层任务切换,都可能让当前生成失去意义。代理如果继续在后台请求下游模型,就会浪费生成额度和连接资源。
更好的做法是让流式转换层能感知客户端断开,并把取消信号传给下游客户端。
这件事看起来像优化,实际上是稳定性问题。尤其在本地机器或小服务器上,僵尸请求越积越多,最后会表现为“代理慢”“服务不稳定”“下一次请求莫名超时”。
兼容代理除了成功路径,也要把取消路径解释清楚。
鉴权默认设置要保守
代理项目通常要面对两个 key:
- 代理访问下游提供商的 key。
- 客户端访问代理时提交的 key。
如果代理没有启用客户端侧校验,任何能访问代理地址的人都可能间接使用下游额度。对于只监听本机的开发环境,这可能暂时可接受;一旦部署到服务器或局域网,就要重新评估。
我更倾向于把规则写成:
本机调试可以宽松跨机器访问必须启用客户端校验公网部署必须有额外访问控制同时,健康检查可以返回“是否配置了 key”,但不能泄漏真实 key、请求 header 或私有 base URL 细节。
错误解释比堆栈更有用
兼容代理夹在客户端和下游 provider 中间,错误来源会变复杂。
可能是客户端请求格式错了,可能是代理转换错了,可能是下游限流,可能是模型不支持工具调用,也可能是网络超时。
如果代理只把底层异常原样抛出去,使用者很难判断下一步该修哪里。
更实用的错误处理应该做三件事:
- 对下游错误做分类。
- 给客户端返回协议兼容的错误结构。
- 日志里保留足够排查的信息,但避免泄漏密钥和私有配置。
兼容层要把错误翻译成客户端能处理、开发者也能排查的形式。
我会怎么测试这种代理
普通 /health 只能证明进程活着。
最后要测兼容代理,我会至少准备这些场景:
- 基础非流式聊天。
- 带 system message 的请求。
- 多模态输入。
- 工具定义和工具调用返回。
- 完整 tool result 回合。
- 流式文本输出。
- 流式工具调用参数。
- 客户端取消流式请求。
- 上下文长度估算或近似计数接口。
- 鉴权开启和关闭两种路径。
- 下游错误分类。
这些测试不一定都要每次打真实 provider。很多转换逻辑可以用固定 JSON fixture 测。真实请求只用来做集成抽样。
兼容代理别只测一句回复
API 兼容代理做得好,客户端换 provider 后仍然能稳定聊天、调用工具、处理流式输出和报错。
字段映射只是第一步,后面要补齐的是这些行为细节。
继续读