# Codex 报「unexpected status 401 Unauthorized」或「Missing environment variable」：原因与修复

> https://router.one/zh/codex-401-unauthorized 的 Markdown 镜像，供 AI 助手与爬虫使用。Router One 是 OpenAI 兼容的统一 LLM API 网关。
> 最后更新：2026-11-02

这两条报错都和 Codex 发给 model provider 的 API Key 有关，只是出现在不同的时刻。按 openai/codex 源码（rust-v0.166.0，2026-11-02 核对），配了 env_key 的自定义 provider 从 env_key 指定的环境变量里读取 Key；这个变量没设、为空或只有空白字符时，Codex 不发请求，直接打印以「Missing environment variable」开头、带变量名的提示。「unexpected status 401 Unauthorized」出现在请求发出之后，同一行里的 url 指出了是哪台服务器返回的 HTTP 401。url 是 https://api.router.one/v1/responses 时，说明 Router One 拒绝了这把 Key：Key 缺失或无效时，Router One 在调用任何模型之前就返回 401。用本页的 Router One provider 块时，在启动 Codex 的 shell 或编辑器里把 ROUTER_ONE_API_KEY 设成控制台 → API 密钥里当前有效的 Key，或者写进 ~/.codex/.env（Codex 启动时会加载它，其中的值优先于 shell 里的值），再重新启动 Codex。网关在中国大陆可直连，不需要 VPN。

## 两条报错分别是什么意思

在 Codex 的终端界面里，两条报错都显示为红色，开头是 ■。按 openai/codex 源码（rust-v0.166.0，2026-11-02 核对），它们的组成如下，第一行带的是一键安装写入的 env_key_instructions：

`Codex`

```text
■ Missing environment variable: `ROUTER_ONE_API_KEY`. Create an API key at https://router.one/dashboard/api-keys and set the ROUTER_ONE_API_KEY environment variable.
■ unexpected status 401 Unauthorized: <服务端的错误信息>, url: <Codex 请求的地址>, request id: <id>
```

- 第一行写出 env_key 指定的变量名；provider 块里设了 env_key_instructions 时，后面再接这段说明。Codex 在准备请求时读取这个变量，值为空或只有空白字符也按没设处理，而且不会重试，所以这次请求根本没有发到任何服务器。
- 第二行冒号后面是服务端自己的错误信息：JSON 错误正文里有 error.message 就用它，没有就原样打印正文（最多 1000 字节），正文为空时写 Unknown error。后面依次是 `, url: …`（Codex 请求的地址）、响应带 cf-ray 头时的 `, cf-ray: …`、响应带 x-request-id 头（没有时改读 x-oai-request-id）时的 `, request id: …`，以及响应带 x-openai-authorization-error 头、或 x-error-json 头里带错误码时的 `, auth error: …` 和 `, auth error code: …`。

## Missing environment variable：设好 Codex 要读的变量

Codex 从 env_key 指定的变量里读取 Key，而 env_key 写在顶层 model_provider 选中的那个 [model_providers.<id>] 块里：按本页的 Router One 配置是 ROUTER_ONE_API_KEY，你自己的配置块写了别的名字，就是那个名字。按 openai/codex 源码（rust-v0.166.0，2026-11-02 核对），Codex 读取启动时继承的环境变量，另外启动时还会加载 ~/.codex/.env（CODEX_HOME 下的 .env 文件），其中的同名变量会覆盖 shell 里的值。之后才设的、或者只在另一个终端里设的变量，它都看不到。先设好变量，再从带着这个变量的 shell 启动 Codex：

`terminal`

```bash
# macOS / Linux，当前终端：
export ROUTER_ONE_API_KEY="sk-your-router-one-key"
# 以后新开的终端（zsh；bash 改成 ~/.bashrc，macOS 上的 bash 改成 ~/.bash_profile）：
echo 'export ROUTER_ONE_API_KEY="sk-your-router-one-key"' >> ~/.zshrc
# 检查是否已导出且不是空白（不打印 Key）：
printenv ROUTER_ONE_API_KEY | grep -q '[^[:space:]]' && echo "已导出" || echo "没有导出，或者是空白"
# Windows PowerShell，当前窗口：
$env:ROUTER_ONE_API_KEY = "sk-your-router-one-key"
# 以后新开的窗口（保存为用户环境变量）：
[Environment]::SetEnvironmentVariable("ROUTER_ONE_API_KEY", "sk-your-router-one-key", "User")
```

- shell 配置文件在终端启动时读取：改完 ~/.zshrc、~/.bashrc 或 ~/.bash_profile，要新开一个终端，或者在当前终端里 source 一下。用 SetEnvironmentVariable 保存的变量只对之后新开的 PowerShell 窗口生效，所以当前窗口还要再用 $env: 设一次。
- IDE 插件：按 Codex 的配置基础文档（2026-11-02 核对），CLI 和 IDE 插件共用同一套配置层，所以编辑器里同样需要这个变量；插件里的 Codex 用的是编辑器启动时的环境。设好变量后，把编辑器完全退出再重新打开。
- ~/.codex/.env 由 Codex 自己读取，不受启动它的 shell 或编辑器影响：在里面写一行 `ROUTER_ONE_API_KEY=sk-your-router-one-key`，CLI 和编辑器里的 Codex 重新启动后都能读到。如果这个文件里已经有 ROUTER_ONE_API_KEY，Codex 用的就是那一行的 Key，要么保持更新，要么删掉。
- provider 块里没有任何凭据设置时，Codex 根本不带 Key。按 openai/codex 源码（rust-v0.166.0，2026-11-02 核对），没有打开 requires_openai_auth 的自定义 provider 只发送这个块自己配置的凭据：env_key 读到的 Key、experimental_bearer_token（Codex 配置参考不推荐这种写法），或者这个块的 auth、gateway_oauth 设置取得的令牌，不会用 codex login 保存的登录信息。请求不带 Key 时，Router One 返回 401。
- 如果报错里是别的变量名，比如 OPENAI_API_KEY，说明你的会话用的 provider 块就是这么写的。接 Router One 时，给 Key 单独用一个变量，像下面的 Router One 配置那样用 ROUTER_ONE_API_KEY，免得读取 OPENAI_API_KEY 的其他工具把你的 Router One Key 拿去用。

## unexpected status 401：先看 url

这一行里的 url 是请求发往的地址，看它就知道是谁返回的 401。按 openai/codex 源码（rust-v0.166.0，2026-11-02 核对），这个地址由当前 provider 的 base_url 加上 /responses 组成；内置的 OpenAI provider 用 API Key 登录时是 `https://api.openai.com/v1`，用 ChatGPT 登录时是 `https://chatgpt.com/backend-api/codex`，除非 ~/.codex/config.toml 里的 openai_base_url 给了它别的地址，这时内置 provider 的凭据会发到那个地址，包括 ChatGPT 登录令牌。内置 provider 先走 WebSocket，所以它的 Reconnecting... 和回退提示里，同一个端点可能显示成 wss:// 开头的地址：

| 报错里的 url | 说明什么 | 怎么办 |
| --- | --- | --- |
| https://api.router.one/v1/responses | Router One 拒绝了这把 Key。Key 缺失或无效（包括已吊销、已过期或不存在的 Key）时，Router One 在调用任何模型之前就返回 401，所以控制台 → 日志里没有这次请求的记录 | 从控制台 → API 密钥重新复制 Key，确认它仍然有效、没有过期，把 ROUTER_ONE_API_KEY 设成它，再重新启动 Codex（见下一节） |
| https://api.openai.com/v1/responses | 会话走的是 Codex 内置的 OpenAI provider（API Key 登录），请求发到了 OpenAI 的 API，没有经过 Router One | 在 ~/.codex/config.toml 第一个表头之前写上 model_provider = "router"，并配好下面那个 provider 块 |
| https://chatgpt.com/backend-api/codex/responses | 会话走的是内置 provider 的 ChatGPT 登录，这个登录被拒绝了 | 重新登录（先 codex logout 再 codex login），或者让本地会话改走下面的 Router One provider |
| 其他地址 | 当前 provider 块的 base_url，或者内置 provider 的 openai_base_url（内置 provider 的凭据会发到这个地址，包括 ChatGPT 登录令牌），指向了别处，比如别的服务，或者主机名写错了 | 用 model_provider = "router" 选中 [model_providers.router] 块，并在块里写 base_url = "https://api.router.one/v1" |

## Router One 拒绝了 Key 时

url 是 https://api.router.one/v1/responses 的 401，问题出在 Codex 发出的 Key，和模型无关。逐项检查，然后重新启动 Codex：

- Key 是当前有效的：它在控制台 → API 密钥里仍然存在、没有过期，而且变量里存的就是这把 Key，不是后来已经换掉的旧 Key。
- 值里只有 Key 本身：连引号一起粘进去、在前面多写了 Bearer，或者 Key 中间混进空格，都会被拒绝。按 openai/codex 源码（rust-v0.166.0，2026-11-02 核对），Codex 把变量的值原样放在 Bearer 后面；值里带换行时，这个值根本放不进 Authorization 头，请求就会不带 Key 发出去。
- 必须是 Router One 的 Key：变量里放的是其他服务的 Key（比如 OpenAI API Key）时，Router One 同样返回 401；请到控制台 → API 密钥创建一把 Router One 的 Key。
- ~/.codex/.env 里没有旧 Key：按 openai/codex 源码（rust-v0.166.0，2026-11-02 核对），Codex 启动时会加载这个文件，里面的 ROUTER_ONE_API_KEY 会覆盖 shell 里的值，所以旧的那一行要改掉或删掉。
- 会话用的是 Router One 的 provider 块。如果 ~/.codex/config.toml 改用 openai_base_url 把内置的 OpenAI provider 指向 Router One，按 openai/codex 源码（rust-v0.166.0，2026-11-02 核对），Codex 发的是它自己的 OpenAI 登录凭据（通常是 codex login 保存的那份），不是 ROUTER_ONE_API_KEY：改用下面的 provider 块，或者用 codex login --with-api-key 以你当前的 Router One Key 登录，绝不要与 ChatGPT 登录同用。
- 已经在运行的程序还拿着旧值：改变量之前就打开的编辑器或终端，发出的仍是旧 Key。
- Router One 返回的 401 在控制台 → 日志里没有记录，因为网关在调用任何模型之前就拒绝了请求。如果 Key 一直被拒，写邮件给 support@router.one 时附上报错时间和报错行里的 request id（如果有），但不要附上 Key 本身。

## 不启动 Codex，先检查 Key

Router One 的接口参考里，GET /v1/models 对无效或缺失的 API Key 返回 401，所以用它就能按 Codex 发送 Key 的方式测试这把 Key，而不用调用模型。在启动 Codex 的那个 shell 里运行：它测试的是这个 shell 里的 ROUTER_ONE_API_KEY；如果 ~/.codex/.env 里也设了这个变量，Codex 发的是那里的值。它只会打印 HTTP 状态码：200 表示 Key 被接受，401 表示被拒绝。

`terminal`

```bash
curl -s -o /dev/null -w "%{http_code}\n" https://api.router.one/v1/models -H "Authorization: Bearer $ROUTER_ONE_API_KEY"
# Windows PowerShell：
curl.exe -s -o NUL -w "%{http_code}" https://api.router.one/v1/models -H "Authorization: Bearer $env:ROUTER_ONE_API_KEY"
```

## 用一键命令把 Codex 接到 Router One

带上你的 Router One Key 运行一键命令。它会在缺少 Codex CLI 时先安装，用 Router One 的配置替换 ~/.codex/config.toml（env_key = ROUTER_ONE_API_KEY，以及对应的 env_key_instructions；原有的文件保留为 `config.toml.bak.<time>`，需要的其他设置可以从里面复制回来），把 Key 写进 shell 配置文件（zsh 是 ~/.zshrc，bash 是 ~/.bashrc，其他 shell 是 ~/.profile），再发一个测试请求，Key 无效时会报 401。Windows 用 PowerShell 那一行，它会把 Key 存成用户环境变量。之后在新开的终端里，或者在安装之后才启动的编辑器里运行 codex。macOS 上用 bash 时，新开的终端窗口读的是 ~/.bash_profile：除非它已经加载 ~/.bashrc，否则把同一行 export 也加进去。

`terminal`

```bash
curl -fsSL https://router.one/install/codex.sh | bash -s -- sk-your-api-key
# Windows PowerShell：
$env:ROUTER_ONE_API_KEY = "sk-your-api-key"; irm https://router.one/install/codex.ps1 | iex
```

## Router One 的 provider 配置

顶层的 model 和 model_provider = "router" 两行写在第一个表头之前，块里的 base_url、env_key、wire_api 三行照下面的样子保留，而且都写在 ~/.codex/config.toml 里：按 Codex 的高级配置文档（2026-11-02 核对），项目里的 .codex/config.toml 写了 model_provider 或 model_providers 会被忽略。这个 provider 不要打开 requires_openai_auth：按 Codex 认证文档（2026-11-02 核对），它会为这个 provider 打开 OpenAI 登录（用 ChatGPT 或 OpenAI API Key 登录），而 Router One 的 provider 用 env_key 读取 Key。一键安装写入的文件指向同一个 provider，另外加了 env_key_instructions，也就是 Codex 在缺少变量的提示后面打印的那段说明。

`config.toml`

```toml
# ~/.codex/config.toml
model = "gpt-5.6-sol"
model_provider = "router"

[model_providers.router]
name = "router"
base_url = "https://api.router.one/v1"
env_key = "ROUTER_ONE_API_KEY"
wire_api = "responses"

# 在 shell 里：
# export ROUTER_ONE_API_KEY=sk-your-router-one-key
```

## 常见问题

### 「unexpected status 401 Unauthorized」是 Router One 的报错吗？

这是 Codex 对 HTTP 401 的通用说法，不管 401 是哪台服务器返回的。按 openai/codex 源码（rust-v0.166.0，2026-11-02 核对），Codex 先打印 unexpected status、HTTP 状态和服务端的错误信息，再打印它请求的 url。url 是 https://api.router.one/v1/responses 时，是 Router One 拒绝了 Codex 发出的 Key。用 Router One provider 块时，这把 Key 来自 ROUTER_ONE_API_KEY：把它设成控制台 → API 密钥里当前有效的 Key，再重新启动 Codex。如果是用 openai_base_url 把内置 provider 指向了 Router One，发出去的是 Codex 自己的 OpenAI 登录凭据（包括 ChatGPT 登录令牌），不是 ROUTER_ONE_API_KEY（见「Router One 拒绝了 Key 时」一节）。url 是别的地址，说明请求没有发到 Router One。

### Codex 会重试 401 吗？

会，但每次发的都是同一把 Key。按 openai/codex 源码（rust-v0.166.0，2026-11-02 核对），HTTP 层从不重试 401；Codex 的 401 处理可能会重新加载并刷新已保存的 ChatGPT 登录，但不会改动通过 env_key 读到的 Key。之后采样循环会和其他意外状态码一样重试这个请求：在 Router One provider 这类走 HTTPS 的 provider 上最多 stream_max_retries 次（默认 5 次），等待时间逐次拉长，所以报错之前可能先出现几行 Reconnecting...。内置的 OpenAI provider 先走 WebSocket，这几次重试用完后会回退到 HTTPS，可能再重试同样的次数。重试再多也修不好 Key：改好变量，再重新启动 Codex。

### 401 那一行冒号后面写着 invalid token、Incorrect API key provided 或 detail Unauthorized，是什么意思？

那是服务端自己的错误信息，不是 Codex 的。按 openai/codex 源码（rust-v0.166.0，2026-11-02 核对），Codex 优先打印 JSON 错误正文里的 error.message，没有这个字段就原样打印正文（最多 1000 字节），正文为空时写 Unknown error，所以措辞取决于 url 对应的服务端。例如 OpenAI 的错误码文档（2026-11-02 核对）列出了 Invalid Authentication、Incorrect API key provided 等 401 错误；Router One 的接口参考里，示例 401 正文的错误信息是 invalid api key。正文形如 {"detail":"Unauthorized"} 时，说明服务端返回的就是这种格式，Codex 会原样显示。不管写的是什么，先看 url。

### 报错里的变量名是 OPENAI_API_KEY，或者是一个我从没设过的变量，为什么？

Codex 报的是会话所用 provider 块里 env_key 写的那个变量。按 openai/codex 源码（rust-v0.166.0，2026-11-02 核对），内置的 OpenAI provider 没有 env_key，所以缺少变量的提示一定来自某个自定义 provider 块：你自己写的、安装脚本写的，或者 Codex 的 profile 文件里的。检查 ~/.codex/config.toml 顶层的 model_provider 行，以及它指向的 [model_providers.<id>] 块。接 Router One 时，像本页的配置那样让 env_key 指向 ROUTER_ONE_API_KEY，并设好这个变量，免得读取 OPENAI_API_KEY 的工具用到你的 Router One Key。

### 变量已经设好了，VS Code 插件还是报 Missing environment variable，怎么办？

插件里的 Codex 用的仍是编辑器启动时的环境。按 Codex 的配置基础文档（2026-11-02 核对），CLI 和 IDE 插件共用同一套配置层，所以 Router One provider 在编辑器里同样需要 ROUTER_ONE_API_KEY。把编辑器完全退出（所有窗口都关掉）再重新打开。如果编辑器还是拿不到这个变量，就把 Key 写进 ~/.codex/.env，写成一行 `ROUTER_ONE_API_KEY=sk-your-router-one-key`：按 openai/codex 源码（rust-v0.166.0，2026-11-02 核对），Codex 启动时会自己加载这个文件，不管是谁启动了它；写好后再重启一次编辑器。

### codex login --with-api-key 提示 API key saved，为什么还是 401？

按 openai/codex 源码（rust-v0.166.0，2026-11-02 核对），这个命令保存 Key 时并不校验它（提示原文是 API key saved. Authentication is checked when you send a request.），而且保存的 Key 属于内置的 OpenAI provider：没有打开 requires_openai_auth 的自定义 provider 从不发送它。如果会话用的是 Router One provider，起作用的是 ROUTER_ONE_API_KEY 里的 Key；如果用的是内置 provider，401 那一行的 url 会显示这把 Key 发到了哪里；用 openai_base_url 把它指向 Router One 时，用 codex login --with-api-key 以你当前的 Router One Key 重新登录，绝不要与 ChatGPT 登录同用。codex login status 只报告 Codex 自己的 OpenAI 登录状态，不读取 ROUTER_ONE_API_KEY，所以 Router One provider 能正常用时它也可能显示 Not logged in；codex logout 清除保存的登录信息。

### 在中国大陆能用这套配置吗？

能。网关在中国大陆可直连、无需 VPN，配置和在其他地区完全一样：同一份 config.toml，同一个 ROUTER_ONE_API_KEY 变量。遇到 401 也一样，先看 url，再查 Key。

## 相关页面

- Codex 429 报错（exceeded retry limit）：https://router.one/zh/codex-exceeded-retry-limit
- Codex 模型满载报错：https://router.one/zh/codex-model-at-capacity
- Codex 模型不支持报错排查：https://router.one/zh/codex-model-not-supported
- Codex 用量上限报错：https://router.one/zh/codex-usage-limit
- Codex Responses API：https://router.one/zh/codex-responses-api
- Codex CLI 中国大陆接入：https://router.one/zh/codex-china
- 错误码速查：https://router.one/zh/llm-api-error-codes
- Claude Code 401 / 403 排查：https://router.one/zh/claude-code-403
- CLI 配置指南：https://router.one/zh/docs/guides/cli-setup
- 排障问答：https://router.one/zh/docs/guides/faq
- GET /v1/models 接口参考：https://router.one/zh/docs/models/listModels
- Codex 认证文档（英文）：https://learn.chatgpt.com/docs/auth
- 本页规范地址：https://router.one/zh/codex-401-unauthorized
- 模型与每模型 token 价格：https://router.one/zh/models （markdown：https://router.one/zh/models.md ）
- 定价：https://router.one/zh/pricing
- API 文档（markdown）：https://router.one/zh/docs.md
- 公司事实（markdown）：https://router.one/zh/facts/company.md
