# Codex 怎么通过 CLIProxyAPI 反代：Docker 部署、OAuth 登录和客户端配置

> Canonical: https://vxi.me/posts/cliproxyapi-codex-docker-proxy/
> Locale: zh-cn
> Published: 2026-03-30
> Updated: 2026-10-14
> Markdown: https://vxi.me/posts/cliproxyapi-codex-docker-proxy.md

**配套开源仓库**：本文的一键部署版本，一条命令完成 Docker 部署、key 生成与配置渲染，另附客户端配置生成脚本与 Nginx 示例：

<a class="gh-repo-pill" href="https://github.com/kuangyongjie/cliproxyapi-codex-deploy" target="_blank" rel="noopener"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg><span>kuangyongjie/cliproxyapi-codex-deploy</span></a>

```bash
git clone https://github.com/kuangyongjie/cliproxyapi-codex-deploy.git
cd cliproxyapi-codex-deploy
./scripts/setup.sh
```

如果你现在已经把 Codex 用顺手了，迟早会碰到一个挺现实的问题：我到底要不要继续老老实实用官方直登，还是干脆自己加一层代理，把 Codex 放进我自己的 AI 开发环境里。

这件事一开始看起来像“技术洁癖”，但真用起来就不是了。你如果只有一台电脑、一个账号，而且平时主要就是自己本地开工，那官方直接登录确实最轻，没什么好争的。装完能用，少一层服务，少一层维护，也少一层排障路径。这种场景下，为了“可控”强行多架一个代理，很多时候只是给自己加活。

但一旦你开始有别的想法，情况就变了。比如你不想每台机器都重新登一遍；比如你希望 Codex、Claude Code、Gemini CLI 这些东西后面都能走同一个入口；再比如你想把 OAuth 登录状态、访问控制、兼容接口、甚至后面的多账号轮询都收进一台你自己能控的服务里。到了这个阶段，像 [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) 这种项目就很有意思，因为它不是单纯“帮你转发请求”，而是在做一层可控的接入面。

我当时真开始认真看这条路，也不是因为“想折腾得更高级”，而是因为发现官方直登很适合单机开工，但一旦我想把 Codex 接进自己平时那套 VPS、自部署工具和脚本环境里，很多东西就开始变散了。登录状态在本机，别的项目各配各的入口，后面再加一个调用端，又是一轮重复配置。核心问题其实很简单：如果你想把 Codex 通过 CLIProxyAPI 反代出去，用 Docker 跑在一台 Linux 服务器上，跑通 OpenAI OAuth，然后回到本地 Codex App 改配置接过去，这条链路到底应该怎么定，哪里最容易翻车。

## 代理方案到底好在哪

我先把结论说清楚：如果你只图最快开工，官方直登还是最低摩擦方案；但如果你要的是“以后我的 Codex 不只是一个 app，而是我整套开发环境的一部分”，那代理方案就很值得。它最大的价值，不是炫，而是把“某台机器上的登录状态”变成“我自己掌控的一层基础设施”。

而且这个“基础设施”一旦搭出来，受益的通常不只是 Codex 自己。你很快就会发现，真正方便的地方在于你终于有了一个可以复用的 OpenAI-compatible 入口。以后你自己部署的其他项目，只要支持自定义 `base_url` 或 OpenAI 兼容接口，理论上都能直接接过来。像龙虾这类项目，其实就很适合吃这种统一入口。它本身作为前端或调用层，并不值得你再单独维护一套上游登录状态；让它和 Codex 一样都挂在这层代理后面，实际会省心很多。别的自部署前端、Agent 面板、脚本服务也一样，它们只要认你这层代理发出去的 `api-key`，就能共用同一个入口。

这里最容易被低估的一点，是“共享”的其实不是某个写死在项目里的上游密钥，而是代理层保存下来的那份 OAuth / token 状态。以前你可能要在 Codex、脚本、面板、另一个容器里各自处理登录，机器一换、容器一重建，又得重新补一遍。现在变成上游状态集中保存在 CLIProxyAPI 这一层，下游项目只认代理地址和代理 key。这个差别看起来像少填几项配置，实际上是把整套维护方式改了。你后来再接一个龙虾项目，或者再补一个只会读 OpenAI-compatible 接口的内部工具，脑子里想的就不是“这次又要怎么登录一次”，而是“给它发哪把 key、挂到哪个入口”。

这件事对独立开发者特别有价值，因为它把“每个项目各配一遍”变成了“上游只配一次，下游按项目分发自己的访问 key”。换句话说，真正敏感、真正麻烦的那层 OAuth 状态和 token，不再散落在每台机器、每个容器、每个项目配置里，而是被你集中收口到了代理服务这一层。以后你换机器、重装客户端、补一个新项目，改的通常只是下游访问方式，不是再从头重做上游授权。

而且这里还有个很实际的好处：代理 key 的策略可以按你的阶段来。你现在项目不多，完全可以先让几个自己控制的内部项目共用一把代理 key，先把链路打通，省掉重复配置。等后面 Codex、龙虾项目、聊天前端、自动化脚本这些东西越挂越多，再按项目拆 key、做限流、做日志区分。也就是说，这层代理既适合你现在先图省事，也给你后面规范化留了位置。

如果你后面还会继续接别的工具，这种好处会越来越明显。比如今天你先让 Codex 走这层代理，明天你再把 Claude Code、Gemini CLI，或者你自己部署的别的调用端也放进来，外部看到的依旧可以是一个统一入口。甚至从运维视角看，这也很省脑子：限流、访问控制、日志、上游切换、模型映射，都开始有了一个固定落点，而不是分散在每个项目自己的环境变量里。

## 先把链路拆成三块

我最后理解这条链路时，是把它硬拆成了三块。第一块是服务器上跑起来的 `CLIProxyAPI` 服务本身。第二块是 Codex 的 OAuth 登录状态，要真正写进这个服务挂载出来的认证目录里。第三块才是本地 Codex 客户端配置，也就是 `~/.codex/config.toml` 和 `~/.codex/auth.json` 这两个文件。只要你脑子里这三层不混，后面其实就顺了；反过来，很多人第一次接不通，往往就是把“代理登录”和“客户端配置”当成了一件事。

`CLIProxyAPI` 官方 README 现在已经明确写了它支持 Codex via OAuth，也给了 Docker、Docker Compose、Codex 客户端配置文档。光看官方文档，其实路线并不复杂。真正麻烦的是第一次做时，你很容易误判每个文件到底是给谁用的。`config.yaml` 是代理服务的，`auth/` 是代理登录状态的，`~/.codex/config.toml` 是本地 Codex App 的，这三份东西别搅在一起。

## 服务器端：Docker 部署与 OAuth 登录

如果是我自己搭，我会先在服务器上收一个很小的目录，不去搞太花的结构。差不多这样就够了：

```text
/opt/cliproxyapi/
├── docker-compose.yml
├── config.yaml
└── auth/
```

这里的 `auth/` 很关键，因为官方 `auth-dir` 默认就是 `~/.cli-proxy-api`。你用 Docker 时，本质上就是把容器里的这个目录映射出来，让 OAuth 登录后留下来的状态别随着容器消失。很多人第一次能登录成功，但第二次容器一重建就发现状态没了，问题基本都出在这里。

`config.yaml` 我不建议一上来写得很大。先让它稳稳跑起来再说。官方 `config.example.yaml` 里，最值得先关心的其实就这么几项：

```yaml
host: ""
port: 8317
auth-dir: "~/.cli-proxy-api"

api-keys:
  - "<proxy-api-key>"

remote-management:
  allow-remote: false
  secret-key: ""
```

这里有两个点最好一开始就想明白。一个是 `api-keys` 不是 OpenAI 的 key，它是你下游客户端访问这层代理时要带的访问令牌。这个设计反而正是代理方案的价值之一，因为你可以给不同项目发不同的下游 key。比如 Codex App 用一个，龙虾这类自部署项目再用一个，后面想吊销、轮换或者拆分权限时，动的是代理层，不是每个项目都回头重配上游。另一个是 `remote-management.allow-remote`，如果你现在只是拿它服务 Codex，没有明确的远程管理需求，就先关着。这个项目默认也是关的，我觉得这个默认值挺对，因为很多人刚搭起来就喜欢顺手把控制面也一起暴露，后面反而给自己留坑。

Docker Compose 这块，我会先定成最保守也最容易解释的版本。核心目的只有两个：把 API 端口映出来，把认证目录和配置文件挂进去。

```yaml
services:
  cli-proxy-api:
    image: eceasy/cli-proxy-api:latest
    container_name: cli-proxy-api
    restart: unless-stopped
    ports:
      - "127.0.0.1:8317:8317"
      - "127.0.0.1:1455:1455"
    volumes:
      - ./config.yaml:/CLIProxyAPI/config.yaml
      - ./auth:/root/.cli-proxy-api
```

这两个端口我都绑在了 `127.0.0.1` 上，是有意的。API 端口只需要被本机的 Nginx 访问到（后文 Nginx 本来就是反代 `http://127.0.0.1:8317`），而 OAuth 回调监听面曾经有过对外发布的历史风险（上游 issue #3463）。默认把这两个端口暴露到公网，等于把一个临时的登录面变成了永久的攻击面，所以不要把它当成安全的默认值。如果你的网络拓扑确实需要更广的访问，再单独声明限制条件。

再多说一句镜像 tag：`:latest` 是浮动 tag。本文的 v8 字段对照核对的是上游仓库 2026-10-14 的 main 分支，并没有用这个镜像实际跑过。部署前先确认你拉到的 tag 还是这个布局，或者直接锁定一个自己验过的 digest。

这里把 `1455` 也映射出来，不是因为你的业务流量都走它，而是因为官方文档明确写了 Codex 的本地 OAuth 回调用这个端口。这个细节很容易被忽略，因为很多人眼里只盯着 API 端口 `8317`，结果服务明明跑着，`--codex-login` 也执行了，最后授权跳不回来，或者浏览器里转完发现代理这边没拿到状态。不是 CLIProxyAPI 坏了，是你把 OAuth 这一段链路想得太简单了。

等 Compose 起好以后，先别急着改本地 Codex。这个顺序真的很重要，应该先验证代理自己能不能拿到 Codex 的 OAuth 状态。官方给的命令是：

```bash
docker compose exec cli-proxy-api /CLIProxyAPI/CLIProxyAPI -no-browser --codex-login
```

我自己很喜欢这个 `-no-browser`，尤其是在远程 Linux 服务器上。它不会尝试在容器里开浏览器，而是把登录 URL 直接吐出来，你复制到本地浏览器里做授权就行。这一步你要理解成“给代理服务登录”，不是“给我的 Codex App 登录”。也就是说，真正被授权的是 CLIProxyAPI 这一层，授权成功后，状态应该会落到你挂载出来的 `auth/` 目录里。

关于这个回调，有一件事在远程机器上特别容易搞错。浏览器跑在你的本地电脑上，所以 `http://localhost:1455/auth/callback` 解析到的是你的本地电脑，而不是 VPS——不管容器的端口映射怎么写。对于远程 Linux/VPS 场景，真正能闭环的顺序是：

1. 先在**本地**（不是服务器上）建好隧道：
   ```bash
   ssh -N -L 1455:127.0.0.1:1455 <user>@<vps>
   ```
2. 再执行上面的登录命令。CLI 会输出 OAuth URL（`-no-browser` 下它甚至会自己打印 SSH 隧道指引）。
3. 在本地浏览器里打开这个 URL 完成授权，`localhost:1455` 的回调就会经由隧道回到容器。

如果跳过隧道，CLI 会停在 "Waiting for Codex authentication callback..."，约 15 秒后提示 `Paste the Codex callback URL (or press Enter to keep waiting):`，这时直接把浏览器地址栏里的完整回调 URL 粘贴进去就行——这是 CLI 自带的手动路径，不是野路子。

如果这一段没跑通，后面你本地怎么改 `config.toml` 都没意义。因为客户端只是换了入口，真正能不能代表你去拿 Codex 结果，还是得看代理自己有没有那份 OAuth 状态。所以第一次做时，我会先看两个东西：一个是 `docker compose logs -f` 的实时输出，另一个是 `auth/` 目录里有没有新文件写进去。前者告诉你登录流程卡在哪，后者告诉你这次授权到底是不是落盘了。

## config.yaml 字段速查（v8）

前面示例里的 `config.yaml` 用的是旧版顶层字段写法（`host`、`port`、`auth-dir`、`api-keys`、`remote-management`），照样能用。但上游已经切到 v8 布局（`config-version: 8`），所有配置都收进具名小节里。如果你对照的是上游仓库现在的 `config.example.yaml`，看到的就是新写法。下面是 Codex 代理场景实际用到的字段对照，每个字段名都已对照上游 main 分支（2026-10-14 实取）核对过。Plus fork 可能有分化，套用前先确认它们锁定的镜像版本和 `config.example.yaml`。

| 字段（v8） | 旧写法 | 说明 |
|---|---|---|
| `config-version: 8` | — | 声明 v8 布局。新旧写法同时出现时，v8 优先。 |
| `server.host` | `host` | 绑定网卡。空字符串表示监听所有网卡。 |
| `server.port` | `port` | 客户端调用的 API 端口，本文用 `8317`。 |
| `oauth.auth-dir` | `auth-dir` | Codex OAuth 状态存放位置，必须和 `auth/` 的挂载对应上，否则重启就丢登录。 |
| `access.api-keys` | `api-keys` | 发给下游客户端的 key，不是上游 OpenAI 凭据。 |
| `management.allow-remote` | `remote-management.allow-remote` | 没有明确的远程管理需求就保持 `false`。 |
| `management.secret-key` | `remote-management.secret-key` | 管理 API 的密钥，留空则管理 API 直接禁用。 |
| `client.codex.optimize-multi-agent-v2` | — | 可选，Codex Desktop / codex-tui 多 agent 场景优化，默认 `false`。 |

按本文配置写成的 v8 风格最小配置：

```yaml
config-version: 8

server:
  host: ""
  port: 8317

oauth:
  auth-dir: "~/.cli-proxy-api"

access:
  api-keys:
    - "<proxy-api-key>"

management:
  allow-remote: false
  secret-key: ""
```

两点值得单独说。第一，`access.api-keys` 和旧写法的 `api-keys` 是同一份名单：发给下游的 key，永远不是上游密钥。第二，容器里 `oauth.auth-dir` 默认解析到 `/root/.cli-proxy-api`，这正是 compose 里把 `./auth` 挂到那里的原因。两边改了一边没改，“昨天还好好的今天重启就掉登录”的坑就回来了。

## Docker Compose 独立路径

文首的配套仓库把下面这些做成了一键脚本。如果你想每个文件都自己掌控、看清楚每一步在跑什么，下面是同一套配置的独立 compose 版本，不依赖任何辅助脚本。

目录还是前面那个小结构：

```text
/opt/cliproxyapi/
├── docker-compose.yml
├── config.yaml
└── auth/
```

compose 文件就用“服务器端”上面那一份——同样的 `127.0.0.1` 绑定、同样的挂载，按上游仓库自己的 `docker-compose.yml` 裁到本文需要的样子。这里不再重复贴第二份，因为正确配置应该只有一份源头，照着上面复制即可。`./auth` 还是要落到 `oauth.auth-dir` 指的地方，否则每次重建容器 OAuth 状态就丢。

启动并观察：

```bash
docker compose config
docker compose up -d
docker compose logs -f
```

第一条值得养成习惯：`docker compose config` 只渲染生效配置、不启动任何容器，缩进写错、挂载路径写错这类问题在变成运行时谜题之前就能发现。

然后按上一节同样的方式登录：

```bash
docker compose exec cli-proxy-api /CLIProxyAPI/CLIProxyAPI -no-browser --codex-login
```

说句实话：上面 compose 文件的每个字段都对照过上游仓库的 `docker-compose.yml` 和本文配置，但这次更新我没有在真机上实际执行过这份文件。如果你的环境不一样（比如 rootless Docker、非默认数据目录），卷路径是最先要核对的地方。

## 本地端：Codex 客户端与 Nginx 配置

到这里很多人会有个误区：既然代理已经做完 OpenAI OAuth 了，那本地 Codex 不是就可以什么都不用配了吗？实际上不是。按 CLIProxyAPI 官方 Codex 客户端文档，本地还是要改两个文件：`~/.codex/config.toml` 和 `~/.codex/auth.json`。只是这里的逻辑跟你直连 OpenAI 时已经完全不一样了。

你可以把它理解成两层 key。上游那层，是代理自己持有的 OAuth/token 状态，它代表的是“CLIProxyAPI 这层能不能替你去拿 Codex 能力”；下游这层，则是你发给客户端或别的项目的访问 key，它代表的是“谁可以调用我的代理”。这个拆分很值钱，因为以后你再接一个新项目，通常不需要再碰上游授权，只需要给它一把新的下游 key，再把 `base_url` 指过来就行。

如果你的代理跑在远程服务器，我会直接把 `config.toml` 理解成“把 Codex 的 provider 指到我自己的域名上”。像这样：

```toml
model_provider = "cliproxyapi"
model = "gpt-5-codex"
model_reasoning_effort = "high"

[model_providers.cliproxyapi]
name = "cliproxyapi"
base_url = "https://codex-proxy.example.com/v1"
wire_api = "responses"
```

这里最容易写错的，其实反而不是模型名，而是两处小地方。第一处是 `base_url`，很多人明明服务跑在远程域名上了，结果还沿用官方示例里的 `http://127.0.0.1:8317/v1`，最后本地 Codex 当然还是在找自己机器上的代理。第二处是 `wire_api = "responses"`，这个不能漏。CLIProxyAPI 官方给 Codex 的兼容方式就是走 `responses`，少了这一句，客户端和代理经常会出现你看不懂的行为偏差。

至于 `auth.json`，它反而比很多人以为的简单得多。官方文档给的例子就是：

```json
{
  "OPENAI_API_KEY": "sk-dummy"
}
```

第一次看到这里，很多人会皱眉头：我都走代理了，怎么还要填 `OPENAI_API_KEY`？其实你要把它理解成客户端的一个占位要求，而不是“这里真要放一个能用的 OpenAI key”。真正起作用的是上面的 `config.toml` provider 配置，这里的 `sk-dummy` 只是让 Codex 本地侧的配置结构完整。也正因为这样，我更不建议在这里手痒放进真的上游 key，你本来都已经把接入控制收口到代理层了，没必要又把敏感信息往客户端这一层塞回去。

如果你还想把 `8317` 收到一个正式域名后面，比如 `codex-proxy.example.com`，那 Nginx 也别搞太复杂。最重要的是保住 `/v1/` 这个路径，不要为了看起来整齐把它改来改去。一个够用的写法就是：

```nginx
server {
  listen 443 ssl http2;
  server_name codex-proxy.example.com;

  location /v1/ {
    proxy_pass http://127.0.0.1:8317/v1/;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}
```

只要这层入口稳定下来，它的意义就不只是给 Codex 用。很多自部署项目本身吃的就是 OpenAI-compatible 协议，它们往往也支持改 `OPENAI_BASE_URL`、`OPENAI_API_BASE` 或类似变量。到那一步，你其实已经不再是在“给 Codex 做个专用代理”，而是在给自己手上的一批项目搭一个统一 AI 网关。你今天先接 Codex，明天再接龙虾项目或别的前端，配置思路会非常像：

```bash
OPENAI_BASE_URL=https://codex-proxy.example.com/v1
OPENAI_API_KEY=<proxy-api-key-for-this-project>
```

真正舒服的地方就在这里。上游 OAuth、模型接入、账号轮询这些重东西，集中在 CLIProxyAPI 层；下游每个项目只管认一个稳定入口和一把自己的访问 key。以后你想换上游、加账号、做容灾、甚至把某个项目单独禁掉，都不用把所有客户端和所有自部署服务重新翻一遍。

如果你手上已经有别的自部署项目，这种统一入口的价值会更直接。比如你拿 Codex 做代码工作流，拿龙虾这种前端项目做日常对话或模型试验，再加几个内部脚本去跑自动化任务。以前这三类东西很容易各自带一套配置，哪个项目里到底塞了什么 key，过两周自己都记不清。现在它们都挂到同一个代理后面，真正长期保存的只有代理层那份上游状态，你要管理的是“哪个项目能不能用、用哪把代理 key、走哪个域名入口”，而不是到处找散落的 token。

## 最常见的翻车点

如果你问我实际最常见的翻车点是什么，我会说不是 Docker，也不是 Compose 本身，而是下面这些很细的小地方：

- 代理服务已经跑起来了，但 `auth/` 根本没拿到 Codex OAuth 状态
- 本地 `config.toml` 改了，可 `base_url` 还写着 localhost
- Nginx 只反代了域名，没有把 `/v1/` 路径保留下来
- 忘了 `wire_api = "responses"`
- 以为 `auth.json` 要填真 OpenAI key，又把本来不需要暴露的东西放回本地

你会发现，这些问题都不是“命令写错”，而是你对这条链路的职责分层有没有想清楚。直接登录当然简单，因为认证、请求、客户端都在一层里；CLIProxyAPI 这条路则是把登录状态、代理服务、反代入口、本地客户端拆开了。拆开以后更灵活，但第一遍接的时候脑子里一定要有拓扑。

所以最后还是回到那个问题：它跟直接登录到底有什么区别，值不值得。我的看法挺明确的。直接登录适合“我自己马上要用”；CLIProxyAPI 更适合“我要把 Codex 变成我整套 AI 开发环境里可控的一部分”。前者低摩擦，后者高自由度。你如果已经开始同时用多种 AI coding 工具，或者有多台机器要保持一致入口，那后者的价值会越来越明显。尤其当你手里不只 Codex 一个调用端，而是还挂着龙虾这类自部署服务时，这个差别会一下子变得很具体：你终于不用每个项目都重新处理一次上游认证，也不用把密钥和 token 分散埋在不同容器和配置文件里。

还有一件事我觉得很多人会后知后觉：既然你已经开始把这类东西做成自己的基础设施，就别把 `config.yaml` 和 `auth/` 当成随时可重来的临时文件。它们其实已经是状态了。真要长期跑，最好顺手把这两块也纳进备份里。不然哪天容器重建、机器迁移、目录误删，你会发现最费时间的从来不是重拉镜像，而是把那些登录状态和细碎配置重新拼回来。

如果你现在就在搭 Codex 工作流，这篇 [新版 Codex 有什么变化：为什么它现在更像一个开发环境](/posts/what-changed-in-the-new-codex/) 可以顺手一起看；如果你准备把这个代理长期挂在 VPS 上，那篇 [Docker 数据卷自动备份到 GitHub：适合 1Panel 的低成本容灾方案](/posts/docker-volume-backup-github-sync/) 也值得补一下，因为像认证目录、配置文件这种东西，真出过一次问题就知道备份不是锦上添花。


## Runbook：CLIProxyAPI 接不上 Codex 时

这条链路出问题时，按职责分层排比“从头重装”更快：

1. **Proxy 服务**：先确认 CLIProxyAPI 容器本身健康，`8317` 的 API 服务在监听。
2. **OAuth 状态**：执行官方 Codex 登录流程后，确认挂载出来的 `auth/` 目录真的产生了状态文件。
3. **客户端入口**：本地 `~/.codex/config.toml` 的 `base_url` 必须指向真实远程域名，而不是继续留在 localhost。
4. **协议**：确认 provider 仍然使用 `wire_api = "responses"`。
5. **反代**：Nginx 要完整保留 `/v1/` 路径，再去看上游返回；不要把路径改写问题误判成模型问题。
6. **下游 key**：`auth.json` 只满足客户端结构；真正的访问控制应该由代理层的 key 管理，不要重新把上游真实凭据散回客户端。

如果你是从官方 localhost 示例迁到远程域名，下面这个 diff 比重新对照整段配置更容易发现错误：

```diff
-base_url = "http://127.0.0.1:8317/v1"
+base_url = "https://codex-proxy.example.com/v1"
```

重新做 OAuth 时，真正要执行的仍然是代理容器里的登录命令：

```console
$ docker compose exec cli-proxy-api /CLIProxyAPI/CLIProxyAPI -no-browser --codex-login
```
