Windows + Clash 环境下 Codex CLI 接入 DeepSeek V4 Pro 完整踩坑实录
适用版本 :Codex CLI v0.120+(Rust 重写版)· DeepSeek V4 Pro · Windows 10/11 · Clash for Windows / Clash Verge
最后更新 :2026-06
作者背景:在 Windows 主力机 + Clash 翻墙的真实环境下把这套链路跑通,踩了所有能踩的坑,记录在此。
为什么需要这篇文章
Codex CLI 和 DeepSeek 各自都很香,但两者不能直连------这不是配置问题,是协议层根本不兼容:
objectivec
Codex CLI → OpenAI Responses API(私有协议)
DeepSeek → Chat Completions API(标准 OpenAI 格式)
中间必须有一个翻译层(bridge/proxy)。
网上现有教程大多是 Mac/Linux 视角,跳过了 Windows 特有的代理冲突,并且对 SSE 流式中断、OpenRouter 兼容性这些深层问题几乎没有解释。这篇文章是我在 Windows + Clash 环境下把完整链路跑通的第一手记录。
整体架构
bash
Codex CLI
│ Responses API(POST /v1/responses)
▼
codex-bridge(本地 Node.js,端口 4000)
│ Chat Completions API(翻译后)
▼
DeepSeek API(api.deepseek.com)
│ 走系统代理 / 直连(取决于你的 Clash 配置)
▼
DeepSeek V4 Pro 模型
关键认知:Codex 的工具调用、沙盒、上下文管理全在客户端,bridge 只负责协议翻译,不会影响 Codex 的核心能力。
坑 #1:OpenRouter 不是银弹------SSE 格式不兼容
现象
社区里最常见的建议是"用 OpenRouter BYOK 就行"。理论上 OpenRouter 支持 Responses API,于是很多人第一步就去配 OpenRouter。结果:
- 短任务偶尔能跑
- 稍长的任务流式响应中途断掉(
stream disconnected) - Codex 报错后重试,陷入死循环
根本原因
OpenRouter 的 Responses API 实现与 Codex 期望的 SSE 事件格式存在细节差异:
| 字段 | Codex 期望 | OpenRouter 实际返回 |
|---|---|---|
event: response.output_item.done |
每个 output item 结束时必须发送 | 部分情况下省略或延迟 |
data: [DONE] 时机 |
stream 完全结束后 | 有时提前发送 |
| heartbeat keep-alive | 长任务期间需要空行保活 | 不保证 |
Codex CLI 对 SSE 格式的解析是严格的,任何一个事件缺失或顺序错乱,它就认为流已结束。
解决方案
不要把 OpenRouter 作为 bridge 方案。用本地 bridge 直连 DeepSeek 官方 API,自己控制 SSE 格式。
坑 #2:SSE heartbeat 与 upstream timeout 的隐性矛盾
现象
本地 bridge 配好后,短对话正常,但让 Codex 执行稍复杂的任务(比如分析一个完整代码库)时:
javascript
Error: stream disconnected after 60s
或者更诡异------任务静默终止,没有任何报错,Codex 直接回到提示符。
根本原因
这是三层 timeout 叠加的问题:
DeepSeek V4 Pro 思考耗时 → 可能 30-120 秒
Node.js 默认请求超时 → 通常 60 秒
Codex SSE 心跳期望 → 如果 30 秒内没有任何 SSE 事件,认为连接断开
DeepSeek V4 Pro 在深度思考时会有较长的静默期(模型在 reasoning,但还没输出),这段时间 bridge 如果不主动发送 keep-alive 事件,Codex 会单方面断开连接。
解决方案
在 bridge 代码中加入 SSE heartbeat 逻辑,每 15 秒发送一个注释行:
javascript
// 在 bridge 的 SSE 响应处理函数中
const heartbeatInterval = setInterval(() => {
res.write(': keep-alive\n\n'); // SSE 注释行,Codex 会忽略内容但保持连接
}, 15000);
// 请求完成后清理
upstream.on('end', () => {
clearInterval(heartbeatInterval);
res.end();
});
同时需要延长 upstream 请求的 timeout:
arduino
// Node.js 发出请求时
const options = {
timeout: 300000, // 5 分钟,给 DeepSeek 足够的思考时间
// ...
};
如果你用的是 wujfeng712/codex-bridge,这个问题已经在其实现中处理了。推荐直接用这个版本而不是自己从头写。
坑 #3:Clash 劫持回环流量------最隐蔽的坑
现象
bridge 启动正常,curl 直接测试 localhost:4000 也能通,但 Codex 通过 bridge 请求 DeepSeek 时报:
401 Unauthorized
或者:
arduino
Error: connect ETIMEDOUT api.deepseek.com
换一种表现:bridge 收到了 Codex 的请求,转发出去,但 DeepSeek 那边验证失败------明明 API key 是对的。
根本原因
Clash 的 TUN 模式会劫持所有网络流量,包括本机回环(127.0.0.1) 。
完整的问题链路是:
less
Codex → 127.0.0.1:4000(本地 bridge)
↓ 被 Clash TUN 接管
Clash 把这个请求当作"需要代理的流量"
↓
发送到你的境外节点
↓
节点转发到 api.deepseek.com
↓ 但 Authorization header 在途中被修改或丢失(部分节点会处理 header)
DeepSeek 收到无效 key → 401
即使 Clash 没有修改 header,回环流量经过代理节点再回来,API key 的来源 IP 变成了节点 IP,而 DeepSeek 的速率限制可能把多用户共享节点 IP 的请求当作异常处理。
解决方案
在启动 Codex(或 bridge)之前,设置 NO_PROXY 环境变量,告诉系统代理不要处理本机回环流量:
PowerShell(推荐,每次启动 bridge 前执行):
ini
$env:NO_PROXY = "127.0.0.1,localhost,::1"
$env:no_proxy = "127.0.0.1,localhost,::1" # 部分工具读小写版本
node proxy.mjs
一行命令版(推荐写成启动脚本):
ruby
$env:NO_PROXY="127.0.0.1,localhost,::1"; $env:no_proxy="127.0.0.1,localhost,::1"; node proxy.mjs
更彻底的方案:在 Clash 配置文件中加入旁路规则:
markdown
# Clash config.yaml
bypass:
- localhost
- 127.*
- 10.*
- 172.16.*
- 192.168.*
- ::1
⚠️ 验证方法:bridge 启动后,先用 curl 测试:
csscurl -H "Content-Type: application/json" http://127.0.0.1:4000/v1/responses -d '{"model":"deepseek-v4-pro","input":[{"type":"message","role":"user","content":"hi"}],"max_output_tokens":10}'如果返回正常响应,说明 bridge → DeepSeek 链路通了,再接 Codex。
坑 #4:config.toml 字段位置敏感,错了不报错
现象
按照文档配好 ~/.codex/config.toml,启动 Codex 后:
- 模型选择界面看起来正常
- 发送第一条消息后,Codex 实际请求的还是
api.openai.com(用了默认配置) - 或者报
Model metadata not found,但任务继续进行,最终结果不对
根本原因
Codex config.toml 对 [model_providers.xxx] 块的字段顺序和缩进有隐性要求 ,wire_api = "responses" 必须在 base_url 之前声明,否则静默使用默认值。
另一个常见问题:provider 名称和 model 字段的引用必须大小写完全一致。
正确的 config.toml 模板
ini
# ~/.codex/config.toml
# 注意:先备份原文件!
# cp ~/.codex/config.toml ~/.codex/config.toml.bak
model = "deepseek-v4-pro"
model_provider = "deepseek_bridge"
[model_providers.deepseek_bridge]
name = "DeepSeek via Bridge"
wire_api = "responses" # ← 必须在 base_url 之前
base_url = "http://127.0.0.1:4000/v1"
env_key = "DEEPSEEK_API_KEY"
model_context_window = 128000 # DeepSeek V4 Pro 上下文窗口
model_max_output_tokens = 8192
在 .env 或启动前设置:
ini
$env:DEEPSEEK_API_KEY = "sk-你的DeepSeek密钥"
验证配置是否生效
启动 Codex 后,在第一条消息里让它自报家门:
你现在使用的是哪个模型?返回 model 字段的完整值。
如果返回 deepseek-v4-pro,配置生效。如果返回 gpt-* 或空,说明 config 没有被正确加载,检查文件路径和字段顺序。
坑 #5:reasoning_content 在多轮 tool call 中丢失(进阶)
现象
使用 DeepSeek V4 Pro 的思考模式(深度推理)时,第一轮 tool call 输出质量很好,但从第二轮开始,模型的回答开始"变蠢"------逻辑跳跃,前后矛盾,像是忘记了自己在想什么。
根本原因
DeepSeek V4 Pro 在思考模式下,每次响应包含两部分:
css
reasoning_content → 模型的内部推理过程(用户不可见)
content → 最终输出
Codex 是多轮对话框架,每一轮 tool call 都需要把上下文传回模型。标准 Chat Completions API 里没有 reasoning_content 的续传机制------如果 bridge 不做特殊处理,第二轮请求里的上下文就丢失了推理链,模型等于从头开始想。
解决方案
bridge 需要在服务端缓存 reasoning_content,并在下一轮请求时作为 prefix 或 cache 形式回注。
wujfeng712/codex-bridge 实现了一个 LRU bounded store ,用 previous_response_id 作为 key 缓存推理内容,在多轮 tool call 时自动重播。这是目前中文生态里极少有人解释清楚的机制。
如果你自己实现 bridge,需要在 POST /v1/responses 处理函数里:
csharp
// 伪代码,说明逻辑
const cache = new LRUCache({ max: 50 });
async function handleResponses(req, res) {
const { previous_response_id, ...rest } = req.body;
// 如果有上轮 ID,取出缓存的 reasoning_content
const cachedReasoning = previous_response_id
? cache.get(previous_response_id)
: null;
// 构造发给 DeepSeek 的请求,把 reasoning 回注
const upstreamPayload = buildChatCompletions({
...rest,
prefix_reasoning: cachedReasoning, // 实际字段名取决于 DeepSeek API 版本
});
// 转发,收到响应后缓存本轮的 reasoning_content
const response = await callDeepSeek(upstreamPayload);
cache.set(response.id, response.reasoning_content);
// 翻译回 Responses API 格式返回给 Codex
return translateToResponsesAPI(response);
}
完整启动流程(可做成 .ps1 脚本)
把下面内容保存为 start-codex.ps1,每次启动时运行:
php
# start-codex.ps1
# Codex CLI + DeepSeek V4 Pro 一键启动脚本(Windows + Clash 环境)
# 1. 设置 NO_PROXY,避免 Clash 劫持回环流量
$env:NO_PROXY = "127.0.0.1,localhost,::1"
$env:no_proxy = "127.0.0.1,localhost,::1"
# 2. 设置 DeepSeek API Key(建议改为从系统环境变量读取,不要硬编码)
# $env:DEEPSEEK_API_KEY = "sk-你的密钥" # 取消注释并填入,或提前在系统变量里设置
# 3. 启动 bridge(后台运行)
$bridgePath = "$env:USERPROFILE\Desktop\codex-bridge" # 改为你的实际路径
Start-Process -FilePath "node" -ArgumentList "proxy.mjs" -WorkingDirectory $bridgePath -WindowStyle Minimized
# 4. 等待 bridge 启动
Start-Sleep -Seconds 2
# 5. 验证 bridge 可用
try {
$testBody = '{"model":"deepseek-v4-pro","input":[{"type":"message","role":"user","content":"ping"}],"max_output_tokens":5}'
$response = Invoke-RestMethod -Uri "http://127.0.0.1:4000/v1/responses" -Method Post -Body $testBody -ContentType "application/json" -TimeoutSec 10
Write-Host "✅ Bridge 启动成功,DeepSeek 连接正常" -ForegroundColor Green
} catch {
Write-Host "❌ Bridge 或 DeepSeek 连接异常,请检查" -ForegroundColor Red
Write-Host $_.Exception.Message
exit 1
}
# 6. 启动 Codex
codex
快速排错对照表
| 现象 | 最可能的原因 | 检查方向 |
|---|---|---|
stream disconnected 且任务未完成 |
SSE heartbeat 缺失 / upstream timeout | bridge 有无 keep-alive 逻辑;DeepSeek 请求超时设置 |
401 Unauthorized,key 明明是对的 |
Clash 劫持回环流量,header 被处理 | 检查 NO_PROXY;临时关闭 Clash TUN 模式测试 |
404 Not Found 在 /v1/responses |
没有 bridge,直接打 DeepSeek | base_url 是否指向本地 bridge 而非 DeepSeek 官方 |
| Codex 实际用的还是 OpenAI 模型 | config.toml 没生效 | wire_api 字段位置;provider 名称大小写 |
| 第一轮推理好,后续变差 | reasoning_content 多轮丢失 | bridge 是否支持 previous_response_id 缓存 |
Model metadata not found 警告 |
DeepSeek 不返回 OpenAI 格式的 model 元数据 | 这是正常警告,不影响使用,可忽略 |
| PowerShell 变量设置后无效 | 大小写问题,部分工具读 no_proxy(小写) |
同时设置 NO_PROXY 和 no_proxy |
推荐使用的 Bridge 实现
不建议自己从零写 bridge,直接用已经验证的版本:
Zero-dependency,单文件 proxy.mjs,不需要 npm install,处理了:
- Responses API ↔ Chat Completions 双向翻译
- SSE streaming bridge
- reasoning_content 多轮缓存
- 多 provider 路由(DeepSeek / MiMo / OpenAI)
- per-provider reasoning effort 映射
启动方式:
bash
# 克隆
git clone https://github.com/wujfeng712-ui/codex-bridge.git
cd codex-bridge
cp env.example .env
# 编辑 .env,填入 DEEPSEEK_API_KEY
# 启动(加上 NO_PROXY)
$env:NO_PROXY="127.0.0.1,localhost,::1"; $env:no_proxy="127.0.0.1,localhost,::1"; node proxy.mjs
Node.js 版本要求 20+ (
--env-fileflag 在 Node 18/19 上不可用)
成本参考
这套方案把 Codex CLI 的后端从 GPT-5 换成 DeepSeek V4 Pro:
| 项目 | 费用 |
|---|---|
| Codex CLI 本体 | 免费(Apache 2.0 开源) |
| DeepSeek V4 Pro API | 约 $0.27/M input tokens(2026 年 4 月降价后) |
| codex-bridge | 免费开源 |
| vs. GPT-5 直接使用 | 降低约 30-50 倍成本 |
已知局限
- Codex 内置
web_search工具不可用:这个工具在 OpenAI 服务端,bridge 无法模拟 - 图片输入受限:DeepSeek V4 Pro 的视觉能力与 GPT-5 有差异
- 协议可能变化:Codex CLI 正从 TypeScript 迁移到 Rust,未来版本的 Responses API 格式可能调整,bridge 需要跟进更新
相关资源
- wujfeng712/codex-bridge --- 推荐的 bridge 实现
- deepseek-ai/awesome-deepseek-agent --- DeepSeek 官方 Codex 集成文档(Moon Bridge 方案,Mac/Linux 为主)
- CC Switch Issue #2553 --- DeepSeek provider 直连 404 的官方确认
- Codex CLI 官方 GitHub --- 最新版本和 changelog
如果这篇文章帮你省了时间,欢迎 Star 或转发。有新的坑欢迎在评论区补充。