Skip to content

fix(openai): 改写 Codex turn metadata 时保留非 ASCII 转义 - #7426

Merged
Wei-Shaw merged 1 commit into
Wei-Shaw:mainfrom
xlplbo:fix/openai-codex-turn-metadata-ascii
Sep 22, 2026
Merged

Wei-Shaw merged 1 commit into
Wei-Shaw:mainfrom
xlplbo:fix/openai-codex-turn-metadata-ascii

Conversation

@xlplbo

@xlplbo xlplbo commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

目的

网关改写 x-codex-turn-metadata(账号身份、设备指纹)之后,输出仍保持可见 ASCII,避免工作区路径含中文等非 ASCII 字符的 Codex 会话被上游中途断开。

解决的问题

x-codex-turn-metadata 是一段内嵌 JSON 字符串,既作为 HTTP 头发送,也放在 Responses WebSocket 请求体的 client_metadata 里。它的 workspaces 字段以工作区路径为键,路径里有中文目录名很常见;客户端发出时非 ASCII 字符是 \uXXXX 转义形式,整段是可见 ASCII。

网关有四处会改写这段元数据,流程都是 json.Unmarshal → 改字段 → json.Marshal:

改写 头 请求体 client_metadata
账号身份 applyCodexAccountIdentityHeaders applyCodexAccountIdentityEmbeddedMetadata
指纹收敛 rewriteCodexTurnMetadataFields rewriteClientMetadataEmbeddedTurnMetadata

json.Marshal 不保留转义形式:\u4e2d(“中”)会被写成原始 UTF-8 字节,DEL(0x7f)也原样输出。两种写法在 JSON 语义上等价,但这段字符串有请求头用途,上游对其中的原始非 ASCII 字符不兼容。

用户可见的表现(WS v2 passthrough,工作区路径含中文目录名):上游只回 codex.rate_limits 和 codex.response.metadata 两条元事件,随后以 1000、空 reason 关闭,业务轮次没有终态;网关侧报 upstream websocket closed before terminal event,客户端收到 1011。它是确定性的报文编码问题:同一请求换到别的账号照样复现,换号解决不了。

这不是“WS 不支持中文”:任务指令和上下文里的中文原样发送都正常,问题只在这一段元数据字符串。ctx_pool 路径平时不暴露,是因为 openai_ws_forwarder_ingress.go 会先用握手头里的 x-codex-turn-metadata 覆盖每帧元数据,而握手元数据不含 workspaces;passthrough 保留每轮元数据,所以暴露。

效果

真实上游:修复前的单变量对照

抓取真实 Codex 客户端的一个完整失败请求帧(107713 字节,含全部工具声明),对真实上游逐项重放,每次只改一个变量:

对照 结果
原样完整请求 上游提前关闭
只去掉内嵌 turn metadata 生成完成,产生 exec 工具调用
只把元数据里的两个中文字符换成 ASCII 生成完成,产生 exec 工具调用
紧接着恢复原样完整请求 再次提前关闭
简化请求,元数据只留 workspaces,中文保持 \uXXXX 转义 生成完成
上一行只把内嵌 JSON 改成原始中文(解码后语义完全相同) 提前关闭
workspaces 保持客户端转义形式,再加 installation_id 触发网关身份改写 提前关闭
2048 字节纯 ASCII 的 turn metadata 生成完成(排除长度因素)

共 23 次重放,11 次完成、12 次按预期复现关闭(这是诊断样本分布,不是业务失败率)。单独去掉工具、缩短上下文、去掉 reasoning 配置都不能消除故障。

真实上游:修复后的验收

部署本修复后直接访问网关,不经任何转发器或元数据变换:

测试 结果
上面那个 107713 字节的失败请求,原样重放(请求 SHA256 与修复前相同) response.completed,产生 exec 工具调用
完整 Codex 客户端走 WS,工作区路径含中文 两条工具命令成功,回复正确,无 HTTP 回退
同上,连接中途发生换号 预热加三个业务轮次全部完成,无 HTTP 回退
完整 Codex 客户端走 HTTP 两条工具命令成功,回复正确

该修复自 2026-09-13 起在我们的自建实例上运行。

单测:改前改后对比

新增用例 TestCodexTurnMetadataRewritePreservesHeaderSafeJSON(4 组输入 × 6 个改写入口 = 24 个子用例,断言输出每个字节都在 0x20–0x7e,且解码后除被改写字段外与输入逐字段相等):

含非 ASCII / DEL 的 3 组输入(18 个子用例) 纯 ASCII 输入(6 个子用例)
改前(upstream/main d2e319b,只还原两个调用点文件) 18 个全部失败:输出含 0xe4 等原始 UTF-8 字节或 0x7f 通过
本 PR 全部通过 通过

改动

  • 新增 marshalCodexTurnMetadata(openai_codex_turn_metadata.go):先 json.Marshal,再把 ≥ 0x7f 的字符写成 \uXXXX,BMP 之外的字符写成 UTF-16 代理对;不含此类字符时直接返回 json.Marshal 的结果,不多分配。处理的是所有非 ASCII 字符,不按“是否中文”判断。
  • 上表四处改写点从 json.Marshal 换成该函数,其余逻辑不动。

只改这一段元数据的序列化方式;请求正文、工具定义、路由、换号与冷却行为都不变,解码后的字段名、取值、字面反斜杠与原有转义语义保持不变。没有采用“用握手元数据覆盖每轮元数据”的做法,那样会丢掉每轮新增的工作区信息。

测试

cd backend
go test -tags=unit ./internal/service -run 'TestCodexTurnMetadata|TestCodex.*Identity|TestFingerprint|TestCodexFingerprint' -count=1
  • 上述定向用例通过;./internal/service 全量 -tags=unit:除 TestOllamaProbeCallback_StaleLongDoesNotOverrideNewShort 外全部通过。该用例是上游既有用例,本机 Windows 上因时钟步进固定失败,本 PR 未触及相关限流代码。
  • golangci-lint run ./internal/service/:0 issues。

代价与保留项

  • 转义后每个 BMP 字符从 3 字节变成 6 字节,元数据略变长;含非 ASCII 字符时多一次遍历与一次分配。
  • 上游没有给出异常详情,只能确定“原始非 ASCII 字符会触发关闭、转义形式不会”,不能断言上游内部是哪一种校验或编码器报错。
  • 修复前的真实上游复现只在 WS v2 passthrough 链路上做过。HTTP 与 ctx_pool 的出口用的是同一个序列化函数、存在同样的编码风险,所以一并修了,但这两条链路修复前的故障没有单独复现过;修复后 HTTP 做过完整客户端验收。

@Wei-Shaw
Wei-Shaw merged commit 680a992 into Wei-Shaw:main Sep 22, 2026
8 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 22, 2026
@xlplbo
xlplbo deleted the fix/openai-codex-turn-metadata-ascii branch September 23, 2026 13:43
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants