如果你以为把 Codex CLI 接到第三方 API 服务只是改改base_url的事,那我得先给你泼盆冷水——这事的常见结局是:一脸自信地改完 config.toml,然后贴一张 401 或 404 的报错截图去群里求助。我第一次接公司内部模型网关的时候,也抱着“就两三行配置”的心态,结果在config.toml里反复折腾了大半天,最后发现问题是 Key 压根没被 Codex 读到。这篇文章就把 Codex CLI 接第三方 API 这件事彻底拆开讲清楚:config.toml怎么写、API Key 放哪里、401/404 要怎么一步步排查,以及那些文档里不会写但你迟早会踩的坑。
1. 折腾前必须想清楚:第三方 API 接入的本质
1.1 默认配置其实就是一条“官方通道”
Codex CLI 开箱即用,是因为程序内部内置了一套默认的模型提供商(model_provider)配置:默认服务地址、默认模型名、默认的环境变量名全都写死了。你在终端里运行 codex 就能直接对话,不是因为你配置了什么,而是因为这套默认值已经藏在程序里。
那“接入第三方 API”是什么意思?说白了,就是绕过这套默认值,让 Codex CLI 把请求发到别的地方去。它不关心对端是谁,只按照你在config.toml里告诉它的几个信息工作:用哪个模型名请求、按哪一组连接参数发送、用哪把钥匙开门。
这个认知非常重要,因为后面所有的 401/404 排查,本质上都是在这几个信息里找“对不上的那个环节”。
1.2 model_provider 是连接参数的聚合点
在config.toml里,model和model_provider是两码事。model只是一个名字,model_provider才决定“怎么连”。
打个比方:model是收件人姓名,model_provider是快递公司的发货地址。同一个model名,配上不同的 provider,请求会被送到完全不同的地方。所以接入第三方 API 的第一步不是急着改 model,而是先想清楚 provider 这边要定义哪些参数。
1.3 哪些场景值得去折腾第三方 API
最常见的几种场景:
- 公司内部搭建了统一的模型网关,希望所有工具都走同一个入口,方便做计费和审计;
- 本地起了兼容 OpenAI 协议的服务(例如 Ollama、vLLM 这类),不想让代码和数据出网;
- 需要一个统一的中转层,把不同模型供应商的接口风格差异屏蔽掉;
- 出于成本或合规考虑,需要把请求路由到特定的后端服务。
这些场景的共同点只有一个:你需要告诉 Codex CLI,“换个地址发送,换把钥匙开门”。而完成这一切的入口,就是~/.codex/config.toml和model_providers配置块。
2. config.toml 的最小骨架:位置、核心字段与语法要点
2.1 配置文件的位置与加载优先级
Codex CLI 的配置文件默认放在用户目录下:macOS/Linux 是~/.codex/config.toml,Windows 是%USERPROFILE%\.codex\config.toml。文件不存在也不影响启动,因为默认值在程序里兜底;但一旦你想接入第三方 API,就必须从这里开始。
另外,Codex CLI 支持用--config参数显式指定配置文件路径。这个功能在你有多个接入目标时会非常有用,后面专门讲多配置文件方案时我会再展开。
2.2 一份最小可用的配置长什么样
先看一个能直接跑的骨架:
# ~/.codex/config.toml model = "your-model-name" model_provider = "my-gateway" [model_providers.my-gateway] name = "My Gateway" base_url = "https://your-gateway.example/v1" env_key = "MY_GATEWAY_API_KEY" wire_api = "chat"这里每一行都有明确作用:
model:告诉 Codex CLI 用哪个模型名去请求。注意,这个名字不是 Codex 官方那边说了算,而是由你接的第三方服务商定义的;model_provider:指定使用下面哪一个[model_providers.xxx]连接配置;base_url:API 的基础地址,所有请求都会在这个地址上拼接路径;env_key:指定从哪个环境变量读取 API Key,这是 Key 管理的推荐方式;wire_api:请求协议类型,通常是chat或responses,对应两种不同的后端接口风格。
2.3 TOML 里三个容易翻车的小细节
TOML 语法本身很简单,但越简单越容易在细节上翻车:
段名必须顶格。
[model_providers.my-gateway]这一行不能有缩进,也不能跟在别的字段后面。之前见过有人把段名写在model_provider那一行的下面,还没有空行分隔,Codex 解析配置时会直接报错或者干脆忽略掉这一段。字符串记得加双引号。base_url、env_key 这类值都要用双引号包起来,写成
base_url = https://xxx这种不带引号的形式,TOML 解析器不会把它当作合法字符串。不要依赖注释里的空格。
#后面怎么写都无所谓,但要确保配置项前面没有多余的缩进或空格,尤其是从文档里复制配置的时候,很容易复制进来一些不可见字符。
3. model_providers 配置块:第三方 API 的完整接入写法
3.1 直接可以抄的接入示例
下面这份配置,是我实测过比较稳妥的一种写法,适用于大多数兼容 OpenAI 协议的第三方网关:
model = "your-model-name" model_provider = "thirdparty" [model_providers.thirdparty] name = "Third Party Gateway" base_url = "https://api.thirdparty.example/v1" env_key = "THIRDPARTY_API_KEY" wire_api = "chat"如果服务商明确支持新的 Responses 协议,只需要把wire_api改成responses:
[model_providers.thirdparty] name = "Third Party Gateway" base_url = "https://api.thirdparty.example/v1" env_key = "THIRDPARTY_API_KEY" wire_api = "responses"配置块里几个字段的作用和注意事项,我整理成了表格:
| 字段 | 作用 | 是否必填 | 备注 |
|---|---|---|---|
| name | 提供商显示名 | 否 | 日志和调试时会看到 |
| base_url | API 基础地址 | 是 | 决定请求发到哪里 |
| env_key | API Key 的环境变量名 | 与 api_key 二选一 | 推荐用环境变量方式 |
| api_key | 直接写 API Key | 与 env_key 二选一 | 不推荐,但临时调试很省事 |
| wire_api | 请求协议类型 | 是 | chat 或 responses |
3.2 base_url 的拼接规则:/v1 到底怎么算
这是 404 出现频率最高的根源,值得单独拉出来讲。Codex CLI 不是把 base_url 当作完整接口地址去用的,它会在 base_url 后面继续拼接具体路径:
- 当
wire_api = "chat"时,Codex 会请求{base_url}/chat/completions - 当
wire_api = "responses"时,Codex 会请求{base_url}/responses
所以 base_url 必须写到“版本前缀为止”,绝对不要把完整接口路径写进去。常见错误:
# 错误:Codex 会拼成 /chat/completions/chat/completions base_url = "https://api.thirdparty.example/v1/chat/completions"# 错误:尾部带斜杠,拼出来变成 /v1//responses base_url = "https://api.thirdparty.example/v1/"正确的写法是保持简洁:
base_url = "https://api.thirdparty.example/v1"另外注意版本前缀。绝大多数服务商用的是/v1,但也有少数用/v2或自定义路径。如果你配了/v1而服务商实际路径是/v2,一样是 404。先翻文档确认版本前缀,再下手。
3.3 wire_api 选 chat 还是 responses:不同服务端的差异化
wire_api这个字段的作用是告诉 Codex CLI 用哪种协议格式发请求。选错的话,表现往往不是“报错告诉你协议不对”,而是 404 或者 400,非常迷惑。
responses:官方新版协议,请求路径是/responses。如果你接的是官方最新服务,选它;chat:绝大多数第三方网关、自建服务、兼容层支持的协议,请求路径是/chat/completions。这也是接入第三方 API 时最稳妥的选择。
怎么确定服务商支持哪种?两个办法:先查服务商文档里的 API 地址;如果文档写得含糊,就先用 curl 分别打一下/v1/chat/completions和/v1/responses,看哪个路径能通。哪个通,就配哪个 wire_api。
3.4 env_key 与 api_key:Codex 读 Key 的两种路径
model_providers配置块里,API Key 有两种提供方式:
- env_key:指定一个环境变量名,Codex 启动时会去读这个环境变量的值作为 API Key;
- api_key:直接在配置里把 Key 写死。
两种方式二选一即可。同时写的情况下,不同版本 Codex 的优先级行为可能有差异,为了避免“我明明改了 Key 怎么还是旧 Key”的怪问题,建议只保留一种。
我的习惯是:日常使用只用 env_key,临时调试时偶尔在配置里临时塞一个 api_key 测试,测试完立刻删掉。
4. Key 放哪里:环境变量、写在配置里、auth.json 三种方式的取舍
4.1 首选方案:环境变量 + env_key
这是最推荐的方式。操作分两步:
第一步,把 Key 放进 shell 环境变量。以 macOS/Linux 为例,在~/.zshrc或~/.bashrc里加一行:
export THIRDPARTY_API_KEY="sk-你的key"第二步,在 config.toml 的 provider 里指定 env_key:
[model_providers.thirdparty] name = "Third Party Gateway" base_url = "https://api.thirdparty.example/v1" env_key = "THIRDPARTY_API_KEY" wire_api = "chat"这样 Codex CLI 启动时就会自动从环境变量里取 Key。修改环境变量之后记得新开一个终端,或者source ~/.zshrc,否则加载的还是旧值。
4.2 备选方案:config.toml 里的 api_key 字段
直接写在配置里的方式,长这样:
[model_providers.thirdparty] name = "Third Party Gateway" base_url = "https://api.thirdparty.example/v1" api_key = "sk-你的key" wire_api = "chat"优点:配置一次就完事,不用操心环境变量。缺点也很明显:config.toml 是纯文本,别人 cat 一眼就能看到你的 Key;如果哪天手滑把整个目录提交到了代码仓库,那 Key 就相当于裸奔了。
如果一定要用这个方式,至少做好两件事:
- 配置文件权限收紧:
chmod 600 ~/.codex/config.toml - 把
.codex目录加进.gitignore,或者至少绝对不要把 config.toml 提交到仓库
4.3 auth.json 与 codex login 的适用边界
Codex CLI 的codex login命令会把登录后的会话信息写到~/.codex/auth.json,走的是 OAuth 登录体系。这个文件在接入第三方 API 时一般用不上——除非你的服务商专门接入了 OAuth 认证流程。
容易踩的一个坑是:你曾经用codex login登录过官方账号,然后改用环境变量方式接第三方 API,结果 Codex 仍然优先使用 auth.json 里的登录态,导致请求没走你配置的 provider,或者 Key 对不上。遇到这种情况,先检查 auth.json 是不是还在,临时改名或移除它再试一次。
4.4 权限设置与团队协作防泄漏经验
团队协作场景下,我最常看到的问题是:成员把 Key 直接写进自己机器上的 config.toml,然后截图转发给同事。这等于把 Key 公开了。更安全的做法是:
- 环境变量 Key 放入开发机自己的 profile 文件,不上传、不截图;
- 在 CI 环境里,把 Key 设置到 CI 的 secret 变量中,不写进配置文件;
- 定期轮换 Key。如果怀疑某个 Key 可能泄露了,直接去服务商后台吊销重建,比在配置文件里改来改去可靠得多。
5. 401 Unauthorized 排查:从“Key 没读到”到“服务端拒了”
5.1 401 的本质与先手判断
401 的意思是“认证失败”:服务端收到了请求,但没认出你的身份。问题要么出在“Codex 没有拿到 Key”,要么出在“拿到 Key 了,但服务端不认这个 Key”。
排查 401 时,最忌讳一上来就去问服务商“我的 Key 怎么无效”。正确的第一步,先确认本机的 Key 有没有被正确加载。
5.2 第一步:确认 env_key 与环境变量真的对上了
在终端里直接检查环境变量是否存在:
env | grep THIRDPARTY_API_KEY如果什么都搜不到,说明环境变量根本没设进去。另一个很容易踩的坑:env_key在 config.toml 里写错了。比如实际环境变量叫THIRDPARTY_API_KEY,config.toml 里却写成THIRDPARTY_KEY,Codex 读不到,就会用空字符串去请求,服务端当然 401。
还有一个调试技巧:临时在 provider 里加一个明显错误的api_key,比如api_key = "sk-wrong-key"。如果报错从“API key is required”变成“invalid api key”或“unauthorized”,说明环境变量这条路是通的,问题出在 Key 本身。用这个办法可以快速区分“读取问题”和“效验问题”。
5.3 第二步:curl 手测,绕开 Codex 单独验证
排查 401 最有效的工具就是 curl。它可以直接绕过 Codex CLI,把我们关心的请求原样打到服务商那里,看服务端到底认不认这个 Key。
curl -sS https://api.thirdparty.example/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer ${THIRDPARTY_API_KEY}" \ -d '{"model":"your-model-name","messages":[{"role":"user","content":"ping"}]}'注意Authorization头用的是Bearer方式。把THIRDPARTY_API_KEY换成你实际定义的环境变量名。执行后分两种情况:
- curl 也报 401:基本断定是 Key 本身的问题,过期、被吊销、没权限、IP 白名单限制都有可能;
- curl 通了,但 Codex 报 401:问题出在 Codex 到服务商之间的链路,最可疑的是代理或请求头的传递。
5.4 代理与 Header 丢失:一个容易被忽略的 401 场景
很多开发机处于企业内网环境,需要设置HTTP_PROXY、HTTPS_PROXY、ALL_PROXY环境变量才能访问外网服务。Codex CLI 基于 Node 运行时,会读取这些代理环境变量。
问题在于,某些代理实现会重组请求头,尤其是对Authorization头处理得不够严谨,可能导致 Key 在代理节点丢失或被改写,最终服务端收到的还是没带 Key 的请求。
验证方法很简单:临时清空代理变量再跑一次 Codex,或者用 curl 加上-x参数单独测试带代理和不带代理两种情况。如果清空代理后正常,就要检查代理配置是否对该域名做了特殊处理。
6. 404 Not Found 排查:base_url 拼接与模型名错位的根因
6.1 404 的三种含义
401 是“不认识你”,404 是“找不到东西”。但“找不到”也有区分:
- 路径不存在:请求发到了一个根本没有的接口路径;
- 模型不存在:路径是对的,但这个模型名在服务商那边没有;
- 接口协议不匹配:服务商只开放了 chat 接口,你配了 responses,请求自然落到不存在的路径上。
所以排查 404 时,第一步先判断自己遇到的是哪一种。
6.2 base_url 拼接错误案例集
base_url 拼接出错是 404 的头号原因。我把常见的错误写法和后果整理成一张表:
| base_url 写法 | Codex 实际请求路径 | 结果 |
|---|---|---|
https://api.thirdparty.example/v1/chat/completions | /v1/chat/completions/responses | 404 |
https://api.thirdparty.example/v1/ | /v1//responses | 部分网关 404 |
https://api.thirdparty.example | /responses | 404,少了版本前缀 |
https://api.thirdparty.example/v2 | /v2/responses | 404,版本号对不上 |
排查方法:把 base_url 改成服务商文档里给的最简形式,先不要加任何多余的路径,然后分别试试chat和responses两种 wire_api 哪一个能通。
6.3 模型名与 wire_api 不匹配的典型案例
有些网关做了模型映射,对外暴露的模型名和实际模型名不一致;有些则要求模型名带特殊前缀或后缀。你拿官方模型名去请求第三方网关,如果对方没有对应映射,服务端会返回一个包含类似model not found的 404。
注意 404 的响应体往往有明确提示。比如:
{ "error": { "message": "The model 'your-model-name' does not exist" } }这个提示比状态码本身更有价值。遇到 404,先看响应体里的 error.message,再决定是改 base_url 还是改模型名。
6.4 用日志、curl、响应体把 404 定位到根因
完整的定位步骤,我建议按这个顺序来:
- 开启 Codex CLI 的调试日志,观察它实际请求的完整 URL;
- 用 curl 分别打
/v1/chat/completions和/v1/responses,确认哪条路径是通的; - 把 curl 的响应体完整打出来,不要只看状态码,error.message 才是关键线索;
- 根据上面的信息,反向修正 base_url、wire_api 或 model 名。
7. 多 Provider 切换与日常使用经验
7.1 一份配置定义多个 Provider 后如何切换
实际使用中,很多人不止一个接入目标:公司网关一套、本地测试一套、官方服务一套。config.toml是支持定义多个 provider 的:
model = "your-model-name" model_provider = "gateway-a" [model_providers.gateway-a] name = "Gateway A" base_url = "https://gateway-a.example/v1" env_key = "GATEWAY_A_KEY" wire_api = "chat" [model_providers.gateway-b] name = "Gateway B" base_url = "https://gateway-b.example/v1" env_key = "GATEWAY_B_KEY" wire_api = "responses"切换默认 provider 只需要改model_provider这一个字段。注意,model和model_provider是全局的,所以换 provider 时也要确认对应的模型名在那个服务商那边存在。
7.2 多配置文件方案:比频繁改字段更稳
如果你的接入目标实在太多,频繁改model_provider也很烦。我更推荐做成多个配置文件,每个接入目标一个:
# ~/.codex/config.gateway-a.toml model = "model-a" model_provider = "gateway-a" [model_providers.gateway-a] name = "Gateway A" base_url = "https://gateway-a.example/v1" env_key = "GATEWAY_A_KEY" wire_api = "chat"# ~/.codex/config.gateway-b.toml model = "model-b" model_provider = "gateway-b" [model_providers.gateway-b] name = "Gateway B" base_url = "https://gateway-b.example/v1" env_key = "GATEWAY_B_KEY" wire_api = "responses"然后启动时用参数指定配置:
codex --config ~/.codex/config.gateway-a.toml codex --config ~/.codex/config.gateway-b.toml还可以在 shell 里配两个别名,一个指向一个配置,切换起来非常顺手。
7.3 实测下来最值得记住的三条经验
最后分享三条我用下来的体会:
接入一个新的服务商之前,先用 curl 把接口测通,再动 Codex 配置。这一步至少能省掉一半的排查时间。绝大多数 401/404 在 curl 阶段就能定位。
保存一份“已知可用”的配置。不管什么时候出问题,先切回这份配置,确认是环境问题还是新配置的问题,再继续排查。
别被状态码表面骗了。401 不一定就是 Key 错,404 不一定是地址错。Codex CLI 报错时,真正的线索在调试日志、请求 URL 和响应体的 error.message 里。状态码只是入口,不是结论。