# Codex 多模型本地路由方案 · 配置说明（抄作业版）

> 一套让 **Codex 客户端**同时用上「OpenAI 官方 OAuth 模型 + 第三方 API + 本地推理机」的配置方案。
> 全部模型统一走一个本地路由（`127.0.0.1:9999`），Codex 侧只需认一个 provider。
> 本文已脱敏：内网 IP、Key、家目录路径均以占位符表示。

---

## 一、这套方案解决了什么

Codex 默认只能接一个 `model_provider`。想同时用：

- **OpenAI 官方模型**（GPT-5.6 系列 / GPT-6 Astra，走 ChatGPT Plus 订阅额度，不花 API 钱）
- **第三方 API**（DeepSeek、OpenRouter、腾讯混元…）
- **本地推理机**（局域网 SGLang / llama.cpp 跑的 Qwen）

……原生做不到。方案是：**写一个本地 HTTP 路由代理**，Codex 只连它，它按请求里的 `model` 字段分流到不同上游。

```
                     ┌──────────────────────────────┐
   Codex 客户端  ───▶│  本地路由 model-router.py      │
 (model_provider      │  127.0.0.1:9999               │
  = "router")         │  按 model 字段分流             │
                     └───────┬──────────┬────────────┘
                             │          │
        ┌────────────────────┼──────────┼────────────────────┐
        ▼                    ▼          ▼                    ▼
  chatgpt.com          api.deepseek   openrouter       局域网推理机
  (ChatGPT OAuth)      .com           .ai              (192.168.x.x)
  GPT-5.6 / GPT-6      DeepSeek V4    Ox Alpha         Qwen3.8 本地
```

---

## 二、用到的文件清单

| 文件 | 作用 |
|---|---|
| `~/.codex/config.toml` | Codex 主配置：指定默认模型、provider、模型目录路径 |
| `~/.codex/models.json` | **自定义模型目录**（选择器里显示哪些模型、各自能力） |
| `~/.codex/bin/model-router.py` | **本地路由代理**（核心，约 430 行 Python，零依赖） |
| `~/Library/LaunchAgents/com.terry.model-router.plist` | launchd 守护，开机自启 + 挂掉自动重启 |
| `~/.codex/bin/codex-switch.sh` | 一键在「官方直连」与「本地路由」两套配置间切换 |
| `~/.codex/secrets/*` | 各上游的 API Key（独立文件，**不写进配置**） |

---

## 三、config.toml 关键配置

```toml
model = "gpt-5.6-luna"           # 默认模型
model_provider = "router"        # 走本地路由
model_reasoning_effort = "low"
model_catalog_json = "~/.codex/models.json"   # 自定义模型目录
sandbox_mode = "danger-full-access"
service_tier = "default"

[model_providers.router]
name = "Router (GPT + DeepSeek + Local Qwen)"
base_url = "http://127.0.0.1:9999/v1"
wire_api = "responses"           # Codex 用 Responses API
requires_openai_auth = true      # 把 ChatGPT OAuth 凭证透传给路由

[model_providers.deepseek]       # （可选）DeepSeek 直连备份通道
name = "DeepSeek"
base_url = "https://api.deepseek.com/v1"
wire_api = "responses"

[model_providers.deepseek.auth]
command = "$HOME/.codex/bin/deepseek-token"   # 动态取 token 的命令
```

要点：
- **`wire_api = "responses"`** —— Codex 新版走 OpenAI Responses API，路由必须按这个协议转发。
- **`requires_openai_auth = true`** —— 让 Codex 把 ChatGPT 登录凭证（Authorization / ChatGPT-Account-ID）带给路由，路由再原样转发给 `chatgpt.com`，这样**官方模型用订阅额度而非 API Key**。
- **`model_catalog_json`** —— 指向自定义 `models.json`，决定选择器里出现哪些模型。

---

## 四、models.json（自定义模型目录）

结构是 `{"models": [ {...}, {...} ]}`，每个模型一个对象。按来源分组（推荐）：**OpenAI 官方 → OpenRouter → 本地 → 其他(DeepSeek/腾讯)**。

关键字段（不同 Codex 版本要求不同，**缺字段会导致整个目录加载失败**）：

```json
{
  "slug": "gpt-5.6-luna",              // 模型 ID（发给上游的 model 值）
  "display_name": "GPT-5.6-Luna",
  "description": "Fast and affordable agentic coding model.",
  "default_reasoning_level": "medium",
  "supported_reasoning_levels": [
    {"effort": "low",    "description": "..."},
    {"effort": "medium", "description": "..."},
    {"effort": "high",   "description": "..."},
    {"effort": "xhigh",  "description": "..."}
  ],
  "shell_type": "shell_command",        // ⚠️ 必填，缺了 Codex 报 "missing field shell_type"
  "visibility": "list",                 // list=显示在选择器, hide=隐藏
  "supported_in_api": true,
  "priority": 3,
  "additional_speed_tiers": ["fast"],
  "service_tiers": [{"id":"priority","name":"Fast","description":"..."}],
  "availability_nux": null,
  "upgrade": null,
  "apply_patch_tool_type": "freeform",
  "web_search_tool_type": "text_and_image",
  "truncation_policy": {"mode":"tokens","limit":10000},
  "supports_parallel_tool_calls": true,
  "supports_image_detail_original": true,
  "context_window": 272000,
  "max_context_window": 272000,
  "comp_hash": "3000",
  "effective_context_window_percent": 95,
  "experimental_supported_tools": [],
  "input_modalities": ["text","image"],
  "supports_search_tool": true,
  "use_responses_lite": true,
  "tool_mode": "code_mode_only",
  "multi_agent_version": "v1"
}
```

### 常见坑（务必注意）

1. **`shell_type` 必填**。某些 Codex 版本（0.144.5~0.148.0）还会要求 `supports_parallel_tool_calls`，缺任一字段 → 整个 `models.json` 被拒载，报：
   ```
   failed to parse model_catalog_json as JSON: missing field `shell_type` at line N
   ```
2. **官方模型的完整字段可以「克隆」**：从 Codex 官方缓存 `~/.codex/models_cache.json` 里拷贝同类模型（如 `gpt-5.6-luna`）的字段，再改 `slug`/`display_name`/上下文长度即可，避免手写漏字段。
3. **`use_responses_lite` / `tool_mode` / `multi_agent_version`**：不同官方模型取值不同，建议照官方缓存抄。

---

## 五、model-router.py（核心路由）

一个**零依赖**的 Python HTTP 服务（只用标准库），监听 `127.0.0.1:9999`。

### 5.1 路由表

```python
CHATGPT_BASE = 'https://chatgpt.com/backend-api/codex'

UPSTREAM = {
    # —— OpenAI 官方（走 ChatGPT OAuth，用订阅额度）——
    'gpt-5.6-sol':   CHATGPT_BASE,
    'gpt-5.6-terra': CHATGPT_BASE,
    'gpt-5.6-luna':  CHATGPT_BASE,
    'gpt-5.5':       CHATGPT_BASE,
    'gpt-5.4-mini':  CHATGPT_BASE,
    'gpt-5.2':       CHATGPT_BASE,
    'gpt-6-astra':   CHATGPT_BASE,      # 新的官方模型照这个加
    # —— 图像生成（内置 image_gen 用）——
    'gpt-image-2':   CHATGPT_BASE,
    'gpt-image-1.5': CHATGPT_BASE,
    # —— 第三方 ——
    'deepseek-v4-flash': 'https://api.deepseek.com',
    'deepseek-v4-pro':   'https://api.deepseek.com',
    'stealth/ox-alpha':  'https://openrouter.ai/api',
    'hy4-preview':       'https://tokenhub.tencentmaas.com',
    # —— 本地推理机 ——
    'qwen3.8-27b-q4-nvidia':        'http://192.168.x.x:8081',
    'qwen3.8-27b-q4':               'http://192.168.x.x:8080',
    'qwen3.8-27b-w4a16-awq-dflash2':'http://192.168.x.x:8199',
    'qwen3.8-27b-fp8':              'http://192.168.x.x:30000',
}
```

### 5.2 按主机分别鉴权

路由从**独立文件**读各上游的 Key（不写死在配置里），再给对应主机加 `Authorization` 头：

```python
DEEPSEEK_KEY_FILE   = os.path.expanduser('~/.codex/secrets/deepseek_api_key')
OPENROUTER_KEY_FILE = os.path.expanduser('~/.codex/secrets/openrouter_api_key')
QWEN_KEY_FILE       = os.path.expanduser('~/.codex/secrets/qwen_api_key')
TENCENT_KEY_FILE    = os.path.expanduser('~/.codex/secrets/tencent_tokenhub_api_key')
```

- **chatgpt.com**：转发 Codex 传来的 `Authorization` / `ChatGPT-Account-ID`（官方 OAuth 透传）
- **api.deepseek.com / openrouter.ai / dashscope / tokenhub**：读本地 Key 文件后加 `Bearer`
- **局域网推理机**：加 `Bearer dummy`（本地服务不校验），且**不转发**任何 OAuth 凭证

### 5.3 代理（可选）

```python
PROXY_BY_HOST = {
    'api.deepseek.com': 'http://127.0.0.1:1080',   # 走本地代理
    'chatgpt.com':      'http://127.0.0.1:1080',
    'openrouter.ai':    'http://127.0.0.1:1080',
    # 局域网推理机不在表里 → 直连
}
```

### 5.4 路径改写（关键）

Codex 请求 `/v1/responses`，而 ChatGPT 后端真实端点是 `<base>/responses`（没有 `/v1`）：

```python
if base == CHATGPT_BASE and path.startswith('/v1/'):
    path = path[len('/v1'):]      # /v1/responses → /responses
```

图像生成同理：Codex 发 `/v1/images/generations` → 转发为
`https://chatgpt.com/backend-api/codex/images/generations`。

### 5.5 流式透传

上游是 SSE 流式响应，路由用 **chunked 逐块透传**（`resp.read1(8192)` → `wfile.write`），保证 Codex 边收边渲染，不缓冲整包。

### 5.6 它顺便解决的兼容问题

- **本地 Qwen 的 chat template 要求 system/developer 消息在最前** → 路由会把它们前移，避免 500。
- **OpenRouter 对 Responses 请求校验严格**（不认 `custom_tool_call` 等类型、`null` 字段）→ 对 openrouter 主机做 body 清洗。
- **腾讯 TokenHub 偶发 307/429/503** → 对这些码指数退避重试。

---

## 六、image_gen（内置图片生成）怎么打通

这是最容易踩坑的地方。

**现象**：Codex 内置 `image_gen` 工具生成图片时报「模型路由错误 / unknown model 'gpt-image-2'」。

**原因**：内置 image_gen 工具底层调用 **OpenAI Images API**（`POST /v1/images/generations`，模型 `gpt-image-2`），而请求被发到了本地路由。路由的 `UPSTREAM` 里没有 `gpt-image-2` → 直接 400。

**修复**（三处）：

1. 路由表加图像模型（见 5.1）
2. 路径改写让 `/v1/images/generations` → `<codex-base>/images/generations`
3. **转发图片端点必需的头**：`x-codex-imagegen-request-id` 和 `User-Agent`（缺了会 403）

修好后，内置 image_gen 就走官方 `chatgpt.com`、用**订阅额度**生成，不需要单独的 `OPENAI_API_KEY`。

> 验证方式：用 ChatGPT OAuth 凭证向本地路由 `POST /v1/images/generations`，
> body `{"model":"gpt-image-2","prompt":"...","size":"1536x1024"}`，
> 返回 `data[0].b64_json`（base64 PNG）即成功。

---

## 七、launchd 守护（开机自启 + 崩溃重启）

`~/Library/LaunchAgents/com.terry.model-router.plist`：

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key><string>com.terry.model-router</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/python3</string>
        <string>$HOME/.codex/bin/model-router.py</string>
    </array>
    <key>RunAtLoad</key><true/>
    <key>KeepAlive</key><true/>
    <key>StandardOutPath</key><string>$HOME/.codex/bin/model-router.log</string>
    <key>StandardErrorPath</key><string>$HOME/.codex/bin/model-router.err.log</string>
</dict>
</plist>
```

加载 / 重启：

```bash
launchctl load  ~/Library/LaunchAgents/com.terry.model-router.plist   # 首次
launchctl kickstart -k gui/$(id -u)/com.terry.model-router            # 改代码后重启
```

`KeepAlive=true` 表示进程挂掉会自动拉起，**改完 router 代码必须 kickstart 才生效**。

---

## 八、复刻步骤（从零）

1. 装好 Codex 桌面版 / CLI，并用 ChatGPT 账号登录一次（生成 `~/.codex/auth.json`）。
2. 建立目录与密钥文件：
   ```bash
   mkdir -p ~/.codex/bin ~/.codex/secrets
   echo '你的DeepSeekKey'  > ~/.codex/secrets/deepseek_api_key
   echo '你的OpenRouterKey'> ~/.codex/secrets/openrouter_api_key
   # …按需添加其它上游 Key
   ```
3. 放好 `bin/model-router.py`（按第五节改好路由表 / 密钥路径 / 代理），本地起服务验证：
   ```bash
   python3 ~/.codex/bin/model-router.py &      # 手动先跑，看是否报错
   curl -s http://127.0.0.1:9999/v1/models | head
   ```
4. 放好 `models.json`（第四节结构），确认 JSON 合法：
   ```bash
   jq . ~/.codex/models.json > /dev/null && echo OK
   ```
5. 改 `config.toml`：`model_provider="router"` + `[model_providers.router] base_url="http://127.0.0.1:9999/v1"` + `model_catalog_json`。
6. 装 launchd plist（第七节），`launchctl load`。
7. **重启 Codex 客户端**（模型目录是启动时读的），选择器里即可看到全部模型。
8. 验证图片生成：在 Codex 里让它生成一张图，或直接 curl `/v1/images/generations`（第六节）。

---

## 九、踩坑清单（都踩过）

| 现象 | 原因 | 解决 |
|---|---|---|
| `failed to parse model_catalog_json: missing field 'shell_type'` | `models.json` 某条目缺必填字段 | 补齐 `shell_type` 等；建议从官方 `models_cache.json` 克隆 |
| 选择器里没有新加的官方模型 | 只加了自定义目录，没加路由 | `models.json` **和** router 的 `UPSTREAM` 两处都要加 |
| 官方模型报 `Unauthorized` | 没透传 OAuth 头 | `requires_openai_auth=true`，路由转发 `Authorization`/`ChatGPT-Account-ID` |
| 官方模型报 `Store must be set to false` / `Stream must be set to true` | 请求参数不符官方要求 | 由 Codex 客户端正常发请求即可，勿手工构造 |
| `unknown model 'gpt-image-2'` | 路由表缺图像模型 | 第六节三处修复 |
| 图片端点 403 | 缺 `x-codex-imagegen-request-id` / `User-Agent` 头 | 路由补转发这两个头 |
| 改了 router 不生效 | launchd 里进程还在跑旧代码 | `launchctl kickstart -k` 重启 |
| 本地 Qwen 报 500 | system 消息位置不对 | 路由前移 system/developer 消息 |
| OpenRouter 报参数错误 | Responses 请求里有它不认的类型 | 对 openrouter 主机做 body 清洗 |
| 用第三方中转时官方插件/远程操作消失 | 切供应商覆盖了 `auth.json` | 让官方登录态留在 `auth.json`，第三方信息只写 `config.toml` |

---

## 十、两条注意事项

1. **密钥管理**：所有 Key 放 `~/.codex/secrets/` 独立文件，**不要**写进 `config.toml` / `models.json` / router 源码；分享配置前务必脱敏（IP、Key、家目录）。
2. **账号风险**：把官方 OAuth 凭证转发给自建代理属于非官方用法，官方可能调整校验策略（例如要求特定 `client_version` 头）。第三方中转同理，注意别把官方登录态交给不受信任的中转。

---

*本文档由本机实际运行的配置整理，已脱敏。照着搭即可复现「一个路由吃下所有模型」的效果。*
