Claude Code 遇上 GPT-5.6:一次跨协议桥接的完整优化之旅

Claude Code 遇上 GPT-5.6:一次跨协议桥接的完整优化之旅

最近,公司的 GitHub Copilot 订阅取消了 Claude 系列模型。Claude Code 还在,但之前配置的 Claude 模型随之失效——客户端仍然能打开,背后的那颗大脑却已经不可用了。

既然 Claude Code 本身支持通过自定义 API 接入其他模型,我便把它切到自己的 CopilotX,改用 gpt-5.6-sol。本以为只是换一个模型继续干活,配置完成后,我对它说了最简单的一句:

hi

它回给我的不是问候,而是一记干脆的 400:

API Error: 400 model "gpt-5.6-sol" is not accessible via the /chat/completions endpoint

这句话其实已经把答案说了一半:新的模型存在,认证也成功了,只是走错了门。

GPT-5.6 Sol 能用,但它只接受 Responses API;Claude Code 发出的却是 Anthropic Messages 请求。夹在两者中间的 CopilotX,仍然把所有 /v1/messages 请求翻译到 /chat/completions。于是一个看起来像模型权限的问题,真正的根因却藏在协议边界里。

GitHub Copilot 模型目录的一次变化,迫使我把 Claude Code 切到 GPT;而这次被动换模,又撞开了 CopilotX 一直隐藏着的协议裂缝。

这篇记录 CopilotX v2.8 的诞生。它不是一次简单的 endpoint 替换,而是一次关于“代理层到底应该替谁做决定”的完整优化。

第一反应:是不是配置写错了?

因为刚刚从 Claude 换到 GPT,看到 400 时,我最先怀疑的当然是新配置。我检查了项目下的 .claude/settings.json:Claude Code 的 base URL、认证变量、默认模型都指向了 CopilotX,看起来没有异常。更关键的是,请求已经抵达上游,并且上游准确识别出了 gpt-5.6-sol——如果地址或密钥错了,通常不会得到这么具体的 endpoint 错误。

于是归因很快收敛到 CopilotX 内部:

这一跳实际协议
Claude Code → CopilotXAnthropic /v1/messages
CopilotX 内部转换Anthropic → OpenAI Chat
CopilotX → GitHub Copilot/chat/completions
GPT-5.6 Sol 真正支持的入口/responses

旧设计在 Claude、Gemini 和过去的 GPT 模型上一直工作,所以它显得非常合理:既然 Claude Code 用 Anthropic 格式进来,那就统一转成 CopilotX 已经成熟的 Chat 格式出去。

但 GPT-5.6 出现之后,这个隐含前提失效了。统一的客户端入口,不再意味着统一的上游协议。

一个看似直接、其实过头的方案

最直觉的修法是:既然 GPT-5.6 只支持 Responses,那就让 /v1/messages 从此全部转去 /responses。

这时问题反过来了:如果把所有 Anthropic 请求都强行切到 Responses,GPT-5.6 是好了,但只支持 /chat/completions 的模型怎么办?Gemini 等已经稳定工作的模型,可能会因为这次“修复”集体回归。

从一个写死的 Chat,换成一个写死的 Responses,本质上没有进步。只是把撞错的门换了一扇。

真正的答案:让模型能力决定传输协议

最终方案保留了 Claude Code 看到的公共接口:它仍然只需要调用 /v1/messages。变化发生在 CopilotX 内部——每次请求推理前,先读取 GitHub Copilot 实时模型目录里的 supported_endpoints,再选择上游传输。

if model supports Chat:
    use the existing Chat bridge
elif model supports Responses:
    use the new Responses bridge
else:
    reject before inference

完整规则是:

模型能力CopilotX 的选择
只支持 Chat Completions继续走原有 Chat 路径
只支持 Responses走新的 Anthropic ↔ Responses 桥
两者都支持第一阶段优先 Chat,减少回归面
模型缺失或能力声明异常推理前明确报错

还有一个刻意没有采用的策略:先试 Chat,失败了再重试 Responses。

这种“撞门式路由”看起来省事,实际上会制造更多不确定性。非流式请求可能重复计费,工具调用可能执行两次;流式响应一旦已经向客户端发出第一个字节,就更不可能安全换轨。既然模型目录已经给出了能力,就应该在请求发出前做一次确定的选择,而不是拿生产请求当探针。

这是这次优化里最关键的架构转折:协议不是客户端的永久属性,也不该靠模型名字猜;它是模型在当前目录中声明的能力。

路由只占一小半,真正难的是搭桥

决定“走 Responses”只需要很少的代码。让 Claude Code 在桥的另一端仍然感觉自己面对的是一个完整的 Anthropic 服务,才是工作量最大的部分。

一轮 Agent 对话远不止输入一句话、输出一段文本。桥接层需要同时处理:

  • system 指令、文本和图片;
  • 多轮完整历史;
  • tool_use 与 tool_result;
  • 非流式结果和流式 SSE 事件;
  • token usage、停止原因、拒绝与错误;
  • reasoning 摘要及其不可见的来源信息。

工具调用尤其不能只做“字段名翻译”。Anthropic 用 content block 表达工具请求和结果,Responses 则使用 function call 与 function call output item。桥接时必须保持 call ID 不变,否则下一轮的工具结果就找不到上一轮的问题,Agent 会在最关键的一步断链。

流式更像是在做一个小型状态机。Responses 发来的是“某个 output item 出现了”“文本增加了一段”“函数参数增加了一段”“响应完成了”;Claude Code 等待的却是 Anthropic 的 message_start、content_block_*、message_delta 和 message_stop。CopilotX 必须按正确顺序开关每个内容块,而且成功时只能发出一次终止序列。上游如果意外断流,也不能伪装成正常结束。

为什么还要保留 reasoning 的“身份证”

这次最容易被忽略的细节,是 reasoning provenance。

Responses API 的 reasoning item 不只有一段可见摘要,它还有成对出现的 item ID 与 encrypted content。Claude Code 下一轮会把之前的 thinking block 随完整历史送回来;如果桥接层只保留摘要、丢掉这对来源信息,上游就无法验证这段 reasoning 是否真的是自己上一轮产生的。

CopilotX 因此把 reasoning item ID + encrypted content 一起封装进 Anthropic thinking block 的 signature。下一轮请求回来时,再把二者原样还原成 Responses reasoning item。

这里没有使用 previous_response_id。Claude Code 本来就会携带完整历史,CopilotX 继续保持无状态:任何一台实例拿到完整请求,都能独立完成转换,不需要记住上一轮落在哪个进程,也不会把代理层变成另一个会话数据库。

顺手揪出的 GPT-5.4 小坑

能力路由写好后,双端点模型 GPT-5.4 暴露了另一个兼容细节。

它既支持 Chat,也支持 Responses,按照第一阶段规则会优先走 Chat。但新一代 GPT 的 Chat 请求不接受传统的 max_tokens,而是要求 max_completion_tokens。Claude Code 传进来的仍然是 Anthropic 的 max_tokens,所以 Chat 转译器还需要对 GPT-5.4 家族做一次字段归一化。

这个 bug 很小,却再次说明:选对协议只是第一层,协议内部仍然存在模型代际差异。 一个成熟代理不能只负责把 URL 拼对,还要把这些差异限制在明确、可测试的兼容层里。

从 57 项测试到真实 Claude Code

这种改动最怕“curl 能说话,Agent 一动手就坏”。所以验收没有停在单次文本响应。

本地 57 项离线测试覆盖了三种模型能力、请求与响应转换、流式事件顺序、工具参数增量、reasoning 往返、异常终止和 GPT-5.4 字段兼容。随后,同一套版本被部署到 Azure VM,依次通过 staging、生产机 localhost 和公网域名三层验证。

生产矩阵包括:

  • GPT-5.6 Responses 非流式与流式;
  • 工具调用 → 工具结果 → 最终回答的完整往返;
  • 流式工具参数拼接;
  • Gemini Chat-only 回归;
  • GPT-5.4 双端点兼容;
  • 真实 Claude Code v2.1.195 端到端调用。

最后,我没有让 Claude Code 只回答一句普通文本,而是让它分别完成基础对话和真实工具调用。终端里出现了两句验收信号:

CLAUDE-CODE-BRIDGE-OK CLAUDE-TOOL-BRIDGE-OK

到这里,CopilotX v2.8.0 才算真正上线。服务健康检查返回 200,版本正确,进程保持 active,当前进程日志没有新增错误。

CopilotX v2.8 真正升级了什么

表面上,这次增加了大约两千行实现与测试,新增一条 Anthropic ↔ Responses 桥,顺便修了 GPT-5.4 的 token 字段。

但我觉得真正值得记下来的,不是代码量,也不是“终于用上 GPT-5.6”。而是 CopilotX 的职责发生了一次变化:

过去,它认为自己的任务是把一种请求格式翻译成另一种固定格式。

现在,它开始理解:自己面对的是一个持续变化的模型目录。相同的公共接口背后,不同模型可能要求不同协议;新模型也不该再靠散落的 if model == ... 一个个补洞。代理层应该读取能力、提前决策、选择一条确定的路径,然后忠实地把语义带到桥的另一端。

所以最终方案从来不是“让 /v1/messages 全部转走 Responses API”。

而是:

让 /v1/messages 保持稳定,让上游协议服从模型能力。

收束

故事开始时,公司的 GitHub Copilot 订阅取消了 Claude 系列模型。我只是想给仍然顺手的 Claude Code 换一颗 GPT-5.6 大脑,让工作继续下去。

那句问候没有出现,反而撞出了一条藏在 CopilotX 深处的协议裂缝。顺着它往下挖,我经历了“配置是不是错了”“要不要全部切 Responses”“会不会把 Chat 模型弄坏”,最后才抵达那个更稳的答案:不要替所有模型选同一扇门,让每个模型公开声明自己能走哪一扇。

现在,Claude Code 依然只认识熟悉的 /v1/messages。Chat-only 模型继续走老路,Responses-only 的 GPT-5.6 走上新桥,双端点模型则保留稳定优先级。

至于换模后的第一句 hi——它当然也终于有了回答。

只不过为了等到它,CopilotX 先学会了看路。


CopilotX 版本线:

日期版本博文
2/9v1.0把 Copilot 变成本地 API
2/9v2.0从本地工具到远程服务
2/13v2.3让 OpenClaw 终于能动手了
4/6v2.4盲飞三个月后,它终于睁开了眼
6/5—让 Codex 用上自家代理
6/12v2.7换锁记:当我把钥匙哈希之后
8/13v2.8Claude Code 遇上 GPT-5.6 ← 本文

Comments