Codex 从 sub_lxapi 切到 custom 后,旧会话一打开就报Model provider 'sub_lxapi' not found。这行报错后面通常还跟着一句“Codex 无法加载 config.toml”,看着像配置文件写坏了,于是不少人反复改 config.toml、删掉让它自动重建、把缓存清一轮,报错一字没变。TaoToken(https://taotoken.net/?utm_source=taotoken_aicg_blog_end)这条统一通道的接法可以把问题从源头掐掉:供应商名只写一次、写死成同一个 ID,之后换模型只在 TaoToken 侧动模型 ID,session 里记录的 provider 再也不变,新旧对话都不会因为“某次切换”翻车。
这篇不从修复脚本开头,而是从来路开头:先弄清 Codex 为什么会记住一个已经不存在的供应商名,再把这套“provider 名锁死”的接法落到config.toml上。
1. sub_lxapi not found 到底是谁在报
1.1 旧对话加载时,Codex 先去找 provider 定义
打开一个历史会话时,Codex 并不是直接读你现在的 config.toml 然后开工,而是先读这条会话自己记下的元信息:这条对话当时用的是哪个 provider。拿到名字以后,再去配置里找对应的 provider 定义,找到才继续加载模型、渲染上下文。
问题就出在这一步。旧会话里记的名字是sub_lxapi,而你在切换时把配置里的定义换成了custom,名字对不上,Codex 找不到对应的 provider 段,于是整条对话串加载失败。它顺手给出的那句“请修复 config.toml 中的问题”具有相当强的误导性——因为 config.toml 本身很可能完全正确,是会话记录和它对不上了。
1.2 provider 名其实记在四个地方
把“供应商名”当成一个值来追踪,会发现它同时落在四个位置,改一个不管用就是因为另外三个还留着旧值:
| 层级 | 位置 | 记录内容 | 切换供应商后 |
|---|---|---|---|
| 第 1 层 | 全局config.toml | provider 定义与默认 provider | 用户可见,容易改,但单独改不够 |
| 第 2 层 | sessions/*.jsonl | 每条会话的session_meta里带 provider 名 | 需要逐行修 |
| 第 3 层 | sqlite/state_5.sqlite | 状态库副本 | 保一致性用,通常不被读 |
| 第 4 层 | 根目录state_5.sqlite | threads 表里的 provider 字段 | 关键,Codex 实际读这份 |
提示:原文里有价值的判断是读取优先级——根目录 SQLite 排在 session_meta 之前,而 config.toml 和环境变量都排在后面。所以“我 config.toml 明明改了”这句话,在这个报错面前没有说服力。
这也解释了为什么删 config.toml 完全无效:它是可再生的,而旧值躺在数据库和会话文件里。同理,清缓存也没用,缓存不负责存 provider 名。
2. 把供应商名固定成同一串,就不会再有残留
2.1 报错的本质是“名字变过一次”
sub_lxapi这个报错之所以出现,是因为过去某次切换时改变了一个长期标识:旧记录写的是 A,新配置写的是 B。凡是把“切换供应商”等同于“改变 provider 名”的用法,都会重复踩一次。
换个思路就简单了:让 Codex 认的 provider 名从第一天起就是固定的一个值,比如taotoken。新会话记taotoken,数据库 threads 表记taotoken,配置文件里定义也是taotoken,三处天然一致。以后你要从 A 模型换到 B 模型,改的是模型 ID,不是 provider 名,session 里的那串字符根本没动过,自然不存在“旧对话找不到 provider”。
2.2 换模型改哪一层:改 TaoToken 侧,不改 session
在这套接法里,需要你手工维护的只有两处:
config.toml里的model = "YOUR_MODEL_ID",用来指定当前默认模型;- TaoToken 控制台里可用模型的选择,也就是实际被调用的那个模型。
provider 名始终是taotoken,[model_providers.taotoken]这一段也始终在。于是不管你是从便宜的模型换到强的模型,还是反过来,Codex 这边只是换了个字符串参数,历史会话记录的 provider 名一直有效,打开即加载。
一句话:把“切供应商”这个动作前置,之后就没有“切换”这回事了。
3. 在 config.toml 里把 Codex 指到 TaoToken 的通道
3.1 先准备两样东西:Key 和模型 ID
第一样是 API Key。打开 TaoToken 注册登录,进控制台创建一把 Key,文中一律用YOUR_API_KEY占位,真实 Key 只放本地,别粘进任何要提交的文件。
第二样是模型 ID。这个必须去模型广场看当时列出来什么就写什么,别照抄别人文章里的日期后缀或者自己拼一个出来——不存在的 ID 会在第一次请求时报错,而且报错信息往往不含“模型不存在”这几个字,容易误判成网络问题。
3.2 写~/.codex/config.toml
文件位置:
- Windows:
C:\Users\用户名\.codex\config.toml - macOS / Linux:
~/.codex/config.toml
内容按下面这样写,重点是model_provider和[model_providers.taotoken]的段落名必须完全一致:
# ~/.codex/config.toml model_provider = "taotoken" model = "YOUR_MODEL_ID" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY"两个容易写错的地方:
base_url一律填https://taotoken.net/api,末尾不要加/v1。多这一层路径,请求会打到不存在的地址上。- 这里的地址是给工具用的接口地址,和你在浏览器里打开的落地页不是一回事,别把带查询参数的那串粘进来。注册、建 Key、看用量走落地页;填进工具的一律是
https://taotoken.net/api。
3.3 环境变量和 auth.json 怎么给 Key
env_key = "TAOTOKEN_API_KEY"的意思是“Key 从这个环境变量里读”,所以你还得把变量导出来:
# macOS / Linux export TAOTOKEN_API_KEY=YOUR_API_KEY # Windows PowerShell(当前窗口有效) $env:TAOTOKEN_API_KEY="YOUR_API_KEY" # Windows CMD(写入用户变量,重开终端后生效) setx TAOTOKEN_API_KEY "YOUR_API_KEY"Codex 各版本对凭据的处理略有差别,有的版本会走~/.codex/auth.json。如果本地已经生成了这个文件,动它之前先把整个~/.codex目录复制一份备份,然后打开你机器上那份已有的 auth.json,照着它现有的字段结构填同一把 Key,别从别处抄一套字段名进来。改动凭据类文件之前先关掉 Codex,改完再启动。
注意:
export只在当前终端窗口有效。关掉窗口再开,变量就没了,表现是“昨天还好好的,今天全部 401”。长期使用请写进 shell 的启动文件,或者用setx。
4. 配完之后怎么验证新旧对话都能开
4.1 新会话先跑一条最小请求
重启 Codex,新开一个会话,让它做一件只涉及文本的事:解释一段 SQL 的写法,或者生成一段示例 SQL 供你参考,但不要执行。
这里有一条底线值得写清楚:Codex 这类工具默认不应该去直连你的库、你的生产机器执行操作。让模型生成 SQL、解释 SQL、对照改 SQL 都可以,真正要跑的时候,由你在本地终端或数据库客户端里执行,再把结果或者报错贴回对话里。这样既安全,也更容易定位问题到底出在模型输出还是出在你的环境。
4.2 打开一条切换前创建的老会话
分两种情况,别混着判断:
- 如果这条老会话当初就是用
taotoken这个名字创建的,它现在应该正常打开。 - 如果它当初记的是
sub_lxapi或者别的名字,那它还是会报同样的错,因为库里那条记录的名字没变过。这不是配置没配好,是历史遗留,交给第 5 章处理。
这套接法保证的是“从现在起不再产生新的残留”,而不是凭空把旧名字擦掉。
4.3 去控制台核对这次调用有没有记上
新会话能正常返回之后,打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 看一眼用量和调用记录,确认刚才那一次请求记在了你刚创建的那把 Key 上。这一步顺便把“Key 正确、Key 属于你、Key 有额度”三件事一次验完,比在 Codex 里反复试请求省事得多。
5. 已经踩了 sub_lxapi 的坑,怎么收尾
5.1 先只读检查,别急着改
老会话报错、你又想抢救,第一步是确认残留分布在哪一层,而不是直接替换字符串。先备份整个.codex目录,然后在本地终端里跑只读查询:
# 根目录数据库:Codex 实际读的那份 sqlite3 ~/.codex/state_5.sqlite \ "SELECT COUNT(*) FROM threads WHERE model_provider='sub_lxapi';" # 看看具体是哪几条会话 sqlite3 ~/.codex/state_5.sqlite \ "SELECT id, model_provider FROM threads WHERE model_provider='sub_lxapi' LIMIT 20;" # 会话文件里还有没有引用 grep -rl "sub_lxapi" ~/.codex/sessions | headWindows 下把路径换成C:\Users\用户名\.codex\state_5.sqlite。这些 SQL 在你自己的终端里执行,不要让 AI 工具替你连库跑;模型可以帮你写 SQL、解释这条 SQL 在查什么,执行权留在你手上。
5.2 把旧值统一成一个稳定名字
思路是:先确认config.toml里已经有[model_providers.taotoken]这段定义,然后把旧记录里的名字统一改成taotoken。数据库两份都要动,根目录那份是关键,子目录那份是为了保持一致:
-- 根目录:Codex 实际读取 UPDATE threads SET model_provider='taotoken' WHERE model_provider='sub_lxapi'; -- 子目录副本:保持一致,避免下次又读出一份旧值 UPDATE threads SET model_provider='taotoken' WHERE model_provider='sub_lxapi';会话文件要逐行处理,不要做全文替换——JSONL 里其他字段也可能出现同名字符串,全文替换会误伤。
#!/usr/bin/env python3 """把 Codex session 里的旧 provider 名改成统一名字,改前请备份 .codex 目录""" import json import pathlib OLD, NEW = "sub_lxapi", "taotoken" root = pathlib.Path.home() / ".codex" / "sessions" for path in root.rglob("rollout-*.jsonl"): raw = path.read_text(encoding="utf-8") out, changed = [], 0 for line in raw.splitlines(): try: item = json.loads(line) except json.JSONDecodeError: out.append(line) continue payload = item.get("payload") or {} if item.get("type") == "session_meta" and payload.get("model_provider") == OLD: payload["model_provider"] = NEW item["payload"] = payload changed += 1 out.append(json.dumps(item, ensure_ascii=False, separators=(",", ":"))) if changed: (path.parent / (path.name + ".bak")).write_text(raw, encoding="utf-8") path.write_text("\n".join(out) + "\n", encoding="utf-8") print(f"{path} 修改 {changed} 处")改之前先把 Codex 完全退出。它在运行状态下可能把内存里的旧状态再写回数据库,刚改完就被覆盖,会让人误以为脚本没生效。
5.3 改完把 provider 名锁死,别再改它
清理完之后再打开那条老会话,通常就能正常加载了。接下来真正防止复发的动作只有一个:以后不管换什么模型,model_provider = "taotoken"这行别再动。要换的是model = "YOUR_MODEL_ID",以及 TaoToken 侧实际调用哪个模型。名字不变,sessions 和 state_5.sqlite 就不会再产生新的不一致。
6. 这套配置最容易踩的几个错
6.1 报 provider 找不到,但明明写了
多数时候是名字对不上:model_provider = "taotoken"配[model_providers.taotoken]才是合法组合,写成[model_providers.taotoken_api]就会重新触发同款报错,只是名字从sub_lxapi换成了你新起的那个。改完配置一定回头对一遍这两处字符串是否完全一致,大小写也算。
6.2 401 和 404 分别指向不同的地方
401 基本是凭据问题:环境变量没导出、终端窗口重开后丢了、或者 Key 粘贴时带了空格。先在一个新终端里echo $TAOTOKEN_API_KEY(Windows 用echo $env:TAOTOKEN_API_KEY)确认变量真的有值。
404 更常见于路径写错:base_url末尾手滑加了/v1,工具本身又拼一次版本路径,请求就落到不存在的地址上。统一填https://taotoken.net/api即可。
6.3 第一次请求就报模型不存在
先怀疑模型 ID 抄错了。模型广场里的 ID 会变,别用几个月前文章里的字符串,也别自己拼日期后缀。以 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 上模型广场当时列出的名字为准,复制粘贴。
7. 接下来做两件小事
第一件,用同一把 Key 在 模型对话 里发一条消息,确认通道、Key、模型 ID 三样东西在当前状态下都对得上。这一步排掉的是环境问题,比在 Codex 里反复重开会话高效。
第二件,如果你接下来要长时间用 Codex 写代码,可以看一下 Coding Plan 的额度安排;需要新 Key 或者想给不同项目分不同 Key,在 控制台 API Keys 里建,建完记得顺手把环境变量也更新掉。如果你同时在用 Claude Code,环境变量对照可以看 Claude Code 接入文档,把 provider 名固定成同一个的习惯照搬过去,同样能省掉一批“切换后旧会话打不开”的麻烦。
回到那条老会话:名字统一、配置稳定之后,它加载靠的就是数据库里那串再也不会变的字符。真正需要长期维护的,只剩你自己的模型 ID 而已。