
最近我在做一个小项目需要把 OpenAI Codex CLI 的请求统一走到自己开发的“OpenAI 兼容网关”上做日志审计。一切配置好之后codex启动却直接炸了——报错信息写着cc switch local proxy failed while handling codex endpoint /responses我在终端里盯着这行错误看了很久第一反应是网关没起端口不对权限不够但反复确认后全都没问题。后来我把 Codex 的源码翻了一遍定位到这是官方代码里一处 URL 拼接分支的错误属于典型的“官方 bug”。我顺手写了一个开源小工具专门诊断和修复这类自定义 provider 的配置路径问题。这篇文章把完整的排查过程、根因分析、工具实现思路和避坑清单都放出来给同样在用 Codex CLI 做自定义 API 端点、本地网关对接、或者想自己给 Codex 提 PR 的人参考。不管基础深浅照着这篇的思路你也能把这类“疑似网络问题”的 CLI 报错一步步拆到代码层面。1. 先说定位这个bug到底错在哪1.1 bug现场完整还原我先把复现条件交代清楚。我的环境是 Ubuntu 24.04 LTSCodex CLI 用的是当时官方最新版本机在127.0.0.1:8080起了一个自研的 OpenAI 兼容网关只实现了/v1/responses和/v1/chat/completions两个端点用于记录所有 AI 请求的入参和出参。模型名、端口这些都不是特殊配置属于非常常见的自建网关场景。Codex 的自定义 provider 配置写在家目录的~/.codex/config.toml里大致长这样model gpt-5 # 替换成你实际使用的模型名 model_provider local-gateway [model_providers.local-gateway] name local-gateway base_url http://127.0.0.1:8080 wire_api responses上报配置完成之后运行codex输入第一句提示词终端大概安静几秒到十几秒然后直接抛出一段错误Error: cc switch local proxy failed while handling codex endpoint /responses. provider: local-gateway这个错误最迷惑人的地方在于它把责任甩给了 “local proxy”字面意思是“本地代理失败”。我第一反应是先检查网关是不是没起来用curl http://127.0.0.1:8080/v1/responses测了一下发现返回了正常的鉴权错误说明网关是活着的。然后又检查了端口绑定、证书、环境变量全都没问题。更奇怪的是我把wire_api从responses改成chat同样的 base_url同样的网关居然能正常跑通。也就是说问题不是“网络不通”也不是“网关坏了”而是和responses这个协议端点强相关。到这里基本可以确定不是本地环境的问题而是 Codex 官方代码在处理自定义 provider 时出现了缺陷。1.2 从报错文本本身拆线索吃瓜要吃到根上。我先把报错文本逐词拆开看这段错误信息其实给了三个关键线索cc switch这个“cc”不是某个未知组件而是 Codex 内部负责配置切换的一段逻辑从源码路径来看就是 config switch 相关模块。local proxy failed这里的 proxy 指的不是传统意义上的网络代理而是“本地服务端点”。官方在报错文案里直接用了 proxy 这个词非常容易把用户带偏到网络排查上去。while handling codex endpoint /responses这是整句话里最值钱的线索——Codex 在启动或对话前会先向某个地址发起/responses请求而这步失败了。综合之后可以断定这是 Codex 在初始化自定义 provider 时向/responses端点发出请求但没能正确到达最后统一包装成了 “local proxy failed”。也就是说问题发生在请求被真正发出之前或之中的某个环节不是目标服务的问题。我顺手把这个报错在社区讨论区搜了一圈发现确实有人遇到过同类问题但大多数人提的 workaround 都是“别用自定义 provider”或者“把 wire_api 改成 chat”。这些办法能绕过问题但没有解决根本。作为长期用 Codex 做二次开发的人不想每次都用 workaround 续命就决定自己从源码层面挖到底。1.3 影响范围与初步定级把现象总结一下这次 bug 的影响面其实不小。凡是走“自定义 provider responses 协议”路线的用户基本都会中招。常见的使用场景包括企业内部的 AI 网关、带了审计或限流功能的中间层、自建隐私环境、以及像我这样为了日志分析做本地转发的开发者。问题的级别我定为“功能性缺陷”而不是“安全事件”它不泄露数据也不破坏配置但会让依赖自定义 provider 的整个工作流瘫痪而且报错信息误导性强消耗排查时间。对团队协作来说这甚至可能让一整天的工作阻塞完全不值得为了绕开它去改掉自己的架构设计。2. 一步步排查如何把锅从“网络”甩回“代码”2.1 排查工具选型我为什么选“echo服务 源码断点”排查这类 CLI 报错最忌讳的就是瞎改配置。我建议按照“抓请求 → 看路径 → 读源码”的顺序来而不是反过来乱试。先说抓请求。Codex 是 Rust 写的直接开RUST_LOGdebug输出的日志对协议细节覆盖不够我要的是“它到底往哪个 URL 发了什么请求”。最简单的办法并不需要 Wireshark 这类重型工具在本地起一个 echo 服务监听 8080 端口把收到的所有请求的 method、path、headers 原样打出来。这样 Codex 一发请求就能在 echo 服务这边看到原始 URL 和请求内容完全不用去猜。# echo_server.py一个用于观察请求的极简HTTP服务 from http.server import BaseHTTPRequestHandler, HTTPServer import sys class Handler(BaseHTTPRequestHandler): def do_GET(self): self._log_request(GET) self.send_response(204) self.end_headers() def do_POST(self): length int(self.headers.get(Content-Length, 0)) body self.rfile.read(length)[:200] self._log_request(POST, body) self.send_response(204) self.end_headers() def _log_request(self, method, bodyb): sys.stdout.write( %s %s \n % (method, self.path)) sys.stdout.write(Headers: %r\n % dict(self.headers.items())) sys.stdout.write(Body-head: %r\n\n % body) sys.stdout.flush() if __name__ __main__: HTTPServer((127.0.0.1, 8080), Handler).serve_forever()然后把 Codex 的配置简化到只保留一个 provider排除掉多 provider 互相影响的可能再跑一次codex。echo 服务给出的结果非常有价值它收到的请求路径是POST /responses而不是我在网关里实现的POST /v1/responses。这一下就锁定了问题方向Codex 在构造自定义 provider 的请求路径时直接把 base_url 替换成了http://127.0.0.1:8080然后在这个基础上追加了/responses但漏掉了标准 endpoint 里的/v1前缀。也就是说它实际请求的是http://127.0.0.1:8080/responses而正确地址应该是http://127.0.0.1:8080/v1/responses。2.2 对照组试验排除干扰项为了确认这不是网关实现的问题我做了一组对照试验结果如下表配置组合请求实际路径是否成功自定义 provider wire_apiresponses/responses失败自定义 provider wire_apichat/v1/chat/completions成功官方 provider wire_apiresponses/v1/responses成功自定义 provider base_url 手动写成 /v1 结尾/v1/responses成功这个表格直接破案只要 base_url 末尾不带/v1responses 协议就会缺前缀但 chat 协议又神奇地没问题。原因其实很好理解Codex 在构造请求时对 chat 协议走了另一个分支那个分支里做了/v1的拼接而 responses 协议的分支里没有。同一套环境里只有“自定义 provider responses”这个组合恰好落入了有缺陷的分支。看到这个逻辑之后手头其实已经有一条立竿见影的临时绕法了把配置里的base_url从http://127.0.0.1:8080改成http://127.0.0.1:8080/v1再运行 Codex立即正常。这个绕法我本地验证了好几轮完全稳定。但绕法只是能干活谈不上“修好了 bug”。Codex 自带更新机制升级之后配置文件还是那套我总不能每次升级都祈祷官方改掉这个分支。要想让所有同类用户受益只能在源码层面找到那行代码把修复方案做成一个可持续使用的工具或补丁。2.3 源码定位罪魁祸首是一处 URL 拼接分支Codex CLI 本身是开源的直接在 GitHub 仓库里搜responses和base_url相关逻辑。顺着wire_api的匹配点找到了构造请求地址的关键函数大致逻辑如下简化后便于理解# 根据 Codex 源码还原出的伪代码用于展示根因 # 已知变量provider_config, wire_api if provider_config.name openai or provider_config.name openai-custom: # 官方 provider 走这里host 和 path 都写死自然没问题 request_url https://api.openai.com/v1/ wire_api else: # 自定义 provider 走这里 # 注意base_url 直接拼上 wire_api没有自动补 /v1 request_url provider_config.base_url / wire_api问题就在 else 分支里base_url / wire_api这个写法把完整路径交给用户来控制。Base URL 的标准格式应该是http://host:port/v1这样拼出来才是http://host:port/v1/responses。但很多用户包括我都习惯把base_url写成服务根地址http://host:port因为之前用 chat 协议时它都能正确补前缀。于是 responses 协议下就把/v1弄丢了。为什么官方自测没发现我猜是官方测试矩阵里自定义 provider 只覆盖了 chat 协议的用例responses 自定义 base_url 的组合没在回归测试里。这属于典型的“测试盲区”也说明这类 CLI 工具的配置路径组合比表面看起来要复杂得多。确认官方仓库最新代码里依然存在这个分支问题之后我决定按“修复工具 官方 PR”两条线走。PR 慢慢等工具先上线救火。3. 修复方案落地从“改一行配置”到“开源小工具”3.1 修复思路对比绕法、改源码、外置工具怎么选面对这个 bug当时有三条路线摆在面前手动改配置把 base_url 末尾补上/v1。零成本但对所有踩坑的人都要重复同样操作且没有解决上游代码缺陷。本地改源码重新编译能彻底修可 Codex CLI 升级后就失效还要维护 fork成本太高。做一个外置诊断/修复工具不改 Codex 本体通过扫描配置文件识别缺陷组合自动修正 base_url再跑冒烟测试验证。既兼容新旧版本又可以做成开源项目让社区一起用。我选了第三条。核心原因很实在它能立刻让所有受影响的人无痛解决问题而不需要等官方发布修复、也不需要用户学会改 TOML 语法。工具的作用是“把坏事拦在门外”就算官方以后修好了这工具在旧版本上也有存在价值。3.2 工具架构与核心实现我给它取了个朴素的名字codex-provider-fix用 Python 3 写的。选 Python 而不是 Shell 的原因是 TOML 解析、错误处理、跨平台路径操作都更顺手分发给开发者用也几乎没有门槛。工具的核心流程分五步读取~/.codex/config.toml用tomllibPython 3.11 内置解析所有model_providers。遍历每个自定义 provider检查wire_api和base_url的组合如果wire_api是responses且base_url不以/v1结尾就判定为需要修复。自动修正在 base_url 末尾补上/v1同时保留原配置备份到config.toml.bak。修改后启动一个内置的本地冒烟测试——直接向修正后的地址发一个最小请求检查链路是否已经打通。提供--rollback参数一行命令恢复备份让不喜欢自动修改的人随时退回原状。关键代码片段如下。这里有个非常容易踩的坑Python 的tomllib只负责读不能写回 TOML 文件所以需要额外装一个tomli_w很多新手在这里被卡住。import tomllib import pathlib import shutil import tomli_w def fix_provider_base_url(toml_path: pathlib.Path) - int: with open(toml_path, rb) as f: data tomllib.load(f) fixed 0 providers data.get(model_providers, {}) for name, prov in providers.items(): if prov.get(wire_api) ! responses: continue base_url prov.get(base_url, ) if base_url and not base_url.rstrip(/).endswith(/v1): prov[base_url] base_url.rstrip(/) /v1 fixed 1 if fixed: shutil.copy2(toml_path, str(toml_path) .bak) with open(toml_path, wb) as f: tomli_w.dump(data, f) return fixed这里有一个值得说的细节修正时我特意用base_url.rstrip(/) /v1而不是简单的字符串追加。因为如果用户原本写了http://127.0.0.1:8080/直接追加会变成http://127.0.0.1:8080//v1有些网关框架会正常处理有些则会路由失败。这种“看似无关紧要的斜杠”恰恰是配置类 bug 最常见的隐藏来源。3.3 冒烟测试让修复结果可验证工具里我加入了一个轻量的冒烟测试模块向修复后的 base_url 发一个 POST 请求到/v1/responses路径请求体只是最简结构不传真实业务数据。判定逻辑很简单如果响应是4xx说明服务端处理了请求链路是通的。如果响应是5xx或直接拒绝连接说明修复方向不对可能是网关本身没有实现该端点。如果响应是2xx说明你的自定义网关刚好也实现了对该端点的完整处理这就是最理想的状态。这个设计把“配置是否正确”和“后端是否支持”两件事分开了。很多用户一看到 5xx 就以为是工具改错了其实不是工具只负责让 Codex 发出的请求走到标准路径上后端支不支持是另一回事。我在 README 里专门用加粗字写了这条说明实测下来能减少大半误解。3.4 开源发布与后续反馈工具我发布在 GitHub 上用的 MIT 协议README 里包含bug 背景、原理说明、一键使用方式、回滚方式、以及完整的伴生测试脚本。发布之后我把修好的逻辑还提给了官方一个 PR核心改动就是给 responses 分支补上/v1前缀改动量很小但问题很典型。后续有一些使用者反馈大多集中在“Windows 路径问题”和“网关不支持/v1前缀”这两类我都加进了 FAQ。最有意思的一个反馈是有人把 base_url 指向了一个仅支持 chat 协议的本地服务但wire_api却写了responses我的工具不仅会补/v1还会给出一个提示告诉用户两种协议不匹配。这算是工具超出预期的价值——它不只是补丁还是一个 provider 配置的“体检表”。4. 常见问题与排查技巧实录4.1 用了工具还是报错先看这几个检查点折腾过一段时间后我总结了一份排查清单先讲最常见的原因。第一确认网关真的在监听你配置的端口。很多人以为“服务没报错”就是启动了其实后端可能根本没监听或者监听在 IPv6 的::1而不是 IPv4 的127.0.0.1。排查命令很直接ss -lntp | grep 8080第二检查环境变量是否覆盖了配置文件。Codex 读取配置的优先级里环境变量高于配置文件如果你设置过OPENAI_BASE_URL或者 Codex 专用的 base URL 变量那么即使配置文件里写的地址是对的实际生效的也可能是环境变量。我遇到过不止一次用户排查半天最后发现是 shell profile 里一个残留变量在作祟。第三检查 wire_api 和网关实现是否匹配。你的网关如果只实现了 chat 协议那就老老实实用wire_api chat强行用 responses 协议一定会失败这不是 Codex 的 bug是配置和部署不匹配。第四检查自定义证书和鉴权头。如果你的网关用了自签名证书或者要求额外的Authorization头Codex 的 provider 配置里必须显式声明相关字段否则请求会在 TLS 握手阶段就失败代码层看到的也只有一句笼统的 “proxy failed”很容易被带偏。4.2 手动排查速查表如果没有我的工具或者你在排查其他类似 CLI 工具下面这张速查表可以直接照用现象可能原因验证手段解法报错含 “local proxy failed”但网关正常URL 路径拼接缺陷echo 服务观察请求路径base_url 补 /v1连接拒绝网关未启动或端口不对ss / netstat启动网关或改端口TLS 握手失败自签名证书未被信任抓包或服务端日志配置证书相关字段请求通了但一直 401/403鉴权头缺失网关日志补充 API Key 配置chat 协议正常responses 异常网关只实现了 chat文档核对换 wire_api 或升级网关这张表的核心思想是每一个“现象”都要对应一个明确的“验证手段”不要凭感觉去猜。CLI 工具报错时最怕的多半不是问题本身而是被错误文案带偏方向。4.3 给开源贡献者的几点经验最后这部分写给想给 Codex 或其他开源 CLI 工具提 PR、做周边工具的人。第一提 issue 前先最小复现。我的习惯是把配置文件缩到最小、把环境变量清空、把影响面隔离到单一变量。如果一个现象只能用一堆复杂配置才能复现那说明你自己还没定位清楚官方也无法快速响应。第二排查时保留证据。echo 服务打出的请求路径、对照组试验表、最小复现仓库这些都要留住既方便自己复盘也能让维护者一眼看明白问题在哪。我在提 PR 时附上了 echo 服务的输出截图和对照组结果维护者理解问题几乎没花时间。第三不要指望官方立刻修复。开源项目再活跃一个边缘配置的 bug 也排不进高优先级队列。外置修复工具的价值就在于“不等上游”先让生态里的用户不卡住。等官方修复发布后这类工具还可以转型成配置校验器继续发挥余热。我个人在这次踩坑里最大的体会是命令行工具的报错文案真的只是“给你一个排查起点”而不是“问题的结论”。“local proxy failed” 这六个字一度把我引向网络排查的死胡同但真正的问题藏在一行 URL 拼接代码里。以后我遇到任何类似的“连接不上”“代理失败”报错都会先起一个 echo 服务器之类的东西看一眼工具到底在请求什么地址再谈后面的网络问题。这个习惯帮我省下的时间远比当时写修复工具花的那个周末多。我也建议每个深度使用 Codex CLI 或其他 AI 编码工具的开发者都花几分钟看看自己的 provider 配置确认 base_url、wire_api、环境变量这些“小东西”是否自洽。很多时候让你卡住一天的不是模型能力不行而是配置声明的路径和代码逻辑里的路径差了那么一个/v1。