ccswitch 说切了,ccx 却没动——我给 Codex 国产模型切换造了一座桥
ccswitch 说切了,ccx 却没动——我给 Codex 国产模型切换造了一座桥
“我明明在 ccswitch 里选了 DeepSeek,怎么 MiMo 的 API 控制台还在跳数字?”——这个问题花了我一个下午才搞明白。
你可能也遇到了这个问题
如果你在用 Codex CLI 接国产模型(DeepSeek、MiMo、Qwen),大概率用的是这套组合:
- Codex CLI:OpenAI 的命令行 AI 编程助手,支持自定义 API 端点
- ccx:智能 API 网关,当 Codex 和各种模型供应商之间的路由器
- ccswitch:模型切换工具,让你在不同模型之间快速切换
三件套配好,理论上就能在 Codex 里无缝切换国产模型了。
但你有没有验证过:ccswitch 说切到 DeepSeek 了,请求真的走了 DeepSeek 吗?
我验证了。结果是——没有。
排查过程:五个检查全过,问题依旧
我做了所有人都会做的排查:
- ✅ 重启终端——没用
- ✅ 检查 ccswitch 激活状态——DeepSeek 卡片显示绿色"已激活"
- ✅ 检查环境变量——没有 ANTHROPIC_BASE_URL 等冲突变量
- ✅ 检查 ccx 服务——进程正常,端口 3000 在监听
- ✅ 在 Codex 里
/status——显示的还是 MiMo
然后我去 MiMo 的 API 控制台一看,请求量在涨。也就是说,不管 ccswitch 怎么切,实际流量一直在走 MiMo。
这说明问题不在 ccswitch,不在 Codex,而在中间层。
真相:两套配置在两个维度上运行
打开 Codex 的配置文件 ~/.codex/config.toml:
model = "mimo-v2.5-pro"
base_url = "http://localhost:3000/v1"
再看 ccx 的配置文件 D:\Tools\.config\config.json:
"responsesUpstream": [
{ "name": "deepseek-o3mlh5", "priority": 2, "baseUrl": "https://api.deepseek.com" },
{ "name": "xiaomimimo-u16yuz", "priority": 1, "baseUrl": "https://token-plan-cn.xiaomimimo.com" }
]
再看 ccx 的会话状态 conversation_state.json:
"currentChannel": 1,
"channelName": "xiaomimimo-u16yuz"
真相浮出水面:
ccswitch 切换的是 Codex config.toml 里的 model 字段——这是一个模型名称字符串。ccx 路由看的是自己的通道 priority——这是一个数字优先级。
两者完全不在一个维度上。ccswitch 改的是"信封上写的收件人名字",ccx 看的是"邮局实际投递的地址"。
更关键的是,这种设计上的割裂导致了配置漂移(Configuration Drift)。你通过 ccswitch 设置的意图(“我想用 DeepSeek”),在 ccx 这一层被完全忽略了。ccx 就像一个固执的邮递员,他只认自己手里的派送优先级列表,不管信封上写的谁。只要 MiMo 的 priority=1 还在,所有新来的"信件"(请求)都会被他送到 MiMo 那里去。
这解释了为什么你做了所有"正确"的检查(重启、看状态、查变量),问题依然存在。因为你一直在检查"信封"(model 字段),而问题出在"邮局"的内部派送规则(priority)上。关键的是:ccx 里 MiMo 的 priority=1(最高),DeepSeek 的 priority=2(备用)。无论 ccswitch 怎么改模型名,ccx 永远把请求路由到 priority 最低的那个通道。
所以你看到的现象就是:ccswitch 显示已切换,但实际流量纹丝不动。
为什么要造 CCX-Bridge 而不是直接改 ccx
发现原因之后,解决方案其实很明确:需要一个东西在中间做翻译——监听 ccswitch 的切换动作,然后同步修改 ccx 的通道优先级。
理论上可以在 ccswitch 里加这个功能,但 ccswitch 是一个 Electron 桌面应用,源码不公开,没法改。
也可以在 ccx 里加"根据模型名自动选通道"的功能,但 ccx 的路由是按优先级走的,不看模型名。
所以最干净的方案是:写一个独立的小工具,专门做这件事。
CCX-Bridge 的设计
核心逻辑
ccswitch 切换模型
↓
config.toml 的 model 字段变化
↓
CCX-Bridge 检测到变化(fsnotify + 3秒轮询)
↓
匹配模型名 → 找到对应的 ccx 通道索引
↓
调用 ccx API: PUT /api/responses/channels/{index}
↓
目标通道 priority=1,其余 priority=2
↓
ccx 把新请求路由到正确的上游
ccx 的 API 发现
ccx 没有公开的 API 文档,以下接口是我通过 Web UI 的 JavaScript 逆向和逐个试错发现的:
GET /api/responses/channels → 获取所有通道及其状态
PUT /api/responses/channels/{i} → 修改通道属性(priority 等)
GET /api/health → 网关整体健康状态
GET /api/conversations → 会话列表和路由状态
ccx 支持配置热加载,PUT 修改后立即生效,不需要重启。
模型名到通道的映射
ccx 的通道名称是随机后缀(比如 deepseek-o3mlh5、xiaomimimo-u16yuz),和 Codex 发送的模型名(deepseek-v4-pro、mimo-v2.5-pro)完全不同。
CCX-Bridge 用子串包含匹配来做映射:配置里写 "pattern": "deepseek",只要模型名里包含 deepseek 就匹配到。多个 pattern 同时匹配时,优先选最长的那个。
技术选型
用 Go 写,编译成单个 exe,9MB,无依赖。原因:
- 用户不需要装 Python/Node 等运行时
- 双击就能跑,适合 Windows 桌面场景
- Go 的
fsnotify库做文件监听很成熟
开发过程中踩的坑
坑 1:ccx 有两套配置目录
系统上有两个 ccx:D:\Tools\ccx-win\ccx-windows-amd64.exe 和 D:\Tools\ccx.exe,它们各自有独立的配置目录(D:\Tools\ccx-win\.config\ 和 D:\Tools\.config\)。我一开始在改 ccx-win 的配置,但运行中的是另一个。
教训: 看进程实际加载的配置,不要想当然。用 Get-CimInstance Win32_Process 看命令行参数。
坑 2:priority 的语义是反直觉的
ccx 的 priority 是数字越小优先级越高。priority=1 是最高,priority=2 是备用。这和很多系统的"数字越大越优先"相反。我第一次测试时搞反了,所有请求都走了备用通道。
坑 3:已建立的对话不会切换通道
这是 ccx 的会话绑定机制。一旦 Codex 的一个对话建立了,通道就固定了。切换优先级只对新对话生效。我在测试时切了通道但没开新对话,以为没成功,反复调试了半小时。
解决方式: 切换后必须开一个新的 Codex 对话。CCX-Bridge 的 Web 页面上也加了提醒。
坑 4:差点把自己断网了
开发 CCX-Bridge 的时候,我用的是 Codex(通过 ccx)来写代码。有一次测试切换功能,把 MiMo 的优先级调低了——当前对话的请求直接失败,因为 ccx 把流量切走了。我正在写的代码、正在跑的命令全部断掉。
教训: 测试网关切换工具时,不要通过被测试的网关来写代码。或者至少,不要切当前正在用的那个通道。
坑 5:config.toml 的写入中间态
Codex 保存配置时,文件会短暂处于不完整状态。如果这时候读取解析,会得到空值或报错。解决方案是只提取 model 字段,对格式错误做容错处理。
怎么用
最简方式
- 从 GitHub Releases 下载
ccx-bridge.exe - 放到任意目录,双击运行
- 浏览器自动弹出状态页面,显示当前同步状态
- 在 ccswitch 里切换模型,CCX-Bridge 自动同步到 ccx
CLI 命令
# 查看当前状态
ccx-bridge status
# 命令行快速切换
ccx-bridge deepseek
ccx-bridge mimo
# 纯后台监听(不开浏览器)
ccx-bridge watch
自定义配置
首次运行自动生成 config.json,可以修改:
{
"ccx_url": "http://localhost:3000",
"ccx_token": "123456",
"mappings": [
{ "pattern": "deepseek", "channel": 0, "name": "DeepSeek" },
{ "pattern": "mimo", "channel": 1, "name": "MiMo" },
{ "pattern": "qwen", "channel": 2, "name": "Qwen" }
],
"health_check": { "enabled": true, "interval_seconds": 30 },
"retry": { "max_retries": 3, "base_delay_ms": 1000 }
}
mappings 数组定义了模型名到 ccx 通道的映射。加新模型就加一行,pattern 是模型名里会包含的关键词,channel 是 ccx 里的通道索引号。
它还有什么不能做的
目前版本(v1.0)的限制:
- 没有系统托盘——需要 GCC 编译 CGO 依赖才能做,目前只能开个浏览器页面看状态
- 不能反向同步——在 ccx 页面手动切了通道,config.toml 不会自动更新
- 不能激活被挂起的通道——如果 ccx 把某个通道标记为
suspended,CCX-Bridge 能切过去,但请求可能还是走不通 - 只支持一个 ccx 实例——如果你跑多个网关,目前管不了
后续计划
| 优先级 | 功能 | 说明 |
|---|---|---|
| P2 | 系统托盘 + 开机自启 | 关掉浏览器也能常驻 |
| P2 | 日志持久化 | 记录切换历史,出问题能查 |
| P2 | 通道余额查询 | 各供应商还剩多少额度 |
| P3 | 双向同步 | ccx 页面切了也能同步回来 |
| P3 | 多网关支持 | 同时管多个 ccx 实例 |
| P3 | 通道健康主动探测 | 向上游发轻量请求验证能不能用 |
写在最后
这个工具解决的是一个很具体的问题:两个工具之间的感知断层。ccswitch 管模型名,ccx 管通道优先级,它们各自都没错,但就是对不上。
这种"胶水层"的需求在工具链生态里很常见。每个工具都做好了自己的事,但工具之间缺一个翻译官。CCX-Bridge 就是做这个翻译的。
如果你也在用 Codex + ccx + ccswitch 的组合,遇到了"切了但没完全切"的问题,希望这个工具能帮你省掉一个下午的排查时间。
GitHub 仓库: xiaodangjia105/CCX-Bridge-
本文由作者在使用 Codex + ccx + MiMo 开发 CCX-Bridge 的过程中撰写。是的,开发过程中差点被自己写的工具把网断了。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)