公司现在什么都讲究省钱,而且是每一分钱都要省。模型调用费当然也不能例外,所以我一直使用 Codex + DeepSeek。休了几天假回来,Codex 升级后却突然变成了每发一条消息就报错:
unknown tool type: namespace
模型一句话都没有返回,请求在推理开始前就被拒绝。更奇怪的是,同一份配置下:
- Codex Desktop 报错;
- 终端里的交互式
codex也报错; codex exec却能正常工作。
三个入口共用一份配置,结果却完全不同,说明网关的兼容情况还与客户端入口有关。
要解释这个差异,需要看版本、工具配置和不同入口实际发出的请求。
排除版本和工具数量
namespace 看起来像 Codex 新版本引入的工具类型,但 npm 安装的较老稳定版仍然报错,另一个看起来更新的内置 CLI 在特定入口下反而正常。因此,问题不能简单归因于版本升级。
清空 browser、computer-use 和 MCP server 等额外工具配置后,Desktop 入口仍然发送 namespace 并被网关拒绝,工具数量没有改变请求类型。
originator 决定工具形式
把几个入口发出的请求并排比较后,差异变得很清楚:
| 入口 | 观察到的 originator | 工具类型 | 第三方模型结果 |
|---|---|---|---|
codex exec | codex_exec | function | 正常 |
| TypeScript SDK | codex_sdk_ts | custom | 拒绝 custom |
Desktop / 交互式 codex | Codex Desktop | namespace | 拒绝 namespace |
终端中的交互式 codex 和 codex exec 走不同链路。前者走 app-server,在我的测试中使用了与 Desktop 相同的客户端身份,因此也会触发 namespace;后者是一次性执行入口,只发送标准 function。
在 SDK 场景中,设置内部环境变量:
CODEX_INTERNAL_ORIGINATOR_OVERRIDE=codex_exec
可以让请求采用 codex_exec 对应的工具形式。但这个办法对 Desktop 无效:GUI 通过 clientInfo.name 提供的身份优先级更高,环境变量无法覆盖。
由于这是内部变量,不应把它当作稳定公开配置依赖。升级后如果行为变化,仍需重新抓包确认。
请求体里实际发生了什么
我在本机启动了一个 mock server,把 Codex 的 base_url 临时指向它,记录实际请求体。在失败请求的 tools 数组中,我看到了多个 namespace 类型工具:其中包括 multi-agent 工具,也包括被打包成 namespace 的 MCP server。
将 originator 切换为 codex_exec 后,工具定义变为 function,同一个第三方模型可以接受请求。这个对照支持了上面的入口差异判断。
抓包时应只记录定位问题所需字段,并对 Authorization、Cookie、提示词和业务数据做脱敏;不要把完整请求日志直接发到公共 issue。
解决方案:在中间增加请求转换代理
Desktop 无法通过环境变量切换身份时,可以在本机增加一层代理:接收 Codex 请求,清理不兼容字段,再转发到公司网关。
代理对 tools 做了两项处理:
- 移除
type = "namespace"的工具; - 移除
web_search工具中网关不认识的external_web_access字段。
核心转换逻辑如下:
def transform_tools(tools):
"""处理第三方模型不兼容的工具定义,返回 (新工具列表, 是否修改)。"""
if not isinstance(tools, list):
return tools, False
changed = False
result = []
for tool in tools:
if not isinstance(tool, dict):
result.append(tool)
continue
tool_type = tool.get("type")
if tool_type == "namespace":
changed = True
if MODE == "flatten":
namespace = tool.get("name", "ns")
for child in tool.get("tools", []):
if isinstance(child, dict) and child.get("type") == "function":
child = dict(child)
child["name"] = f"{namespace}__{child.get('name', '')}"
result.append(child)
continue
if tool_type == "web_search" and "external_web_access" in tool:
tool = dict(tool)
tool.pop("external_web_access", None)
changed = True
result.append(tool)
return result, changed
strip 模式会直接丢弃 namespace 工具,实现简单,但被移除的工具在这一轮对话中不可用。
如果希望保留能力,可以把 namespace 内的 function 展开到顶层,并为工具名增加 namespace 前缀。完整实现需要同时改写请求和响应:模型返回工具调用后,代理还要把名称映射回原始结构,并正确处理流式响应、并发调用和错误结果。只做单向 flatten 很可能造成“模型会调用、客户端接不住”的新问题。
代理的运行配置与协议处理
不要把关闭 TLS 校验写成默认方案
我的内网网关使用自签名证书,初次转发时遇到了:
SSL: CERTIFICATE_VERIFY_FAILED
临时关闭校验可以用于验证链路,却不适合作为长期配置。更稳妥的办法是把公司的根证书加入代理使用的信任链,并显式指定 CA 文件。否则本地代理到网关之间失去服务端身份校验,会留下中间人攻击风险。
只监听本机地址
代理应绑定 127.0.0.1 或 Unix Domain Socket,不要默认监听 0.0.0.0。它承载模型密钥和完整请求内容,一旦暴露到局域网,风险远高于普通开发服务。
不要记录敏感请求体
调试阶段可以记录工具类型和字段名,但不应长期保存 Authorization、完整 prompt、代码、文件内容或模型回复。日志最好采用字段白名单,并对请求 ID 做截断。
处理流式传输和超时
一个能通过简单请求的代理不一定能稳定承载 Codex。至少还要确认:
- SSE 流是否原样转发;
- 客户端断开时上游连接是否及时关闭;
- 超时是否覆盖长时间推理;
Content-Length是否在改写 body 后重新计算;- 压缩请求是否先解压、改写,再正确编码。
现成代理的选择条件
我也考虑过 CC Switch 一类本地路由工具。这次需要转换 tools 数组,因此选用现成代理时,要确认它支持请求体改写、流式响应及工具调用名称映射;仅切换 endpoint、模型和密钥不足以解决问题。
如何确认遇到的是同一个问题
可以从以下几个特征判断:
- 请求在模型输出任何内容之前就被拒绝,错误明确指向
namespace或custom工具类型; - Desktop 或交互式 CLI 失败,而同一配置下的
codex exec正常; - 失败请求的
tools[].type包含namespace或custom; - 网关文档只声明支持标准
function工具。
如果一次性任务已经能通过 codex exec 完成,它也是一个很实用的临时绕行方案:不需要代理,也不会牺牲额外工具。但它不能替代 Desktop 的完整交互体验。
以上行为来自当时使用的 Codex 版本和第三方网关。客户端或接口升级后,字段和行为都可能变化,仍应以实际请求为准。
