本文说明当前项目中的 API 反代实现。目标是让你从入口到上游返回,完整看懂这条链路现在是怎么工作的。
当前版本已经只保留一种反代方式:
- 本地对外暴露 OpenAI 兼容接口
- 上游统一转到 Codex 的
chatgpt.com/backend-api/codex/responses - 不再保留旧的多模式反代
- Axum 请求体大小上限默认放宽到
512 MiB
实现参考了 CLIProxyAPIPlus 的 Codex executor 思路,但不是把它整仓搬进来,而是把核心方法收敛到本项目现有架构里。
GET /healthGET /v1/modelsPOST /v1/chat/completionsPOST /v1/responsesPOST /v1/messagesPOST /v1/images/generationsPOST /v1/images/editsPOST /v1/images/variations
POST https://chatgpt.com/backend-api/codex/responses
也就是说:
- 客户端以为自己在调用 OpenAI 风格的
/v1/* - 实际上本地代理会把请求转换为 Codex
responses协议 - 然后用账号池中的 ChatGPT/Codex 登录态去访问上游
之前的问题有两个:
- 直接打
api.openai.com/v1/*时,很多导入的账号本质上只有 ChatGPT/Codex 登录态,不一定有公共 OpenAI API scope 或 quota - 旧的
conversation直通方式过于贴近历史接口,不适合作为稳定代理基座
现在的方案本质上是:
- 下游维持
/v1兼容,方便接各种现成客户端 - 上游切到 Codex 当前更合适的
responses入口 - 中间由本地代理负责协议转换
sequenceDiagram
participant UI as "前端面板"
participant Tauri as "Tauri Command"
participant Local as "本地 Axum 代理"
participant Store as "accounts.json / 账号池"
participant Upstream as "chatgpt.com/backend-api/codex/responses"
UI->>Tauri: start_api_proxy(port)
Tauri->>Local: 启动 127.0.0.1:port
Local->>Store: 读取账号池
Store-->>Local: 候选账号列表
Note over Local: 等待客户端请求
UI->>Local: POST /v1/chat/completions
Local->>Local: 校验 API Key
Local->>Local: OpenAI 请求转 Codex responses 请求
Local->>Store: 按排序取候选账号
Local->>Upstream: Bearer access_token + ChatGPT-Account-Id
Upstream-->>Local: SSE 响应
Local->>Local: SSE 事件转 OpenAI 响应
Local-->>UI: /v1/chat/completions 响应
前端入口在:
src/components/ApiProxyPanel.tsxsrc/hooks/useCodexController.ts
用户在面板点击“启动 API 反代”后:
- 前端读取端口输入框
- 调用 Tauri 命令
start_api_proxy - 成功后显示:
Base URLAPI Key- 当前命中的账号
- 最近错误
Tauri 命令入口在:
src-tauri/src/lib.rs
相关命令:
get_api_proxy_statusstart_api_proxystop_api_proxy
后端运行态在:
src-tauri/src/state.rs
这里维护:
- 监听端口
- 当前代理 API Key
- 运行任务句柄
- 当前命中的账号 ID/标签
- 最近一次错误
启动逻辑在:
src-tauri/src/proxy_service.rs
start_api_proxy_internal(...) 做的事情是:
- 检查当前是否已经有代理在运行
- 从账号池加载可用账号
- 绑定本地端口,默认
8787 - 生成一个本地代理专用
sk-...API Key - 创建
reqwest::Client - 启动一个
axumHTTP 服务 - 注册以下路由:
/health/v1/models/v1/chat/completions/v1/responses/v1/messages/v1/images/generations/v1/images/edits/v1/images/variations- 对请求体启用
512 MiB默认上限,可用CODEX_TOOLS_PROXY_MAX_BODY_MIB覆盖
- 把运行状态写入全局
AppState
用途:
- 用于判断本地代理服务是否活着
返回:
{ "ok": true }用途:
- 给兼容客户端一个模型列表
特点:
- 这里不是实时问上游拿模型
- 目前返回本地静态模型列表:
gpt-5.6-sol、gpt-5.6-terra、gpt-5.6-luna、gpt-5.5、gpt-5.4、gpt-image-2 gpt-5.6、gpt5.6、gpt-5-6会映射到gpt-5.6-sol- 目的是让大多数依赖
/v1/models的客户端能正常初始化
推理与速度参数:
- 推理强度兼容值:
none、minimal、low、medium、high、xhigh、max - 推理速度:
auto、default、fast、flex priority是fast的上游 wire 别名,standard是default的兼容别名- 请求未指定时默认使用
xhigh与fast ultra属于 Codex 客户端的多代理编排模式;反代收到该兼容值时按实际推理 wire 值max发送- GPT-5.6 的官方推理强度为
none、low、medium、high、xhigh、max;minimal仅保留给旧模型兼容,GPT-5.6 请求会明确拒绝 - 具体模型可用档位仍以上游能力为准;代理会拒绝未知值,不会静默降级
用途:
- 兼容绝大多数 OpenAI Chat Completions 客户端
行为:
- 本地接收 OpenAI 风格请求
- 转为 Codex
responses请求 - 上游始终按 SSE 模式请求
- 如果下游请求
stream: true,本地再把上游 SSE 转成 OpenAI ChatCompletions SSE - 如果下游请求
stream: false,本地会先收完整个 SSE,再拼成普通 JSON 返回
用途:
- 给直接走 OpenAI Responses 风格的客户端使用
行为:
- 非 GPT-5.6 请求体只做必要归一化;GPT-5.6 会转换为 Responses Lite 结构
- 上游仍然统一发到 Codex
responses stream: true时近似透传 SSEstream: false时从 SSE 中提取response.completed,返回标准 JSON- Codex 0.144 WebSocket v2 会收到
426并按客户端内置逻辑回退到 HTTP,避免破坏 warmup 和previous_response_id状态
用途:
- 给 Anthropic Messages 兼容客户端使用
行为:
- 本地接收 Anthropic 风格请求
- 转为 Codex
responses请求 - 流式时转回 Anthropic SSE
- 非流式时转回 Anthropic Message JSON
- 认证推荐使用
x-api-key: sk-... - 需要带
anthropic-version: 2023-06-01
当前兼容字段:
modelsystemmessagesmax_tokensstreamtemperaturetop_pstop_sequencestoolstool_choicethinking.type = enabled
其中 2023-06-01 是 Anthropic 当前公开的 API version,不是模型版本日期。Anthropic 官方版本文档目前仍把它作为 Messages API 的最新版本示例与版本历史项。
用途:
- 给 OpenAI 图片兼容客户端使用
行为:
generations会转成 Codex image generation tooledits和variations会把上传图片转成input_image- 返回值使用 OpenAI 图片接口常见的
b64_json
本地代理有自己的一层鉴权,不直接暴露给任意本地程序。
支持两种传法:
X-API-Key: sk-...x-api-key: sk-...Authorization: Bearer sk-...
如果调用 /v1/messages,还需要带:
anthropic-version: 2023-06-01
校验逻辑:
- 只认启动代理时生成的那一个本地
API Key - 这层 key 只是本地代理的门锁
- 它不是上游 OpenAI API Key
- 它也不是账号池里真实的
access_token
账号来源:
- 应用数据目录下的
accounts.json
数据结构定义在:
src-tauri/src/models.rs
每个账号核心字段有:
labelaccount_idauth_jsonusageplan_type
代理真正需要的认证信息来自 extract_auth(...):
access_tokenaccount_id- 可选
plan_type
候选账号在每次请求时重新读取,并重新排序,不做固定缓存。代理运行中新增、删除、禁用账号后,不需要关闭代理进程,下一次请求就会按最新账号池选择。
排序规则:
- 优先
free账号 - 再比较
1week剩余额度 - 再比较
5h剩余额度 - 最后按标签名排序
也就是:
free计划会优先于其他计划- 在同一类计划中,优先挑“更有余量”的账号
对应逻辑:
load_proxy_candidates(...)compare_proxy_candidates(...)
负载均衡选择“逐个”时,代理会优先复用当前会话已经命中的账号。
会话标识来源:
- 请求头:
x-codex-tools-session - 请求头:
x-codex-session-id - 请求头:
session_id - 请求头:
session-id - 请求头:
x-client-request-id - 请求头:
openai-conversation-id - 请求体:
sessionId - 请求体:
session_id - 请求体:
conversationId - 请求体:
conversation_id - 请求体:
threadId - 请求体:
thread_id - 请求体:
previous_response_id - 请求体:
previousResponseId
规则:
- 同一个会话继续用同一个账号,直到该账号达到 5h 阈值或被上游拒绝
- 不同会话各自绑定账号,互不抢全局当前账号
- 没有会话标识的旧客户端继续走原来的全局逐个模式
- 绑定只保存在内存里,停止代理后会清空
真正向上游发请求时,目标是:
https://chatgpt.com/backend-api/codex/responses
关键请求头:
Authorization: Bearer <candidate.access_token>ChatGPT-Account-Id: <candidate.account_id>Originator: codex_cli_rsVersion: 0.144.0Session_id: <uuid>User-Agent: codex_cli_rs/0.144.0Accept: text/event-streamContent-Type: application/json
这里有几个关键点:
- 不是发到
api.openai.com/v1/* - 不是发到旧的
conversation - 默认模拟的是 Codex CLI 风格头
- 上游固定按 SSE 返回
这是当前链路最核心的一层。
本地会补这些字段:
stream: truestore: falsereasoning.effort: xhighreasoning.summary: autoinclude: ["reasoning.encrypted_content"]service_tier: priority(下游可写fast)
GPT-5.6 Sol/Terra/Luna 会额外使用 Responses Lite 契约:
- 请求头补
x-openai-internal-codex-responses-lite: true - 顶层
tools移到input首项additional_tools - 非空
instructions移到 developer message - 顶层
tools与instructions不再发送 parallel_tool_calls: falsereasoning.context: all_turns
这里 store: false 很关键。
实际验证里,上游 backend-api/codex/responses 明确要求:
- 没带
store: false会报错
OpenAI 风格消息会被转换为 Codex input 数组。
角色映射:
system -> developerdeveloper -> developeruser -> userassistant -> assistanttool -> function_call_output
文本:
text -> input_text或output_text
图片:
image_url -> input_image
文件:
file -> input_file
OpenAI 里的:
assistant.tool_callstool消息
会被拆成 Codex 里的:
function_callfunction_call_output
如果下游传了:
response_formattext.verbosity
本地会转换到上游的:
text.formattext.verbosity
如果下游直接请求 /v1/responses,本地不会像 chat/completions 那样大幅转换,只做必要补丁:
- 强制
stream: true - 强制
store: false - 缺失时补
instructions - 缺失时补
parallel_tool_calls - 缺失时补
reasoning.effort = xhigh - 缺失时补
reasoning.summary = auto - 缺失时补快速档,向上游发送
service_tier = priority - 确保
include里有reasoning.encrypted_content - 丢弃上游不接受的
metadata字段,避免 Cursor 等客户端报Unsupported parameter: metadata - GPT-5.6 请求继续转换为 Responses Lite 契约
如果下游本来就是 Responses 客户端:
stream: true时,基本按 SSE 透回去stream: false时,从完整 SSE 中提取response.completed
这是转换最多的地方。
上游返回的是 Codex SSE 事件流,本地会把关键事件转成 OpenAI ChatCompletions SSE。
| 上游事件 | 本地输出 |
|---|---|
response.created |
初始化状态,不立即输出 |
response.reasoning_summary_text.delta |
delta.reasoning_content |
response.reasoning_summary_text.done |
补一个换行分隔 |
response.output_text.delta |
delta.content |
response.output_item.added with function_call |
delta.tool_calls 开始事件 |
response.function_call_arguments.delta |
delta.tool_calls[].function.arguments 增量 |
response.function_call_arguments.done |
参数结束兜底 |
response.output_item.done with function_call |
工具调用完成兜底 |
response.completed |
finish_reason + [DONE] |
如果下游 stream: false:
- 本地仍然向上游请求 SSE
- 读完整个 SSE
- 找到
response.completed - 从
response.output里提取:- assistant 文本
- reasoning summary
- tool calls
- usage
- 拼成一个普通的 OpenAI ChatCompletions JSON
每次请求会按候选账号顺序尝试。
对单个账号的流程:
- 先用当前 token 发上游请求
- 如果命中“疑似 token 过期/失效”信号,则先 refresh
- refresh 成功后重试这个账号一次
- 如果仍失败,再判断是否属于可切下一个账号的错误
这条链路就是 runtime rotation:
- 客户端只需要一直请求同一个
Base URL - 不需要改写
~/.codex/auth.json - 不需要关闭 Codex App/CLI
- 不需要重启反代服务
- 代理会在请求发到上游前选择账号,并在收到可轮换错误时换到下一个账号
- 流式请求在开始向下游输出前完成账号选择;已经开始输出的同一个流不会中途换账号
如果你要做 wrapper 或 app bind,推荐方式是让 Codex App/CLI 指向本工具的 /v1 反代,并为每个会话透传稳定的 session key。这样官方 App/CLI 进程可以保持打开,账号轮换发生在本地代理层。
wrapper 的职责很简单:
- 确保本工具 API 反代已经启动
- 把 Codex 的 provider/base_url 指到本工具显示的
Base URL - 把本工具显示的
API Key作为下游 key - 为同一个 Codex 会话透传稳定 session key
- 启动官方 Codex CLI/App,不接管官方
codex二进制
app bind 不需要 patch 官方 App 文件。
面板已经提供按钮:
- 先启动 API 反代
- 点击“切到本机反代”
- 工具会备份当前
~/.codex/config.toml和~/.codex/auth.json - 工具会把
openai_base_url写成当前本机Base URL - 工具会把当前反代
API Key写入auth.json - 点击“恢复正常地址”时,还原到绑定前的配置和认证文件
这个方式把“账号池 + token refresh + 运行中轮换”留在 codex-tools,把官方 App/CLI 只当普通 OpenAI/Responses 客户端。
401- 错误体包含:
token expiredjwt expiredinvalid tokensession expiredlogin required
刷新成功后会把新的认证状态写回两处:
- 账号池里的
accounts.json - 如果它正好是当前激活账号,也会回写
~/.codex/auth.json
如果某个账号失败,本地会尽量分类,而不是只报一个泛泛的 429。
当前分类:
额度用完频率限制模型受限鉴权失败权限不足
当所有候选账号都失败时,会返回类似:
- 哪一类失败有多少个
- 再附一个示例原因
这样你能更快判断是:
- 账号整体没额度
- 当前模型不支持
- 还是登录态坏了
运行中的状态会保存在:
ApiProxyRuntimeSnapshot
主要字段:
active_account_idactive_account_labelsequential_session_affinitylast_error
前端轮询这些状态,所以面板上能看到:
- 当前命中的账号
- 最近一次错误
Cloudflared 不是第二套代理逻辑,它只是把当前本地代理继续暴露到公网。
关系是:
- 本地 API 反代先启动
- Cloudflared 再把这个本地端口转出去
所以链路是:
客户端 -> cloudflared 公网地址 -> 本地 127.0.0.1:8787/v1/* -> Codex 上游
Cloudflared 不参与:
- 请求协议转换
- 账号挑选
- token refresh
- SSE 到 OpenAI 的映射
它只负责公网入口。
当前实现支持:
- 入站:
POST /v1/responses/compact - ChatGPT 上游:
{chatgpt}/backend-api/codex/responses/compact - Relay 上游:
{api_base}/responses/compact
行为要点:
- 上游 compact 走 unary JSON(
Accept: application/json),不走 SSE - 客户端
stream: true时,把上游 JSON 合成最小 Responses SSE(output_item.done+response.completed) remote_compaction_v2的compaction_trigger保持普通/v1/responses流式协议,不降级到 unary compact- Responses(HTTP/WebSocket)和 compact 按窄白名单转发 turn-state、beta、turn metadata、window 与 trace 上下文;快速 SSE 响应也保留上游返回的状态头
- 当入站已有 turn-state 且上游等待超过 10 秒时,发送可被客户端消费但忽略的
response.in_progressSSE 心跳;没有可安全回显的状态头时等待上游完整响应后再提交响应头 - 心跳已发出后的失败改为流内
response.failed,并尽量保留上游code/type/message等错误字段 - compact body 与 Codex
CompactionInput对齐,只保留model/input/instructions/tools/parallel_tool_calls/reasoning/service_tier/prompt_cache_key/text,并统一规范化 service tier - ChatGPT 账号上 GPT-5.6
reasoning.effort=max会降级为xhigh - JSON 请求体支持 gzip、标准 zlib-wrapped deflate(兼容 raw deflate)与 zstd;解压在阻塞线程池执行,解压后仍受请求体上限约束
- Relay 返回
404/405/501时视为不支持 compact,并继续尝试后续候选账号
当前版本不是“完整 OpenAI API 网关”,而是“OpenAI 兼容的 Codex 代理”。
明确限制:
- 只支持:
GET /v1/modelsPOST /v1/chat/completionsPOST /v1/responsesPOST /v1/responses/compactGET /v1/responses(旧版 WebSocket;v2 返回426触发 HTTP 回退)POST /v1/messagesPOST /v1/images/generationsPOST /v1/images/editsPOST /v1/images/variations
- 其他
/v1/*路径目前直接返回不支持 - 模型列表是本地静态表,不是实时探测
/v1/responses的流式是近似透传,不额外做深层语义改写- GPT-5.6 Responses Lite 不接收 hosted
web_search、web_search_preview、image_generation工具;这类请求会明确拒绝,客户端扩展工具仍会透传 - 远程图片 URL 会替换为占位文本,Lite 请求不会发送图片
detail - 核心目标是把最常用的聊天链路稳定打通
如果你要继续看代码,优先看这些文件:
- 启动、路由、转换、账号轮换:
src-tauri/src/proxy_service.rs
- Tauri 命令:
src-tauri/src/lib.rs
- 运行态:
src-tauri/src/state.rs
- 账号结构:
src-tauri/src/models.rs
- 前端面板:
src/components/ApiProxyPanel.tsx
- 前端控制器:
src/hooks/useCodexController.ts
以这个调用为例:
curl http://127.0.0.1:8787/v1/chat/completions \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer sk-xxxx' \
-d '{
"model": "gpt-5.6-sol",
"reasoning_effort": "xhigh",
"service_tier": "fast",
"stream": false,
"messages": [
{ "role": "user", "content": "1+1 等于几?只回答结果。" }
]
}'内部实际流程是:
- 本地校验
sk-xxxx - 找到可用账号列表
- 把请求改写成 Codex
responses请求 - 强制补
store: false - 带着账号
access_token + account_id去打上游 - 上游返回 SSE
- 本地抽取
response.completed - 转成 OpenAI ChatCompletions JSON
- 返回类似:
{
"object": "chat.completion",
"choices": [
{
"message": {
"role": "assistant",
"content": "2"
},
"finish_reason": "stop"
}
]
}curl http://127.0.0.1:8787/healthnpm run test:codex-login-proxy -- --api-key 你的sknpm run test:codex-login-proxy -- --api-key 你的sk --prompt '1+1 等于几?只回答结果。'npm run test:codex-login-proxy -- --api-key 你的sk --prompt '1+1 等于几?只回答结果。' --raw这会把:
- 响应头
- 原始 body
- 请求元数据
都落到本地文件,方便继续排查。
如果你想把本工具作为“账号池 + OpenAI 兼容反代”,再由 CC Switch 来统一管理 Codex provider,这条链路是支持的。
原因很简单:
- 本工具下游暴露的是 OpenAI 兼容
/v1接口 - CC Switch 给 Codex 写入的自定义 provider 也是
OPENAI_API_KEY + base_url + wire_api = "responses"这套配置
也就是说:
codex-tools -> CC Switch -> Codex- 协议层是能对上的
当前这套接入方式,适用于:
- CC Switch 中的 Codex 自定义 provider
- 能按 OpenAI
/v1或 Anthropic/v1/messages子集发请求的客户端
不适用于:
- 把本工具当成完整 Claude 官方网关
原因是本工具当前只提供这些出口:
GET /v1/modelsPOST /v1/chat/completionsPOST /v1/responsesGET /v1/responses(旧版 WebSocket;v2 使用 HTTP 回退)POST /v1/messagesPOST /v1/images/generationsPOST /v1/images/editsPOST /v1/images/variations
其中 /v1/messages 是 Anthropic Messages 兼容子集,最终仍会转到 Codex responses 上游。
在 CC Switch 中新增一个 Codex 自定义 provider,可按下面填写。
auth.json:
{
"OPENAI_API_KEY": "这里填 codex-tools 面板里生成的 sk-..."
}config.toml:
model_provider = "codex_tools"
model = "gpt-5.6-sol"
model_reasoning_effort = "xhigh"
service_tier = "fast"
disable_response_storage = true
[model_providers.codex_tools]
name = "codex_tools"
base_url = "http://127.0.0.1:8787/v1"
wire_api = "responses"
requires_openai_auth = true如果你不是本机直连,而是通过 cloudflared 公网域名接入,把 base_url 改成:
base_url = "https://你的公网域名/v1"-
base_url要填到/v1为止- 对:
http://127.0.0.1:8787/v1 - 错:
http://127.0.0.1:8787 - 错:
http://127.0.0.1:8787/v1/responses
- 对:
-
OPENAI_API_KEY要填本工具生成的代理 key- 也就是面板显示的
sk-... - 不是 OpenAI 官方 API Key
- 也不是账号池里真实的
access_token
- 也就是面板显示的
-
wire_api要用responses- 因为本工具上游统一走的是 Codex
responses - CC Switch 的 Codex 自定义 provider 也应使用
responses
- 因为本工具上游统一走的是 Codex
先直接测本工具自己的出口:
curl http://127.0.0.1:8787/health
curl http://127.0.0.1:8787/v1/models -H 'Authorization: Bearer 你的sk'
curl http://127.0.0.1:8787/v1/messages \
-H 'x-api-key: 你的sk' \
-H 'anthropic-version: 2023-06-01' \
-H 'Content-Type: application/json' \
-d '{
"model": "gpt-5.6-sol",
"max_tokens": 256,
"messages": [{"role": "user", "content": "Say hi in one sentence."}]
}'如果这些请求都正常:
- 说明
codex-tools代理本身已经起来了 - 后续问题通常就在 CC Switch 或客户端 provider 配置
如果本机地址可以通,但外网地址不通,优先检查:
- cloudflared 是否真的映射到了当前反代端口
base_url是否写成了公网域名加/v1- OpenAI/Responses 客户端是否带上了
Authorization: Bearer sk-... - Anthropic Messages 客户端是否带上了
x-api-key: sk-...和anthropic-version: 2023-06-01
如果你在 CC Switch 里配置的是 Claude provider,而不是 Codex provider,需要注意:
- 本工具现在可以直接接收 Anthropic Messages 兼容请求:
POST /v1/messages - 认证头按 Anthropic 风格填
x-api-key: sk-... - 版本头填
anthropic-version: 2023-06-01 - 这仍然是兼容转换出口,不是完整 Claude 官方网关
- 上游还是 Codex
responses,不是 Anthropic 官方模型服务
如果某个客户端的 Claude provider 需要填写 base URL:
- 客户端会自动拼
/v1/messages时,base URL 填到http://127.0.0.1:8787 - 客户端要求完整接口地址时,填
http://127.0.0.1:8787/v1/messages - 如果通过公网域名接入,把主机名换成 cloudflared 或远程反代域名
当前反代的本质是:
- 本地对客户端说“我是 OpenAI
/v1,也能接 Anthropic/v1/messages” - 对上游实际上说“我是 Codex CLI,在调用
backend-api/codex/responses” - 中间靠账号池、协议转换、SSE 事件转换,把两边接起来