一个零依赖的 Python 代理:把 Codex 发出的 Responses API 请求转发到上游
(默认 https://opencode.ai/zen/go/v1),API key 直接从 Codex 请求里透传,
本地不保存。
默认启动是纯透传,不做任何改写;另外提供两个可选功能,按需开启:
--fix-agent-message:把 Codex 多智能体(V2)私有的agent_message/encrypted_content改写成标准 Responsesmessageitem;--chat-models <前缀>:命中前缀的模型走 Responses → Chat Completions 协议转换,把只提供 Chat 协议的上游(如 DeepSeek 官方、部分第三方网关) 接入 Codex。
- 纯 Python 标准库实现,无第三方依赖,Python 3.8+ 即可运行;
- 默认纯透传,行为最干净;两个可选功能相互独立,互不影响;
- 完整的 Responses ↔ Chat 双向转换:普通 JSON 与 SSE 流式都支持;
- API key 透传,本地不落盘;日志自动脱敏(
sk-*token、Bearer 值); - 上游错误透传、错误体截断、连接与超时控制、并发连接上限;
- 70 个单元 + 端到端测试。
.
├── proxy.py # HTTP 服务器、路由、转发、模型路由、错误处理
├── responses_chat.py # Responses ↔ Chat Completions 双向转换(纯数据函数)
├── test_proxy.py # 单元 + 端到端测试
├── README.md
└── LICENSE # Apache 2.0
默认启动是纯透传,不做任何改写。 两个功能完全独立,按需开启:
| 启动参数 | 作用 |
|---|---|
| 无(默认) | 纯透传。上游官方修复 agent_message 后,这样启动最干净 |
--fix-agent-message |
把 Codex 多智能体(V2)私有的 agent_message / encrypted_content 改写成标准 Responses message item |
--chat-models <前缀> |
模型前缀命中时,把 Responses 转成 Chat Completions 发给 chat 上游,再把响应(JSON/SSE)还原成 Responses |
行为矩阵:
| 启动方式 | agent_message 请求 | 命中 chat-models 的模型 | 其他模型 |
|---|---|---|---|
python3 proxy.py |
原样透传 | 原样透传 | 原样透传 |
加 --fix-agent-message |
改写后透传 | 改写后透传(未配 chat-models) | 改写后透传 |
加 --chat-models mimo |
转换路径隐式改写 | 转换 | 透传 |
| 两个都加 | 改写 → 转换(mimo)或透传(其他) | 转换 | 改写后透传 |
注意:转换路径始终会先改写 agent_message——Chat 协议无法表达这个
OpenAI 私有 item,不改写会导致子代理任务内容丢失。--fix-agent-message
只决定透传路径是否改写。
--chat-models 的转换规则参考了 OpenAI 官方迁移指南、cc-switch、new-api、
LiteLLM、llama.cpp 以及多个 Codex 转接项目,核心映射:
| Responses | Chat Completions |
|---|---|
instructions |
首条 system 消息 |
input 字符串 |
一条 user 消息 |
message item |
同 role 消息(developer 折叠为 system) |
function_call 连续项 |
合并进同一条 assistant 消息的 tool_calls[] |
function_call_output |
role=tool 消息,tool_call_id=call_id |
reasoning item |
累加挂到同一回合 assistant 消息的 reasoning_content(DeepSeek 带 tools 多轮要求完整回传;content 与 tool_calls 同回合时合并成一条 assistant 消息) |
max_output_tokens |
max_tokens(DeepSeek 系只认 max_tokens) |
reasoning.effort |
deepseek: reasoning_effort(flash: low→low、xhigh→high、max→max,其余→high;pro 等: max/xhigh→max,其余→high)+ thinking;mimo: 只发 thinking:{type:enabled/disabled},不发 reasoning_effort |
text.format |
response_format(json_schema 展平为嵌套结构) |
tools[].function / custom |
Chat function tools;其余类型丢弃 |
tool_choice |
{type:"function",name} 展平为嵌套 function.name |
stream=true |
附加 stream_options.include_usage=true |
响应方向:Chat 的 finish_reason 映射为 Responses 的
completed / incomplete(+incomplete_details) / failed;reasoning_content
还原为 reasoning item,content 还原为 message item,tool_calls 还原为
function_call item;usage 做双字段名兼容换算
(prompt_tokens→input_tokens、completion_tokens→output_tokens、
cached_tokens / reasoning_tokens 进 details)。
流式响应按 Responses SSE 事件序列还原:
response.created → response.in_progress → response.output_item.added → response.content_part.added → response.output_text.delta / response.reasoning_text.delta / response.function_call_arguments.delta → 各 done 事件 → response.completed / response.incomplete / response.failed。不发送 Chat 协议的 [DONE] 哨兵;上游流被截断时会如实
归因为 incomplete/failed,不伪装成成功。
python3 proxy.py --host 127.0.0.1 --port 8787python3 proxy.py --port 8787 --fix-agent-messagepython3 proxy.py --port 8787 --chat-models mimo --fix-agent-message~/.codex/config.toml 保持:
[model_providers.deepseek]
name = "deepseek"
base_url = "http://127.0.0.1:8787/v1"
wire_api = "responses"
experimental_bearer_token = "sk-..." # 保持不变写 http://127.0.0.1:8787(不带 /v1)也可以,代理会自动拼接。
python3 proxy.py --port 8787 \
--chat-models deepseek \
--chat-upstream https://api.deepseek.com/v1此时 deepseek-* 请求会由代理转成 Chat Completions 发往
https://api.deepseek.com/v1/chat/completions;其他模型仍走
--upstream 的 Responses 通道。Codex 的
experimental_bearer_token 换成 DeepSeek 的 API key 即可。
DeepSeek 官方目前已原生支持 Responses API(仅
deepseek-v4-flash)。 如果上游直连 v4-flash 的/responses,可以不开转换走纯透传;v4-pro 及 mimo 等仍走转换路径。
python3 proxy.py --help
--host 127.0.0.1 监听地址
--port 8787 监听端口
--upstream https://opencode.ai/zen/go/v1
透传上游 base_url(Responses 通道)
--fix-agent-message 把 agent_message/encrypted_content 改写成标准
Responses message(透传路径;转换路径始终会改写)
--chat-models mimo 需要转成 Chat Completions 的模型前缀(逗号分隔,
前缀匹配);默认空,即关闭转换
--chat-upstream https://api.deepseek.com/v1
Chat 上游 base_url,默认与 --upstream 相同,
即拼 .../chat/completions;
也可直接写完整 .../chat/completions 地址
--timeout 600 上游超时(秒)
--client-timeout 120 客户端读请求超时(秒,0 表示不设超时)
--max-connections 128 最大并发连接数(超出返回 503,0 表示不限)
-v 调试日志
--log-body 打印请求体(任务内容,按需开启)所有日志统一为「时间戳 + 消息」,一行一条,不带级别和 logger 名:
2026-08-08 21:41:40,953 丢弃 Chat 不支持的 tool: web_search
2026-08-08 21:41:40,953 POST /v1/responses -> 200 (98 ms) agent=main proto=responses
2026-08-08 21:41:40,953 POST /v1/responses -> 400 (981 ms) agent=/root/test_multimodal proto=chat error=bad request
字段说明:
agent=main:主代理请求;agent=/root/xxx:子代理请求(从请求里的agent_message任务信封收件人推断,例如/root/worker)。proto=responses:请求走透传(Responses 通道);proto=chat:请求走了 Responses→Chat 转换。
注意:多智能体 V2 下,主代理的上下文里也包含子代理的任务信封和回传结果 (
agent_messageitem),所以不能靠"请求体是否被改写"判断主/子代理, 而是直接看任务信封的收件人。
默认(-v 不加)只输出有问题的请求,正常请求静默;以下关键失败都会在
控制台留下一行 error=...:
- 连接上游失败 / 超时(502)
- 上游返回 4xx/5xx(错误体里的一行消息)
- 上游流式响应中途断开、SSE 解析失败、流被截断
- Chat 转换失败 / 上游返回 200 + error envelope
- 代理内部异常(500)
上游错误体是压缩/图片等二进制内容时,日志不会原样打印字节,只输出
一行摘要,例如 error=upstream returned HTTP 404 (gzip-compressed body, 1234 bytes)。
错误响应透传会保留上游关键头(Retry-After、WWW-Authenticate、
Content-Encoding 等);错误体超过 1MB 会被截断并带
X-Upstream-Body-Truncated: true 标记。Chat 上游返回 200 + error
envelope(或 200 但无 choices)时,流式和非流式都会还原成 Responses
的 failed 状态,不再把 Chat 错误体原样回给客户端、也不伪装成
completed。
需要看全部请求时加 -v。
仓库通过 GitHub Actions 管理镜像构建与发布:
- 普通 push 到 main:自动构建镜像(amd64 + arm64),只验证能编译, 不会发布;
- 创建并发布 Release(如
v0.1.0):自动生成修复记录 (写入 Release Notes 并更新仓库CHANGELOG.md),然后构建并发布到 GitHub Container Registry,生成v0.1.0、v0.1、v0和latest标签。
发布后的镜像地址:
docker pull ghcr.io/thisdk/codex-opencode-proxy:latest镜像的启动参数与 python3 proxy.py 完全一致、原样透传,比如本地的
python3 proxy.py --port 8787 --chat-models mimo --fix-agent-message -v对应容器启动方式:
docker run --rm -p 8787:8787 \
ghcr.io/thisdk/codex-opencode-proxy:latest \
--port 8787 --chat-models mimo --fix-agent-message -v规则:
- 所有参数原样传给
proxy.py,与直接跑脚本没有区别; - 容器内默认绑定
0.0.0.0(入口脚本自动补--host 0.0.0.0),这样-p 8787:8787端口映射才有效;如果你显式传了--host,以你的值为准; - 不带任何参数时等价于
python3 proxy.py,默认监听8787端口; - 镜像包含
linux/amd64和linux/arm64两种架构;
发布方式(命令行示例):
gh release create v0.1.0 --target main --title "v0.1.0" --notes "发布 v0.1.0"也可以在仓库 Release 页面手动创建并发布 Release。发布后 Actions 会自动
完成修复记录生成与 ghcr.io 推送;之后随时可以用
docker pull ghcr.io/thisdk/codex-opencode-proxy:v0.1.0 拉取对应版本。
本地构建:
docker build -t codex-opencode-proxy .python3 -m unittest test_proxy -v共 70 个测试,覆盖:agent_message 改写开关、默认纯透传、mimo/deepseek
路由分离、Responses→Chat 请求映射、Chat→Responses 非流式还原、SSE 事件
序列与截断/错误路径、经本地 mock 上游的端到端 JSON 与流式转换、上游错误
透传、并发连接与超时处理。
- Python 3.8+
- 无第三方依赖,直接
python3 proxy.py即可运行
- 上游
opencode.ai会按 User-Agent 拦截,普通 Python/curl 请求会被 403 (error code: 1010)。Codex 自己发出的请求带有 Codex 的 User-Agent,可以 正常通过,因此代理默认保持请求头原样透传。 - 如果
encrypted_content里是真正的密文(而非明文),转成input_text后模型看到的是密文。目前 Codex 对第三方 provider 的普通子代理任务发送 的是明文,可直接转换;若遇到真密文场景需要配合上游解密方案。 - 客户端必须完整发送请求体:声明
Content-Length但提前断开会收到 400, 慢连接超过--client-timeout会收到 408;Content-Length与Transfer-Encoding并存会被拒绝。 - close-delimited(无 Content-Length、无 chunked)的上游流式响应会逐块 立即转发,不会等攒满 64KB。
- Chat 转换是无状态的:
previous_response_id、store、metadata等 Responses 状态字段会被丢弃;DeepSeek 在带 tools 的多轮对话里要求完整 回传reasoning_content,代理会把输入里的reasoningitem 挂回 assistant 消息,但如果你遇到 400,请检查上一轮上游返回的reasoning_content是否 完整回传。 - 非 function 类工具(web_search、file_search、computer_use 等)在 Chat
转换路径会被丢弃,只有
function/custom工具可转换。 - 如果
tool_choice引用的是被丢弃的工具,转换会降级为auto,避免上游 返回 400;namespace / tool_search 工具的完整双向还原仍在规划中。 - 转换后的流式响应没有
[DONE]哨兵(Responses 协议本身没有),不要用 Chat 客户端的解析方式去读。