ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Windows下Codex接入DeepSeek:本地适配层与可观测路由搭建指南

2026/9/6 11:35:24 拓冰建站 浏览量
Windows下Codex接入DeepSeek:本地适配层与可观测路由搭建指南 在 Windows 上把 Codex 真正跑通并且接上国内模型服务其实比大多数教程描述的要绕。很多人装完 Codex 后卡在同一个地方官方默认模型用不上想接入 DeepSeek 这类服务时又不知道该怎么配置模型目录、怎么查看请求到底发给谁了。我这次在 Windows 下折腾了一圈最后搭了一个本地模型适配层用“模型目录 可观测路由”的方式把 Codex 彻底盘活了。这篇文章就是这次搭建的完整记录适合那些已经在 Windows 上装了 Codex、希望换成国内模型并想看清楚每一次请求细节的开发者。1. 为什么 Windows 下的 Codex 需要一层“模型适配层”1.1 官方 Codex 与自定义模型之间的落差Codex 本身是个很吃模型能力的工具它默认连接的是官方模型服务配置界面和交互流程都围绕默认模型设计。一旦你想把它切换到国内模型第一个问题就是Codex 官方定义的模型列表里并没有你要接的模型。如果你硬把 model 改成deepseek-chat它不一定认识就算配置了 providerCodex 发给上游的请求路径、鉴权方式也和国家模型服务商的约定有明显差异。我之前直接尝试把 base_url 指到 DeepSeek结果遇到一堆问题要么是请求打到/responses端点上对方根本不支持要么是模型名没对上返回“model not supported”要么是请求发出了但完全不知道到底用了哪个模型、消耗了多少 token。这种“黑盒”状态在本地开发时还能忍一旦你要做多个模型切换、成本分析、问题复盘就完全不够了。于是自然想到在 Codex 和真实模型服务之间加一层适配层用本地服务统一收纳模型配置并提供所有请求的可见性。1.2 “模型目录”到底指什么很多人在网上搜“模型目录”搜出来的大多是 ChatGPT 的模型列表但在 Codex 的语境里它更像一个“可用模型登记表”。Codex 配置文件中有两个关键位置一个是最顶层的model它决定当前会话用哪个模型另一个是model_providers它定义这个模型从哪里来、走什么协议、用哪个 API Key。当我们引入适配层之后“模型目录”可以再扩展一层适配层自己维护一张路由表把 Codex 传过来的模型名映射到真实的模型服务商。比如 Codex 请求里写的是deepseek-chat适配层查到目录里对应的目标地址是 DeepSeek 的接口就把请求转发过去。这样用户只需要在 Codex 配置文件里改一个模型名剩下的真实 URL、Key、模型别名全部由适配层的目录负责。1.3 “可观测路由”解决什么痛点路由这件事本身不复杂复杂的是“路由之后发生了什么”完全不可见。可观测路由的核心目标是把“请求来了 → 路由到哪 → 上游返回什么 → 花了多久 → 消耗了多少 token”这条链路变成可以事后查阅的记录。没有观测层的 Codex 接入方式里遇到报错只能靠猜。而有了适配层之后我可以直接在日志里看到modeldeepseek-chat providerdeepseek status200 duration1843ms prompt_tokens812 completion_tokens231这种数据对开发阶段的调试、上线后的成本控制都非常关键。尤其是当你在多个国内模型间来回切换时没有路由日志根本不知道某个请求到底走的是哪个上游出了问题也没法定位。1.4 一个适配层应该满足的三个指标经过一番折腾我把适配层的需求收敛成三条第一Codex 侧零改造。我不希望每次切换模型都要改 Codex 的复杂配置最好只改一行模型名甚至靠会话内命令切换。适配层必须把模型名和真实供应商解耦。第二日志自动沉淀。每次请求都要有记录包括时间、模型、状态、耗时、token 用量。记录最好是结构化的方便后续用脚本分析。第三稳定可常驻。适配层不能只在调试时跑它要能作为一个本地后台服务常驻开机自启不会因为一个请求异常就挂掉。2. 方案选型本地转发服务比你想的更轻量2.1 三种接入方式的对比我在动手之前先对比了三种接法。第一种是“直连”方案直接在 Codex 的model_providers里把 base_url 指向 DeepSeek 官方接口配好wire_api之后理论上也能跑。但缺点非常明显没有日志、没有统一模型名映射、换模型要改配置再重启 Codex而且一旦上游返回奇怪错误根本无从查起。第二种是“用现成网关”方案社区里有很多开源的 API 网关/聚合服务能统一管理多个模型商并做负载均衡、日志审计。这类工具功能很强但它把问题复杂化了。Windows 下要跑 Docker、要初始化数据库、要维护前端面板为了一个本地 Codex 接入场景太重了。第三种就是我最终采用的“本地轻量路由”方案用 Node 内置的 HTTP 模块写一个不到 100 行的转发服务监听本机端口接收 Codex 的请求然后按模型目录转发到真实的模型服务。它不引入任何额外依赖日志直接追加写入本地文件稳定且透明。三者的对比可以总结成一张表方案配置改动量日志能力模型切换成本Windows 适配难度直连中等无每次改配置重启低但不灵活开源网关高强通过面板操作高需要 Docker 等本地轻量路由低可控改 model 即可切换低适合本次场景2.2 为什么用 Node 内置模块而不是其他技术栈Windows 下做本地服务可选的技术栈很多但我最后坚持用 Node 内置的http模块加全局fetch不引入express也不铺设数据库。原因有三点第一Node 18 以上版本自带fetch可以直接把请求转发给上游不需要额外安装第三方请求库第二内置 http 模块足够处理 Codex 产生的请求量个人开发的并发量不会超过几百 QPS完全不需要引入复杂的 Web 框架第三内置模块意味着没有node_modules依赖拷贝到任何一台 Windows 机器上都能直接跑这对本地工具来说是非常重要的可移植性。2.3 目录结构设计适配层虽然小但目录结构还是值得认真设计这样后续扩展会非常顺手。我在 Windows 上放在C:\codex-route里面的结构是这样的C:\codex-route\ route-server.js model-catalog.json .env logs\ route.ndjsonroute-server.js是服务本体model-catalog.json是模型目录.env保存各种 API Keylogs\route.ndjson是所有请求的结构化日志。这里故意把服务逻辑、配置、日志分离后续加模型、看日志都不需要碰服务代码。3. Windows 环境准备与 Codex 安装3.1 Codex CLI 的安装方式如果你还没有装 Codex最直接的方式是用 npm 安装 CLI 版本。前提是 Windows 上已经有 Node.js 18 以上环境。装好 Node 后打开 PowerShell执行npm install -g openai/codex安装完成后执行codex --version正常情况下会输出版本号。这个 CLI 版本是本文方案的基础因为配置文件控制能力最完整。如果你安装的是 Codex 桌面版下面的配置思路同样适用只不过桌面版的入口位置可能隐藏在设置里建议先回到 CLI 跑通后再开桌面版。3.2 桌面版安装的特殊关注点热词里频繁出现“Codex 桌面版 Windows”这说明很多人确实在尝试桌面版。桌面版的好处是界面友好但它默认配置的覆盖优先级、模型选择交互和 CLI 不太一致有时会出现“CLI 能跑桌面版连不上本地服务”的情况。如果你打算用桌面版安装后先在设置里找到“自定义模型 / Provider”之类的入口确认能手动填model和base_url。部分版本对自定义 provider 的支持并不完整这也是为什么我推荐先以 CLI 为主桌面版作为辅助预览工具。3.3 环境变量的准备与验证适配层需要保存各个模型服务的真实 API KeyWindows 下最标准的做法是配置用户级环境变量。在“系统属性”的“高级 → 环境变量”中新增用户变量DEEPSEEK_API_KEY把对应的 Key 填进去。这里有一个 Windows 特别容易踩的坑改完环境变量之后已经打开的 PowerShell、Codex 进程并不会自动读取新值必须重新打开终端。验证方式也很简单echo $env:DEEPSEEK_API_KEY如果输出为空就是环境变量还没生效或者窗口没重开。这个小小的步骤能省下后面不少排查时间。3.4 生成默认配置文件Codex 安装后首次运行会在用户目录下生成配置文件路径通常是C:\Users\你的用户名\.codex\config.toml。如果这个文件不存在可以先运行一次codex按提示随便操作一下再 CtrlC 退出配置文件就会生成。后续我们的所有 Codex 侧改动都围绕这个文件展开。在编辑之前建议先备份一份默认配置因为 Codex 升级时有时会校验配置字段少个括号都可能导致无法启动。4. 把模型目录落实成两张表4.1 Codex 侧的 Provider 配置适配层的设计里Codex 只需要知道一个“虚拟模型”存在剩下的交给适配层处理。所以config.toml写得非常简单model deepseek-chat model_providers { codex-route { name Codex Route base_url http://127.0.0.1:9877/v1 env_key CODEX_ROUTE_KEY wire_api chat } }这里的核心是wire_api chat。Codex 早期很多问题都出在默认走/responses接口而国内模型普遍只支持/chat/completions。设置成chat之后Codex 会以 OpenAI Chat Completions 的协议格式向base_url发请求后面适配层对接上游就顺畅很多。env_key我随便填了一个占位的CODEX_ROUTE_KEY因为适配层并不校验这个 KeyCodex 只是需要一个环境变量来生成 Authorization 头。你需要在环境变量里也加一个CODEX_ROUTE_KEY值随便填。4.2 适配层的模型目录 JSON在model-catalog.json里我维护了一张真正的模型目录{ deepseek-chat: { provider: deepseek, upstream: https://api.deepseek.com/v1/chat/completions, upstreamModel: deepseek-chat, envKey: DEEPSEEK_API_KEY }, deepseek-reasoner: { provider: deepseek, upstream: https://api.deepseek.com/v1/chat/completions, upstreamModel: deepseek-reasoner, envKey: DEEPSEEK_API_KEY } }这个文件就是适配层的“模型目录”。Codex 传过来的模型名作为 JSON 的 key目录里存储真实的目标地址、真实模型名、以及使用哪个环境变量里的 API Key。4.3 为什么要把模型名拆成两层映射可能有人会问为什么 Codex 侧的模型名和上游的真实模型名要分开直接用同一个名字不行吗分开的好处是灵活性。Codex 传给适配层的模型名可以是你自己定的别名而不是上游厂商真正识别的名字。比如你把 Codex 里的虚拟模型名字叫my-v3目录里指向deepseek-chat这样你以后想换一个模型只需要改model-catalog.json里对应条目指向新地址Codex 的配置文件完全不用动。这就是适配层存在的最大意义模型目录把“客户端可见名”和“供应链真实名”解耦了。5. 用不到 100 行代码实现可观测路由5.1 先看服务主体代码下面这段是整个适配层的核心我把它简化到最直觉的写法方便逐行看明白const http require(http); const fs require(fs); const path require(path); const catalog require(./model-catalog.json); const PORT 9877; const LOG_DIR path.join(__dirname, logs); const LOG_FILE path.join(LOG_DIR, route.ndjson); function logRecord(record) { fs.mkdirSync(LOG_DIR, { recursive: true }); fs.appendFileSync(LOG_FILE, JSON.stringify(record) \n); } function handleError(res, status, message) { res.writeHead(status, { Content-Type: application/json }); res.end(JSON.stringify({ error: { message } })); } const server http.createServer(async (req, res) { if (req.method ! POST || req.url ! /v1/chat/completions) { res.writeHead(200); res.end(ok); return; } let raw ; for await (const chunk of req) raw chunk; let requestBody; try { requestBody JSON.parse(raw); } catch (e) { return handleError(res, 400, invalid json body); } const modelName requestBody.model; const route catalog[modelName]; const startedAt Date.now(); if (!route) { logRecord({ ts: new Date().toISOString(), model: modelName, provider: unknown, status: 404, duration: Date.now() - startedAt }); return handleError(res, 404, unknown model: ${modelName}); } const upstreamBody { ...requestBody, model: route.upstreamModel || modelName }; const upstreamResp await fetch(route.upstream, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env[route.envKey] || } }, body: JSON.stringify(upstreamBody) }); res.writeHead(upstreamResp.status, { Content-Type: upstreamResp.headers.get(Content-Type) || application/json }); const chunks []; const reader upstreamResp.body.getReader(); while (true) { const { done, value } await reader.read(); if (done) break; chunks.push(value); res.write(value); } res.end(); const responseText Buffer.concat(chunks).toString(utf8); logRecord({ ts: new Date().toISOString(), model: modelName, provider: route.provider, status: upstreamResp.status, duration: Date.now() - startedAt, responseBytes: responseText.length }); }); server.listen(PORT, 127.0.0.1, () { console.log([codex-route] listening on http://127.0.0.1:${PORT}); });这个服务做了四件事接收 Codex 发来的 POST 请求、根据请求体里的 model 字段查模型目录、把请求转发到目录里指定的真实上游、把上游响应原样返回给 Codex。整个转发过程不修改消息内容只替换了模型名因此协议保持兼容。5.2 为什么可以直接透传流式响应Codex 在真实对话中通常会开启流式输出上游模型服务返回的是 SSE 格式的数据块。上面的代码没有特别处理 SSE但也没有破坏它因为代码只是把上游的字节流逐块转发给了 Codex 客户端。在 Node 的 fetch 实现里upstreamResp.body.getReader()读取的每一块都是原始字节res.write(value)不做任何解析直接写回所以流式语义天然被保留。重要的是不要在这个过程中对数据做 JSON.parse否则 SSE 多行数据会被打散。这也是我推荐用 Node 内置 http 模块的原因它对字节流的控制是最直接的。5.3 日志为什么用 JSON Lines 而不是单独的数据库可观测的值体现在日志里。我的选择是把每条请求记录追加到一个.ndjson文件里也就是每一行都是一个独立 JSON 对象。这么做的好处是 Windows 下查看非常方便。想看最新五条请求打开 PowerShell 执行Get-Content C:\codex-route\logs\route.ndjson -Tail 5 | ConvertFrom-Json输出会以表格形式展示每条记录的字段比如 model、provider、status、duration。如果用逗号分隔的 CSV解析带引号和转义的字段会很麻烦用数据库在这个场景下又过于笨重。JSON Lines 是在本地方案里平衡可读性和可处理性的最佳格式。5.4 让适配层常驻后台开发调试时可以开一个终端跑node route-server.js。但适配层要长期用就不能依赖手动打开窗口。Windows 下最简单的是创建计划任务schtasks /Create /TN codex-route /TR C:\Program Files\nodejs\node.exe C:\codex-route\route-server.js /SC ONSTART /RL HIGHEST /F这样开机后适配层会自动跑起来。如果临时想停止用schtasks /End /TN codex-route然后taskkill /IM node.exe /F结束残留进程。注意后一条命令会杀掉本机所有 Node 进程如果你还有别的 Node 服务在跑慎用。6. 联调从 Codex 发请求到看到路由日志6.1 先用 curl 做一次本地冒烟测试在改 Codex 配置之前先用curl.exe直接请求一次适配层确认路由本身没问题。在 PowerShell 里执行curl.exe http://127.0.0.1:9877/v1/chat/completions -H Content-Type: application/json -H Authorization: Bearer test -d {\model\:\deepseek-chat\,\messages\:[{\role\:\user\,\content\:\hello\}]}PowerShell 的转义规则比较麻烦不方便的时候可以把请求体单独写到一个 JSON 文件里再通过-d body.json读取。如果适配层配置正常你会收到来自 DeepSeek 的回复内容。如果这里就报错先检查环境变量DEEPSEEK_API_KEY是否在运行 Node 的终端里能读到确认没问题再看目录 JSON 里的 upstream 地址是不是写对了。6.2 回到 Codex 发起一次真实对话冒烟测试成功后直接运行codex命令行开始一次对话。正常情况下Codex 会把请求发到http://127.0.0.1:9877/v1/chat/completions适配层收到之后查目录转发给上游把响应接回来。此刻适配层终端窗口会打印一行启动日志logs\route.ndjson里也会写入一条记录。如果 Codex 一直转圈、没有输出最可能就是 Codex 侧配置没生效。重新看一眼config.toml里的model_providers是否写入了wire_api chat。很多网上旧教程会漏掉这个字段导致 Codex 默认发到/responses然后被适配层直接挡掉。6.3 切换模型看路由变化适配层最爽的时刻就是切换模型。比如把config.toml里的顶层 model 改成model deepseek-reasoner重启 Codex 后再问一次问题。打开日志Get-Content C:\codex-route\logs\route.ndjson -Tail 3你会发现新记录的 model 字段已经变成deepseek-reasonerprovider 仍然是deepseek。如果以后你想接入 GLM、通义千问或者 Kimi只需要在model-catalog.json里新增一个条目然后在 Codex 的 model 字段填对应的 key 即可Codex 侧完全不需要理解目标模型长什么样。6.4 日志字段的解读每一条日志记录都包含了几个关键字段字段含义作用ts请求完成时间分析时段分布modelCodex 传过来的模型名判断当前会话用的哪个模型provider上游厂商标识判断请求转发到了哪里status上游 HTTP 状态码快速定位失败请求duration从收到请求到响应结束的耗时排查性能瓶颈responseBytes响应体字节数粗略估算数据量如果你想统计一次会话消耗了多少 token可以后续在日志解析时把上游响应的usage字段提取出来追加到日志里。这一步我会放在后续完善方案里做属于锦上添花。7. 常见问题与排查实录7.1 请求打到 /responses 导致的“endpoint /responses”错误很多人在网上搜过一个报错关键字大概意思是cc switch local proxy failed while handling codex endpoint /responses。这个问题几乎都是同一个根源Codex 在某个环节仍然使用了默认的 Responses 协议而本地适配层只实现了 chat completions或者上游模型服务不支持/responses。排查顺序是先看 config.toml 里是否所有 provider 都写了wire_api chat再确认跑着的适配层版本是最新的没有旧 config 缓存在内存里最后看适配层日志里是否出现过POST /v1/chat/completions记录如果连记录都没有说明 Codex 的请求根本没到适配层。7.2 模型不支持报错在 Codex 接入国内模型的社群讨论里经常能看到类似model not supported when using codex with a...的报错。这个报错表面上看着像 Codex 不认这个模型其实大多数时候是模型目录的 key 没对上。比如你在config.toml里写了model deepseek-chat但model-catalog.json里的 key 写的是DeepSeek-Chat。大小写不一致都会导致适配层查不到目录返回 404Codex 把 404 包装成“model not supported”。解决办法是保持两个文件里模型名完全一致最好把 JSON 里的 key 统一用小写加中划线。7.3 环境变量改了但日志显示空 Key这个问题在 Windows 上非常典型你在系统设置里加了DEEPSEEK_API_KEY然后开了一个新的 PowerShell 窗口echo $env:DEEPSEEK_API_KEY也能正常输出但适配层记录的上游响应是 401。原因通常是适配层是通过计划任务或老终端启动的进程的环境变量还是旧快照。Windows 下进程一旦启动环境变量就是固定的不会实时跟进系统设置。杀掉 Node 进程重新启动适配层即可。如果是计划任务自启需要先手动停止任务再启动一次。7.4 端口被占用本地端口冲突也常见。检查端口的命令是netstat -ano | findstr :9877如果发现端口被别的进程占了要么换一个端口并同步修改config.toml里的 base_url要么杀掉占用进程。记得改完端口后Codex 和适配层两边的地址都要改少一边就连接不上。7.5 Codex 桌面版连不上本地路由桌面版有时候并不读取命令行版本的config.toml或者有自己的覆盖层。如果你在 CLI 下一切正常但桌面版一直报连不上先去桌面版设置里找“自定义端点 / 模型供应商”相关的入口。不同版本这个入口的位置不一样。如果始终找不到先以 CLI 为准等桌面版支持自定义 provider 更完善再切换不然排查成本会很高。7.6 常见问题速查表现象可能原因处理建议无任何日志写入请求没到适配层检查 base_url 和端口、确认 Codex 重启401 错误API Key 为空或过期验证环境变量、重启 Node 进程404 unknown model模型目录缺少对应 key检查 JSON key 与 Codex model 字段一致性超时无响应上游地址不可达用 curl 直接测 upstream桌面版连不上桌面版覆盖配置使用 CLI 或找桌面版自定义 provider 入口流式对话卡死wire_api 配置错误确保wire_api chat最后说一点个人体会这套适配层我实际跑了几天最大的感受是把“路由”和“日志”放在模型接入的同一层带来的确定性远超预期。以前 Codex 报错我只能看到提示现在我能直接看到请求走了哪条路、在上游卡了多久很多问题一眼就能定位。而且模型目录的好处是长期显现的以后想接一个新的国内模型只需要往 JSON 里加一条记录Codex 那边永远不用动。如果你也在 Windows 上折腾 Codex建议先按这个思路把适配层搭起来哪怕你暂时只接一个模型有日志兜底后面的扩展也会轻松很多。