
v8.0.51 把 Codex 请求里的 system 写成 developer,接严格上游直接 422,而这条行为是被一条单测钉住的;同一版的 Antigravity→Claude 翻译器把 SAFETY 等八种拒答都译成 end_turn。两张补丁都还没进发行版,其中一张的评审里还挂着一条未闭合的 P1。
HEAD、main 与 v8.0.51 指向同一个提交 7cad1cb7,dev 停在 06ec5eac,还是上期那个合并提交。developer 这个角色出去,撞在上游的 422 上;回答里的一次拒答被写成 end_turn,客户端以为一切正常。/v1/responses 经 openai-compatibility 转成 /chat/completions 之后,首条消息角色是 developer,DeepSeek 官方 API 回 HTTP 422。1messages[0].role: unknown variant 'developer',它只认 system、user、assistant、tool、latest_reminder 五种。developer 不是上游给的,是我们自己写出去的。v8.0.51 的 Codex→Chat 翻译器在拼消息时做了一次角色改写:源角色是 system,输出就写成 developer。
internal/translator/codex/openai/chat-completions/codex_openai_request.go 第 218–222 行:system 被改写成 developer,其余角色原样透传。TestOpenAICompatExecutorPreservesResponsesInstructionRoles 断言的八个角色里,有三处是 developer。
internal/runtime/executor/openai_compat_executor_roles_test.go 第 68 行,期望的角色序列里第 2、4、7 位都是 developer;函数名在第 18 行。system,得同时改代码和这条断言——补丁正是这么做的。00b76771。那笔提交确实存在,作者时间 2026-11-01T17:03:01Z,标题是「保留 responses 到 chat completions 的 developer 角色」。2openai/openai/responses。在 v8.0.50 里,那条路会把 developer 降级成 user。
internal/translator/openai/openai/responses/openai_openai-responses_request.go 第 255–259 行:注释写着 developer 内容「以用户文本发出」,代码把角色改成 user;v8.0.51 里这两行已被删掉。/v1/responses 从 v8.0.51 起开始带 developer 出去」这个判断成立。system → developer 逐字节相同,两版只差 parallel_tool_calls 一行。走 Codex 格式的请求,在更早的版本上就已经带着 developer 出去了。executeWithCredentials、流式 executeStreamWithCredentials、CountTokens。ApplyPayloadConfigWithRequest 与 NewPayloadFinalizer 之前。所以配置里的 payload 规则看到的,是改好之后的角色。role 字段偏移,把 "developer" 换成 "system"。偏移对不上、或替换区间越界,就整份返回原样。
internal/runtime/executor/helps/developer_role.go 第 26–58 行(fork 分支 Lingbou/CLIProxyAPI 的 fix/openai-compat-developer-role):第 49 行是那段「偏移对不上就整份放弃」的判断。"\u0064eveloper",gjson 解出来是 developer,但原字节比对不等,于是整份放弃。4has_any_findings: false。补丁现在 open、mergeable clean、4 文件 +198/−2、两笔提交、无 draft。openai-compatibility 上游收到的 developer 都会变成 system。developer 语义——比如同一份请求之后还要转去 Codex——这一改会把它抹掉。反过来,因为归一化排在 payload 规则之前,合并后你仍可以用一条 override 把某个固定的位置改回 developer。unknown variant 并点名 developer,同时这个上游是 openai-compatibility 配的。developer 的上游,或把这个模型从这个 key 的模型白名单里摘掉。payload 的 override 规则按 JSON 路径写,比如 messages.0.role;但一份请求里 developer 出现几处并不固定,发行版那条测试里就有三处。按索引写规则,只能挡住你确认过的那一种排布。finishReason 说明这次生成为什么结束,翻译成 Claude 格式时要变成 stop_reason。v8.0.51 只认两种结局。
internal/translator/antigravity/claude/antigravity_claude_response.go 第 594–607 行:只处理 MAX_TOKENS 与 STOP、FINISH_REASON_UNSPECIFIED、UNKNOWN,其余全落到 end_turn。非流式第 827–842 行是同一套 switch 的第二份拷贝。SAFETY、RECITATION、PROHIBITED_CONTENT、SPII、BLOCKLIST、IMAGE_SAFETY、MALFORMED_RESPONSE、MALFORMED_FUNCTION_CALL。antigravity → openai 那条翻译器早就按这八种判内容过滤。5
internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go 第 278 行调用 isAntigravityContentFilterFinishReason,名单在第 292 行;命中后写的是 OpenAI 侧的 content_filter。content_filter,走 Claude 这条路变成 end_turn。finishReason 是空串时,会把前面已记下的值覆盖掉。第 477–482 行把值赋进去之前没有判空。end_turn 加空内容」不是可用判据:它既可能是模型自然说完,也可能是被安全策略拦下。这正是这个 bug 值钱的地方。SAFETY 拦下的那一次,上游仍报了 8149 个输入 token、输出 0——这段没生成的回答,输入照算。refusal 是 Claude 的正式取值:表示模型拒绝生成,仍然是 HTTP 200,不是错误。WebSearchTextBuffer,只有 HasFinishReason 为真时才倒出来;新的空值保护让那条只带空 finishReason 的路径不再置位,[DONE] 那条分支又不做兜底,于是客户端拿到一段空回答。8has_any_findings: true、has_unresolved_findings: true。补丁现在 open,mergeable_state 标 unstable,unstable 的具体原因我没查。finishReason 是这八个值之一,而下游 Claude 客户端显示 end_turn。这条只能从上游侧看,客户端自己分辨不出来。end_turn 当健康信号,也别反过来把 end_turn 加空内容当成被拦——它本来就可能是正常空响应。| 条目 | 改了什么 | 规模 | 状态 |
|---|---|---|---|
| #7193 | 报障:developer 撞严格上游 422 | 报障单,无补丁 | open、0 评论、无标签 |
| #7194 | developer → system,三条路径 | 4 文件 +198/−2、2 提交 | open、mergeable clean、评审无 finding |
| #7192 | 八种拒答 → refusal | 2 文件 +171/−22、1 提交 | open、unstable、1 条 P1 未闭合 |
dev,都没合并,也都不在 v8.0.51 里。unknown variant;拒答看上游 finishReason 有没有落在那八个值里。end_turn 当成「回答完整」的同义词。github.com
github.com
github.com
platform.claude.com
github.com
github.com
This story was produced automatically by a channel. One sentence is all it takes for Neodrop to keep producing for you.