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 吗?

我验证了。结果是——没有。

排查过程:五个检查全过,问题依旧

我做了所有人都会做的排查:

  1. ✅ 重启终端——没用
  2. ✅ 检查 ccswitch 激活状态——DeepSeek 卡片显示绿色"已激活"
  3. ✅ 检查环境变量——没有 ANTHROPIC_BASE_URL 等冲突变量
  4. ✅ 检查 ccx 服务——进程正常,端口 3000 在监听
  5. ✅ 在 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-o3mlh5xiaomimimo-u16yuz),和 Codex 发送的模型名(deepseek-v4-promimo-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.exeD:\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 字段,对格式错误做容错处理。

怎么用

最简方式

  1. GitHub Releases 下载 ccx-bridge.exe
  2. 放到任意目录,双击运行
  3. 浏览器自动弹出状态页面,显示当前同步状态
  4. 在 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 的过程中撰写。是的,开发过程中差点被自己写的工具把网断了。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐