最近帮团队把 Codex CLI 切到公司内部的 OpenAI 兼容网关时,我踩了一整天的 401 和 404,最后发现根因都藏在config.toml的细节里:一个env_key名字写错、多拼了一层/v1、甚至旧登录 token 没清干净,都会让终端看起来像在“玄学报错”,实际全是配置问题。如果你也打算把 Codex CLI 从官方默认接口换到第三方 API(自建推理服务、内部网关、或市面上那些 OpenAI 兼容接口服务商),这篇按实操顺序来写的东西应该能帮你少走几小时弯路。
我会把结论放在前面:Codex CLI 本质上把“官方 OpenAI”和“第三方服务”都当成同一个东西——一个带模型名、接口地址、认证方式的 provider。你要做的事就是新增一个 provider,写好config.toml,把 Key 放到环境变量里,再把model_provider指过去。下面拆开讲配置结构、Key 放置方式、完整示例,以及 401/404 的定位思路。
1. 接入前先弄懂 Codex CLI 的配置加载机制
1.1 config.toml 到底在哪、哪些配置会被读取
Codex CLI 的默认配置文件是~/.codex/config.toml,这是最常用、也是社区里发配置片段时默认提到的路径。如果你设过CODEX_HOME,那整个配置目录会跟着变,去$CODEX_HOME/config.toml找就行。还有一部分运行参数可以通过命令行覆盖,不过日常接第三方 API,90% 的工作都在这个 TOML 文件里完成。
需要注意一个细节:CLI 通常是每次启动会话时读取配置,运行中的交互会话不会热加载。你改完config.toml后,需要退出当前codex进程重开,新配置才会生效。我以前就干过“改完配置直接在同一个会话里继续跑”,结果反复报错都没意识到是配置没重新加载。
加载优先级大致是这样:命令行参数 > 环境变量 >config.toml。也就是说,如果你在环境变量里设了OPENAI_API_KEY,而config.toml里对应 provider 的env_key也指向它,那这个变量会在请求时被读走。理解这个顺序很重要,后面排查“为什么我改了 config 却还走旧 Key”时会用到。
1.2 最小可用配置长什么样
如果你完全使用官方 OpenAI API,并且已经设置了OPENAI_API_KEY环境变量,其实可以一个config.toml都不写。Codex CLI 内置了官方 provider 的默认值:base_url = "https://api.openai.com/v1"、env_key = "OPENAI_API_KEY"、wire_api = "chat"。但只要你切到第三方,就必须显式写出完整 provider 定义,不然它只会按内置默认值去连官方地址。
一个最基础但结构完整的配置长这样:
model = "gpt-5-codex" model_provider = "openai" [model_providers.openai] name = "OpenAI" base_url = "https://api.openai.com/v1" env_key = "OPENAI_API_KEY" wire_api = "chat"这里每项的作用分别是:
model:全局默认模型名。Codex 启动时会用它向服务端发请求。model_provider:指定用哪个 provider,对应下面[model_providers.xxx]的xxx。name:provider 显示名,主要是给人看的。base_url:API 根地址。官方是https://api.openai.com/v1,注意它已经包含/v1。env_key:Codex 从哪个环境变量里读 API Key。wire_api:接口协议风格,chat表示走/chat/completions这类 Chat Completions 接口。
1.3 第三方 API 接入的本质工作
把“接第三方 API”这件事拆开,其实就是四步:新增一个[model_providers.xxx]段、把base_url指向第三方地址、把env_key指向一个新的环境变量、把model和model_provider切换过去。
用一个生活化类比:config.toml相当于一本通讯录,每次新增服务就像往里加一张名片。base_url是对方的电话号码,env_key是约定的暗号,wire_api是沟通时用的方言。Codex 启动时读到model_provider = "xxx",就翻到那张名片,照着上面的号码打过去,再掏出暗号给对方看。谁的名片写错了,或者暗号没带对,对方自然不认你。
搞清楚这个逻辑后,后面所有问题其实都能归到两大类:401是“暗号不对或对方不认你”,404是“号码打过去但路径不存在”。接下来先把 Key 的事讲清楚,再给完整配置,最后回过来看 401/404。
2. API Key 到底放哪里最稳
2.1 三种常见放法对比
接入第三方 API 时,Key 的存放方式是新手最容易纠结的地方。我见过有人把 Key 直接写死在config.toml的http_headers里,也有人图省事复制到model_provider字段旁边,结果版本一升级直接报解析错误。这里对比三种常见做法:
| 存放方式 | 优点 | 风险 | 推荐度 |
|---|---|---|---|
| 环境变量 | 标准做法,Codex 原生支持,不易误提交 | 每次 shell 要 export,管理稍麻烦 | 强烈推荐 |
写在config.toml | 配置集中,一眼能看到 | 明文落盘,容易跟着配置文件一起提交到仓库 | 不推荐 |
| 系统凭据管理器 | 更安全 | 配置流程复杂,Codex 不原生支持 | 暂不推荐 |
Codex 官方推荐的方式就是环境变量。你在 provider 里写env_key = "MY_API_KEY",Codex 发起请求时就会从当前进程的环境变量里读取MY_API_KEY。终端里可以这样设置:
export MY_API_KEY="sk-xxxx"如果不想每次开终端都 export,可以把这一行写进~/.zshrc或~/.bashrc,或者用direnv这类工具按项目目录自动加载。Windows PowerShell 下用:
$env:MY_API_KEY="sk-xxxx"验证环境变量有没有生效,终端里执行:
echo ${MY_API_KEY:?not set}如果输出not set,说明变量没进当前进程,Codex 拿到空值自然就 401 了。
2.2 登录状态和 API Key 的优先级
这里有个非常隐蔽的坑:Codex CLI 本身支持两种认证方式,一种是 ChatGPT 账号登录(OAuth),一种是 API Key。只要你以前执行过登录操作,~/.codex/auth.json里就可能残留着一份 OAuth token。
这个残留 token 会导致一种很迷惑的现象:你明明在config.toml里配好了第三方env_key,也 export 了对应的环境变量,但 Codex 启动后仍尝试用旧 token 去请求,结果要么 401,要么请求被发去奇怪的地方。最直接的解决办法是执行codex logout清掉登录态,再用 API Key 方式重新登录或直接靠环境变量跑。
我在帮 A 同学排查时就是这样,他在自己机器上配了半天,curl 单独测接口是通的,codex一跑就 401。后来发现原来的 ChatGPT 登录 token 还留在auth.json里,CI 机器没有登录态,反而一次就通了。清掉本地登录态后问题消失。
2.3 安全红线:Key 不要进仓库
这一点说再多都不为过。把 Key 写进config.toml然后整个仓库推到远端,等于把访问凭证公开了。尤其是公司内部网关的 Key,可能有审计和配额限制,泄露后的风险比个人账号更大。
我的习惯是:
config.toml里永远只写env_key,不写真实 Key;- 项目里用
.env文件保存 Key,并确保.gitignore包含.env; - 提交配置样例时,用
sk-xxxx之类的占位符; - 如果怀疑 Key 已经泄露,立刻去服务端后台轮换。
3. 第三方 API 接入实操:config.toml 完整示例
3.1 场景设定
假设团队内部有一个统一模型网关,提供 OpenAI 兼容接口,地址是https://gateway.example.com/v1。网关管理员给服务分配的模型名是codex-gpt-5,并要求使用专属 API Key 访问。目标很简单:让本机 Codex CLI 走这个网关,而不是官方接口。
我用gateway.example.com做演示,你实际配置时替换成自己服务商或公司网关的真实地址。这个地址一定要以“版本根目录”结尾,也就是包含/v1,但不要包含具体的/chat/completions路径。
3.2 完整配置步骤
第一步,先设置环境变量:
export GATEWAY_API_KEY="sk-your-key"第二步,编辑~/.codex/config.toml,追加以下内容:
model = "codex-gpt-5" model_provider = "gateway" [model_providers.gateway] name = "Team Gateway" base_url = "https://gateway.example.com/v1" env_key = "GATEWAY_API_KEY" wire_api = "chat"第三步,重开 Codex CLI,用一句话验证能不能通:
codex exec "say hi"如果你的版本支持codex exec,正常情况下它会调用网关模型并返回回答。如果命令不存在,就看看codex --help,用当前版本支持的非交互参数验证。
第四步,如果网关还需要额外的请求头或查询参数(比如团队标识、审计ID、API 版本号),可以在 provider 段里加:
[model_providers.gateway] name = "Team Gateway" base_url = "https://gateway.example.com/v1" env_key = "GATEWAY_API_KEY" wire_api = "chat" query_params = { "api-version" = "2025-09-01" } http_headers = { "X-Team-Id" = "team123" }query_params会被拼到 URL 后面,http_headers会随每个请求发出。注意不要在http_headers里放 Key,密钥统一走env_key机制,这样日志里不容易泄露。
3.3 多套配置切换:用 profiles 维护不同场景
如果你不想每次改model_provider,也不想把配置弄乱,可以用 profiles 把官方场景和第三方网关场景分开。
model = "gpt-5-codex" model_provider = "openai" [model_providers.openai] name = "OpenAI" base_url = "https://api.openai.com/v1" env_key = "OPENAI_API_KEY" wire_api = "chat" [model_providers.gateway] name = "Team Gateway" base_url = "https://gateway.example.com/v1" env_key = "GATEWAY_API_KEY" wire_api = "chat" [profiles.gateway] model = "codex-gpt-5" model_provider = "gateway"不同版本对profiles的启动参数叫法略有区别,有些叫--profile,有些不支持。最靠谱的方式是codex --help看你手头版本的参数说明。但思路是通用的:把“用哪套模型、哪个 provider、什么模型名”打包成一个 profile,需要时切过去。
3.4 验证配置是否真正生效
很多人在这一步凭感觉判断,结果实际请求根本没走第三方。最简单的方法是从日志里看实际请求的 URL 和状态码。较新版本的 Codex CLI 支持通过--debug或RUST_LOG=debug打开调试输出,不同版本有差异,建议先codex --help确认。
打开 debug 后,正常日志里会出现类似这样的信息:
POST https://gateway.example.com/v1/chat/completions Authorization: Bearer sk-... status=200如果看到的 URL 是https://api.openai.com/...,那说明model_provider没切到 gateway,或者配置根本没被读取。这是最直接的验证方式,比瞎猜强一百倍。
4. 401 Unauthorized 排查实录
4.1 快速定位:是 Codex 的问题还是接口的问题
遇到 401,我建议第一件事不是改 Codex 配置,而是先用curl绕过 Codex 直接打接口。这样可以迅速区分问题在“客户端配置”还是“服务端认证”。
在终端里执行:
curl -i https://gateway.example.com/v1/chat/completions \ -H "Authorization: Bearer ${GATEWAY_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"model":"codex-gpt-5","messages":[{"role":"user","content":"ping"}],"max_tokens":8}'注意看-i返回的响应状态码:
- 如果 curl 也返回 401,问题大概率在 Key 本身或服务端认证逻辑;
- 如果 curl 返回 200、Codex 却 401,问题大概率在 Codex 侧,要么配置没生效,要么读错了环境变量。
这个环节能把排查范围缩小一半,比对着 Codex 日志猜快得多。
4.2 401 高频原因清单
下面这些原因是实际干活时遇到最多的,列成表方便对照:
| 原因 | 典型表现 | 解决办法 |
|---|---|---|
env_key指向的环境变量没设置 | Codex 报缺少 Key,或直接 401 | export 对应变量,用echo ${VAR:?}验证 |
| 环境变量名拼写不一致 | config 里写GATE_WAY_KEY,实际导出的是GATEWAY_API_KEY | 统一拼写 |
| Key 复制时带了换行或空格 | 请求头出现非法字符,服务端拒绝 | 用tr -d '\n'清理后再试 |
| 旧的 OAuth token 残留 | curl 通但 Codex 401 | 执行codex logout清掉auth.json |
| Key 过期、被禁用、余额不足 | 接口稳定 401 或 403 | 去服务商后台确认 |
| base_url 指向错误服务 | 用 A 家的 Key 打 B 家地址 | 核对 provider 的base_url |
这六类原因覆盖了我接触到的绝大多数 401。其中“旧 OAuth token 残留”最隐蔽,建议每次排查 401 时都先默认怀疑它。
4.3 三个真实排查片段
片段一,某开发者把 Key 放在了 provider 的http_headers里,想着“反正能发出去就行”,结果某个版本升级后,配置解析对http_headers里的敏感字段处理方式变了,Key 没被正确读取,一直 401。后来把 Key 改成env_key机制,问题消失。
片段二,某同学设置环境变量时多了个拼写差异:config 里写的是MY_PROVIDER_API_KEY,但终端实际 export 的是MY_PROVIDE_API_KEY,少了一个 R。这种错误肉眼很难发现,建议直接复制粘贴。
片段三,最经典的换行符问题。从网页后台复制 Key 到.env时,末尾带了一个看不见的换行符,程序取到的 Key 长度比正常多 1,服务端校验自然失败。处理方式是在终端里printf '%s' "$VAR" | wc -c对比一下官方后台显示的字符数。
4.4 顺带说一句:403 往往被误解成 401
排查时还会遇到 403 Forbidden,很多人会把它和 401 混为一谈。401 是“没有认证或认证失败”,403 是“认证成功但没权限”。第三方服务上常见的 403 原因包括:Key 欠费停用、IP 不在白名单、模型未对当前 Key 开放。
定位方式仍然是 curl 先测一遍,看 body 里的错误信息。比如返回insufficient_quota,那基本是余额问题,去后台充值或提额即可,跟 Codex 配置无关。
5. 404 Not Found 排查实录
5.1 一句话判断:是路径错还是模型错
404 的排查思路和 401 完全不一样。401 是“认证层挂了”,404 是“请求到达了服务器,但目标路径或资源不存在”。所以第一反应不要再去折腾 Key,而是看 URL 拼得对不对、路径有没有被网关正确转发。
继续用 curl 验证:
curl -i https://gateway.example.com/v1/chat/completions \ -H "Authorization: Bearer ${GATEWAY_API_KEY}" \ -H "Content-Type: application/json" \ -d '{"model":"codex-gpt-5","messages":[{"role":"user","content":"ping"}],"max_tokens":8}'如果返回 404,先检查 URL 本身。如果返回 200,说明接口和模型都没问题,问题在 Codex 侧路径配置或模型名。
5.2 404 高频原因清单
| 原因 | 典型表现 | 解决办法 |
|---|---|---|
base_url少写了/v1 | 所有请求 404 | 按服务商文档补全/v1 |
base_url多了/v1 | URL 变成/v1/v1/chat/completions | 去掉重复段 |
| 多写了完整接口路径 | base_url里带/chat/completions | 只留版本根目录 |
wire_api与接口不匹配 | 打了/v1/completions,但服务只支持/v1/chat/completions | 切换到chat或responses |
| 模型名不存在或未映射 | 返回model not found的 404 | 用服务商后台列出的模型名 |
| 网关前缀路径不对 | 内部转发时路径前缀丢失 | 找网关管理员确认 |
| 末尾带斜杠 | 出现 308/404 | 去掉 URL 末尾的/ |
5.3 路径拼接规则是 404 重灾区
很多人不理解为什么base_url不能顺手把/chat/completions也带上。Codex 的规则很简单:它会把base_url当作“根”,再根据wire_api自动拼接具体接口路径。比如base_url = "https://gateway.example.com/v1"、wire_api = "chat",最终请求就是:
https://gateway.example.com/v1/chat/completions如果你错误地写成base_url = "https://gateway.example.com/v1/chat/completions",最终请求就会变成:
https://gateway.example.com/v1/chat/completions/chat/completions这显然会 404。记住一个口诀:base_url只写到版本根目录,/v1算根目录的一部分,别往后面加了。
5.4 模型名相关的 404
有些服务端为了做模型路由,对“不认识的模型名”也会返回 404,而不是常见的 400。比如网关里只注册了codex-gpt-5,你的 config 里写的是gpt-5-codex,请求照样到网关,但网关找不到对应模型,于是 404。
验证方法很简单:curl 里分别用两个模型名打一次,看哪个能通。或者直接去服务商后台的模型列表页,复制一个真实存在的模型名填进model。如果网关支持模型别名映射,也可以让管理员加一条规则,把官方模型名转发到内部实际模型。
6. 最后分享几个长期有效的操作习惯
6.1 调试三板斧:curl、日志、最小复刻
我后来给团队写接入文档时,固定要求成员按这个顺序排查:
- 先用 curl 直连第三方接口,确认接口本身可用、模型名正确、Key 有效;
- 再开 Codex debug 日志,确认实际请求的 URL、请求头、状态码;
- 最后把
config.toml精简到最小可复现配置,排除多 provider 互相干扰。
“最小复刻”尤其管用。有人配置里同时写了好几个 provider,还有 profiles,切换时互相覆盖,最后自己也搞不清走的是哪套。与其在复杂配置里猜,不如先删到只剩一个 provider、一个 model、一个env_key,通了再逐步加回来。
6.2 团队协同时的 Key 管理
如果团队多人共用同一个网关,Key 管理建议单独拎出来规范:
- 每人一个独立 Key,方便审计和撤销;
.env文件不进仓库,仓库里只放.env.example;- CI 环境用平台自带的 Secrets 能力注入环境变量,不要写死在
config.toml; - 网关侧开启调用日志,谁调了什么模型、状态码多少,一查便知。
6.3 关于“为什么我改了配置没生效”
最后提一个最常见的疑问。很多人改完config.toml后没重启 Codex,认为“配置是实时读取的”,结果怎么改都不对。Codex 启动时会读取配置文件,运行中的会话通常不会重新加载。所以每次改配置,记得退出当前会话再重进。如果改了环境变量,更要确认变量已经进入当前 shell 进程,否则你 export 到 A 终端,却在 B 终端跑 Codex,等于白配。
我自己实际工作中还有一个习惯:每次接新服务商,先写一个verify.sh,脚本里放好 curl 命令和测试模型名,以后改网关、换 Key、升级版本都能一键验证,不用每次手动敲一长串请求。这个脚本成本很低,但能在以后每次排查时省下大量时间。