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

