# Codex Proxy 实现记录 ## 项目目标 将 Codex Desktop App(免费)的 API 访问能力提取出来,暴露为标准的 OpenAI `/v1/chat/completions` 兼容接口,使任何支持 OpenAI API 的客户端都能直接调用。 --- ## 关键发现:WHAM API vs Codex Responses API ### 最初的方案(失败) 项目最初使用 **WHAM API**(`/backend-api/wham/tasks`)作为后端。这是 Codex Cloud 模式使用的 API,工作流程为: 1. 创建 cloud environment(需要绑定 GitHub 仓库) 2. 创建 task → 得到 task_id 3. 轮询 task turn 状态直到完成 4. 从 turn 的 output_items 中提取回复 **失败原因**: - 免费账户没有 cloud environment - `listEnvironments` 返回 500 - `worktree_snapshots/upload_url` 返回 404(功能未开启) - 创建的 task 立即失败,返回 `unknown_error` ### 发现真正的 API 通过分析 Codex Desktop 的 CLI 二进制文件(`codex.exe`)中的字符串,发现 CLI 实际使用的是 **Responses API**,而不是 WHAM API。 进一步测试发现正确的端点是: ``` POST https://chatgpt.com/backend-api/codex/responses ``` #### 端点探索过程 | 端点 | 状态 | 说明 | |------|------|------| | `/backend-api/responses` | 404 | 不存在 | | `api.openai.com/v1/responses` | 401 | ChatGPT token 没有 API scope | | **`/backend-api/codex/responses`** | **400 → 200** | **正确端点** | #### 必需字段(逐步试错发现) 1. 第一次请求 → `400: "Instructions are required"` → 需要 `instructions` 字段 2. 加上 instructions → `400: "Store must be set to false"` → 需要 `store: false` 3. 加上 store: false → `400: "Stream must be set to true"` → 需要 `stream: true` 4. 全部加上 → `200 OK` ✓ --- ## API 格式 ### 请求格式 ```json { "model": "gpt-5.1-codex-mini", "instructions": "You are a helpful assistant.", "input": [ { "role": "user", "content": "你好" } ], "stream": true, "store": false, "reasoning": { "effort": "medium" } } ``` **关键约束**: - `stream` 必须为 `true`(不支持非流式) - `store` 必须为 `false` - `instructions` 必填(对应 system message) ### 响应格式(SSE 流) Codex Responses API 返回标准的 OpenAI Responses API SSE 事件: ``` event: response.created data: {"type":"response.created","response":{"id":"resp_xxx","status":"in_progress",...}} event: response.output_text.delta data: {"type":"response.output_text.delta","delta":"你","item_id":"msg_xxx",...} event: response.output_text.delta data: {"type":"response.output_text.delta","delta":"好","item_id":"msg_xxx",...} event: response.output_text.done data: {"type":"response.output_text.done","text":"你好!",...} event: response.completed data: {"type":"response.completed","response":{"id":"resp_xxx","status":"completed","usage":{...},...}} ``` 主要事件类型: - `response.created` — 响应开始 - `response.in_progress` — 处理中 - `response.output_item.added` — 输出项添加(reasoning 或 message) - `response.output_text.delta` — **文本增量(核心内容)** - `response.output_text.done` — 文本完成 - `response.completed` — 响应完成(包含 usage 统计) ### 认证方式 使用 Codex Desktop App 的 ChatGPT OAuth JWT token,需要以下请求头: ``` Authorization: Bearer ChatGPT-Account-Id: originator: Codex Desktop User-Agent: Codex Desktop/260202.0859 (win32; x64) Content-Type: application/json Accept: text/event-stream ``` --- ## 代码实现 ### 新增文件 #### `src/proxy/codex-api.ts` — Codex Responses API 客户端 负责: - 构建请求并发送到 `/backend-api/codex/responses` - 解析 SSE 流,逐个 yield 事件对象 - 错误处理(超时、HTTP 错误等) ```typescript // 核心方法 async createResponse(request: CodexResponsesRequest): Promise async *parseStream(response: Response): AsyncGenerator ``` #### `src/translation/openai-to-codex.ts` — 请求翻译 将 OpenAI Chat Completions 请求格式转换为 Codex Responses API 格式: | OpenAI Chat Completions | Codex Responses API | |------------------------|---------------------| | `messages[role=system]` | `instructions` | | `messages[role=user/assistant]` | `input[]` | | `model` | `model`(经过 resolveModelId 映射) | | `reasoning_effort` | `reasoning.effort` | | `stream` | 固定 `true` | | — | `store: false`(固定) | #### `src/translation/codex-to-openai.ts` — 响应翻译 将 Codex Responses SSE 流转换为 OpenAI Chat Completions 格式: **流式模式** (`streamCodexToOpenAI`): ``` Codex: response.output_text.delta {"delta":"你"} ↓ OpenAI: data: {"choices":[{"delta":{"content":"你"}}]} Codex: response.completed ↓ OpenAI: data: {"choices":[{"delta":{},"finish_reason":"stop"}]} OpenAI: data: [DONE] ``` **非流式模式** (`collectCodexResponse`): - 消费整个 SSE 流,收集所有 text delta - 拼接为完整文本 - 返回标准 `chat.completion` JSON 响应(包含 usage) ### 修改文件 #### `src/routes/chat.ts` — 路由处理器(重写) **之前**:使用 WhamApi 创建 task → 轮询 turn → 提取结果 **之后**:使用 CodexApi 发送 responses 请求 → 直接流式/收集结果 核心流程简化为: ``` 1. 验证认证 2. 解析请求 (ChatCompletionRequestSchema) 3. translateToCodexRequest() 转换格式 4. codexApi.createResponse() 发送请求 5a. 流式:streamCodexToOpenAI() → 逐块写入 SSE 5b. 非流式:collectCodexResponse() → 返回 JSON ``` #### `src/index.ts` — 入口文件(简化) 移除了 WHAM environment 自动发现逻辑(不再需要)。 --- ## 之前修复的 Bug(WHAM 阶段) 在切换到 Codex Responses API 之前,还修复了 WHAM API 相关的三个 bug: 1. **`turn_status` vs `status` 字段名不匹配** — WHAM API 返回 `turn_status`,但代码检查 `status`,导致轮询永远不匹配,超时 300 秒 2. **`getTaskTurn` 响应结构嵌套** — API 返回 `{ task, user_turn, turn }` 但代码把整个响应当作 `WhamTurn`,导致 `output_items` 为 `undefined` 3. **失败的 turn 返回 200 空内容** — 没有检查 `failed` 状态,直接返回空 content 这些修复在 `src/types/wham.ts`、`src/proxy/wham-api.ts`、`src/translation/stream-adapter.ts` 中。 --- ## 测试结果 ### 非流式请求 ```bash curl http://localhost:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"gpt-5.1-codex-mini","messages":[{"role":"user","content":"Say hello"}]}' ``` ```json { "id": "chatcmpl-3125ece443994614aa7b1136", "object": "chat.completion", "choices": [{"message": {"role": "assistant", "content": "Hello!"}, "finish_reason": "stop"}], "usage": {"prompt_tokens": 22, "completion_tokens": 20, "total_tokens": 42} } ``` 响应时间:~2 秒 ### 流式请求 ```bash curl http://localhost:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"gpt-5.1-codex-mini","messages":[{"role":"user","content":"Say hello"}],"stream":true}' ``` ``` data: {"choices":[{"delta":{"role":"assistant"}}]} data: {"choices":[{"delta":{"content":"Hello"}}]} data: {"choices":[{"delta":{"content":"!"}}]} data: {"choices":[{"delta":{},"finish_reason":"stop"}}]} data: [DONE] ``` 首 token 时间:~500ms --- ## 文件结构总览 ``` src/ ├── proxy/ │ ├── codex-api.ts ← 新增:Codex Responses API 客户端 │ ├── client.ts (通用 HTTP 客户端,保留) │ └── wham-api.ts (WHAM 客户端,保留但不再使用) ├── translation/ │ ├── openai-to-codex.ts ← 新增:Chat Completions → Codex 格式 │ ├── codex-to-openai.ts ← 新增:Codex SSE → Chat Completions 格式 │ ├── openai-to-wham.ts (旧翻译器,保留) │ ├── stream-adapter.ts (旧流适配器,保留) │ └── wham-to-openai.ts (旧翻译器,保留) ├── routes/ │ └── chat.ts ← 重写:使用 Codex API ├── index.ts ← 简化:移除 WHAM env 逻辑 └── ... ```