CC Switch 切换 Codex 中转站后历史会话打不开?一次 model_provider 绑定问题的排查与解决

用 Codex 配合 CC Switch 的人,大概率遇到过这个场景:在第三方 API 和 OpenAI Official 之间切了几次之后,某个历史会话一点开就弹错。

text 复制代码
ChatGPT 无法加载 config.toml,因此此对话串无法继续。

请修复 config.toml:
Model provider `custom` not found.

反方向切,也可能变成 cc-switch-official not found。这不是 API 挂了,是 Codex 的历史会话和 model_provider 之间有绑定关系。下面是我排查的过程、根因,以及现在在用的处理方式。

1. 先看结论

Codex 把每个 Thread 创建时的 model_provider 存进了本地 SQLite,恢复会话时要求这个 Provider 名在 config.toml 里还能解析到定义。CC Switch 只负责改 config.toml,切换时会替换顶层 model_provider,甚至删掉旧的 [model_providers.xxx] 块。两边一对不上,就报 not found。

修法只有两个方向:

  • 保留 Provider 定义,让旧 Thread 引用的名字一直可解析;
  • 把数据库里旧 Thread 的 model_provider 同步成当前值。

两个可以一起做。但要先说清边界:这只解决「Provider 找不到」这一层,解决不了「A 中转站生成的 Responses API 历史,B 中转站不认」这种正文不兼容问题。

2. 适用边界

我是在 macOS + Codex + CC Switch(开本地路由 http://127.0.0.1:15721/v1)这套环境里踩的,涉及三处数据:

  • ~/.codex/config.toml
  • ~/.codex/state_5.sqlite(文件名随版本变,以你机器为准)
  • ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl

如果你的场景只是「换了模型、没动 Provider」,或者根本不改 config.toml,前半段可能用不上。后面「别去改 rollout」的部分,对任何想手改 Codex 历史的人都适用。

3. 报错是怎么来的

切到第三方中转站时,CC Switch 会把 config.toml 写成类似这样:

toml 复制代码
model_provider = "custom"
model = "deepseek-v4-pro-0813"

[model_providers.custom]
name = "bailian"
wire_api = "responses"
base_url = "http://127.0.0.1:15721/v1"

这时新建一个 Thread,Codex 把 custom 记在这个 Thread 上。

切回 OpenAI Official 后,config.toml 变成:

toml 复制代码
model_provider = "cc-switch-official"
model = "<你选的官方模型>"

[model_providers.cc-switch-official]
name = "OpenAI"
requires_openai_auth = true
supports_websockets = false
wire_api = "responses"
base_url = "http://127.0.0.1:15721/v1"

关键在于 [model_providers.custom] 这一段还在不在。如果 CC Switch 把它删掉或替换了,旧 Thread 引用的 custom 就解析不到,Codex 直接报 Model provider 'custom' not found。Codex 官方 issue 里有独立复现:Thread 引用了一个已被删除的 custom provider,点开会报 failed to load configuration: Model provider 'custom' not found(见文末 #38974)。这条复现没经过 CC Switch,说明根子在 Codex 的跨 Provider 会话恢复机制。

4. 三层绑定,别只盯着 config.toml

排查到这,很多人以为「把 [model_providers.custom] 补回来」就够了。实际操作里不一定,因为这背后有三层数据:

  1. config.toml------声明有哪些 Provider、当前用哪个;
  2. state_5.sqlite 的 threads 表------每个 Thread 存了 model_provider 和 model;
  3. sessions/ 下的 rollout-*.jsonl------会话正文,部分新会话是 paginated 模式,带字节偏移。

用 sqlite3 直接看第二层:

bash 复制代码
sqlite3 ~/.codex/state_5.sqlite \
  "SELECT id, model_provider, model, history_mode FROM threads LIMIT 20;"

threads 表里确实有 model_provider(NOT NULL)和 model 两列,还有 history_mode(默认 legacy,新会话可能是 paginated)。我机器上 144 个 Thread 同步完后全部是 custom;history_mode 是 132 个 legacy 加 12 个 paginated。

「Provider 找不到」本质是第 1 层和第 2 层对不上:Thread 记的名字,在 config.toml 里解析不到。

5. 一个更深的坑:别去改 rollout

我一开始想过直接改 sessions/ 下的 jsonl,想把 A Provider 的历史改成 B Provider 能用的样子。结果很快发现这很危险,尤其是 history_mode = paginated 的会话。

这种会话里有一条类似:

json 复制代码
{"history_base":{"thread_id":"01a...","end_ordinal_exclusive":914,"end_byte_offset":9408863}}

end_byte_offset 依赖源 rollout 文件的字节位置。你一旦删行、重排 JSON、合并 rollout,文件字节长度就变了,之后恢复会话可能报 invalid paginated history lineage 或 cutoff byte offset is past the source rollout 之类,严重的这条会话直接打不开。

所以我的处理底线是:脚本只动 threads 表里的 Provider 元数据,不重写 sessions/ 下的 jsonl。跨 Provider 正文不兼容、又实在要续的,我就新建一个会话,比把 rollout 改坏强。

6. 安全一点的同步脚本

下面是给我自己用的同步脚本,核心是把 threads.model_provider(和 model)对齐到当前 config.toml。相比网上流传的一句话版本,加了自动备份、确认提示和锁等待(.timeout):

bash 复制代码
#!/usr/bin/env bash
set -euo pipefail

CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
CONFIG="$CODEX_HOME/config.toml"
DB="$CODEX_HOME/state_5.sqlite"

provider=$(awk -F'"' '/^model_provider[[:space:]]*=/{print $2; exit}' "$CONFIG")
model=$(awk -F'"' '/^model[[:space:]]*=/{print $2; exit}' "$CONFIG")

[[ -n "$provider" ]] || { echo "从 $CONFIG 读不到 model_provider,终止。" >&2; exit 1; }

cp "$DB" "$DB.before-sync-$(date +%Y%m%d-%H%M%S)"

sql=".timeout 5000
UPDATE threads SET model_provider = '$provider';"
[[ -n "$model" ]] && sql+="\nUPDATE threads SET model = '$model';"

printf '将线程统一为 provider=%s / model=%s。已自动备份,请确认 Codex 已退出。[y/N] ' "$provider" "${model:-<不变>}"
read -r ans
[[ "$ans" =~ ^[Yy]$ ]] || { echo "取消。"; exit 0; }

printf '%b\n' "$sql" | sqlite3 "$DB"
echo "完成,当前分布:"
sqlite3 "$DB" "SELECT model_provider, model, COUNT(*) FROM threads GROUP BY 1, 2;"

运行前两件事:退出 Codex;把 state_5.sqlite 换成你机器上的实际文件名。awk 那句假设你的 config.toml 里 provider / model 是单行、双引号写法,CC Switch 默认就是这个格式。

两个可选项,按需改:

  • 只想改 Provider、不想动 model,删掉 model 那两行即可;
  • 想保留「每个 Thread 原本用哪个 Provider」的信息,就跳过脚本,改走第 3 节的「保留 Provider 定义」路线。

7. 相关 Issue

这个问题不是 CC Switch 独有的,是三方叠加才容易触发:Codex 的 Provider / Session 机制 + CC Switch 动态改配置 + 不同中转站对 Responses API 的实现差异。

  • CC Switch #6658:从 API 切到 OpenAI Official 后,原 API 会话报 Model provider 'custom' not found
  • CC Switch #6650:同场景下 OpenAI Official 用量显示错误
  • Codex #38974:删除 custom provider 后,Desktop 仍列出相关 Thread 并打a不开
  • Codex #28549:model_provider 迁移后会话闪退 / 消失
相关推荐
OliverQiu18 小时前
开源一个 macOS 小工具:集中管理 ChatGPT 的网络启动配置
macos·chatgpt·swiftui·mac·proxy·codex·dmg
寒狼GPT20 小时前
Codex 5小时额度是固定重置还是滚动恢复?为什么每个人Reset时间不一样
chatgpt·程序员·ai编程
AI大模型-小华21 小时前
AI 编程进入深度开发阶段:什么时候普通方案不够,需要更高算力支持?
人工智能·chatgpt·ai编程·vs code·codex·codex cli
Mr.朱鹏2 天前
科技周报(第2026-10-10期):GPT6与安全
科技·安全·chatgpt
枫叶丹42 天前
语音 AI 怎样边听边答:实时对话系统的工作原理
开发语言·人工智能·chatgpt·开源·php·agent·codex
云上先途13 天前
AI搜索排名优化,需要重点布局哪些关键词和内容形式?
人工智能·chatgpt
小刘杂谈中3 天前
2026 年如何使用 ChatGPT 进行网页抓取:从入门到实战
chatgpt
DM今天肝到几点?3 天前
OpenAI 向全量用户开放 GPT-6 与 Intelligent UI:聊天框正在变成工作台
gpt·ui·chatgpt
鑫宝的学习笔记3 天前
Codex ChatGPT6 Astra Claude Grok Zhipu 的API Takens
chatgpt
落798.3 天前
ChatGPT 的回答,也能被“结构化抓取”?
人工智能·chatgpt·亮数据