用 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] 补回来」就够了。实际操作里不一定,因为这背后有三层数据:
config.toml------声明有哪些 Provider、当前用哪个;state_5.sqlite的threads表------每个 Thread 存了model_provider和model;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迁移后会话闪退 / 消失