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.sqlitethreads 表------每个 Thread 存了 model_providermodel
  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 同步完后全部是 customhistory_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 lineagecutoff 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 #28549model_provider 迁移后会话闪退 / 消失
相关推荐
梦想出海-Phoebe2 小时前
ChatGPT 被欧盟纳入大型搜索服务监管:AI 正在变成新的信息入口
人工智能·chatgpt
kingbal3 小时前
MAC:chatgpt登陆报错签名异常
macos·chatgpt
T型码农要学习5 小时前
Open WebUI:给本地AI装上网页界面,完美平替ChatGPT
人工智能·chatgpt
yyywxk6 小时前
ChatGPT/Codex Windows 桌面版“幽灵会话”问题:网页端已删除,桌面端仍显示且无法删除/归档
windows·chatgpt
AI砖家1 天前
Codex 客户端频繁提示「正在重连 / Reconnecting」的原因与完整解决方案
人工智能·chatgpt·ai编程·codex
YH55269842 天前
如何减少大模型的token消耗?日常使用,可以通过什么方式减少大模型 token 的消耗
gpt·chatgpt·github
richard_first2 天前
从 ChatGPT 到机器人:NVIDIA Jetson Orin Nano 2 背后的 Physical AI 浪潮
人工智能·chatgpt·机器人
江畔柳前堤2 天前
字节跳动产品全景图:从应用表象到技术深海的七层解剖
人工智能·chatgpt·架构·json·batch
DeepIntelli2 天前
企业独立站内容怎么改,才更容易被大模型引用:从语义化 HTML 到可抽取结构的工程化调整
人工智能·chatgpt