Skip to content

fix(translator): always emit client-required fields on the Responses wire - #77

Merged
TriDefender merged 3 commits into
TriDefender:masterfrom
Catapult291:fix/responses-client-required-fields
Nov 1, 2026
Merged

TriDefender merged 3 commits into
TriDefender:masterfrom
Catapult291:fix/responses-client-required-fields

Conversation

@Catapult291

@Catapult291 Catapult291 commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

fix(translator): always emit client-required fields on the Responses wire

问题

/v1/responses 路径的翻译器(chat-to-responses)在若干响应结构上把字段写成条件发射或空壳。部分严格客户端(本例为 Grok Build 内嵌的 async-openai fork 95b52eb)对这些结构做无 serde default 的反序列化——任何一个字段缺席,整轮请求直接以 missing field \x`` 失败:

结构 缺失字段 客户端症状
ResponseUsage input_tokens_details / output_tokens_details 上游 Chat usage 不带 details 时整轮失败(最常见)
OutputTextContent annotations 文本消息整轮失败
ReasoningItem summary reasoning 条目整轮失败
SummaryTextContent text reasoning summary part 失败
FunctionToolCall arguments(added 事件) 工具调用整轮失败

对宽容客户端(官方 OpenAI SDK 等)无影响——官方服务端总会带齐这些字段,所以此前未暴露。

修复

  • usage:无条件发射 input_tokens_details / output_tokens_details,上游缺省时补 cached_tokens: 0 / reasoning_tokens: 0(去掉原来的条件展开)。语义说明:这里的 0 兼指「上游未上报该分项」与「实测为 0」——Anthropic 转换链不上报 reasoning 计数,含 reasoning 的响应会显示 reasoning_tokens: 0;这是「保住已知三个总量」与「不发明计数」之间的取舍,对消费 usage 数字的客户端,缺失→0 本身就是语义变化(见 chatUsageToResponsesUsage 注释)。
  • output_text:所有 batch/stream 位置统一带 annotations: []。
  • reasoning:output_item.added 时发射 summary: [];summary part 打开时带 text: ""。
  • function_call:output_item.added 时发射 arguments: "",实际参数随后续 delta 事件到达。

类型收紧:responses-types.ts 中 ResponsesUsage 两个 details 对象及其内层 cached_tokens / reasoning_tokens 改为必填;annotations 从共享的 ResponsesContentPart 移出,新增 ResponsesOutputTextPart(text 与 annotations 必填)只用于 output 侧,与实际 wire 契约一致。

测试

  • client-required wire fields 测试块(stream + batch)逐结构钉住客户端契约,并先断言目标事件存在再检查 payload——此前过滤式循环在目标事件缺失时会空过,测不出本要钉住的回归。
  • 覆盖作者 review 点名的三类场景:response.completed 终态 response.output 字段、namespaced function_call 分支、流式下非零 cached_tokens / reasoning_tokens 的保留。
  • 新增 usage 回归测试覆盖「上游 Chat usage 完全不带 details」的形状。
  • 全量套件 1109 pass / 0 fail(bun test);bun x tsc --noEmit 干净(CI 上轮挂的 5 处类型错误已修复,推送前本地已加跑)。
  • 真机审计:用附带脚本对运行中的代理发流式文本 / 流式工具调用 / 非流式三类真实请求,逐事件校验上述字段,修复后 0 缺失(脚本见下方复现方式)。

复现 / 验证方式

  1. bun test src/translator/chat-to-responses.test.ts && bun x tsc --noEmit — 新增用例 + 类型检查。
  2. 实弹审计(可选):向运行中的代理 /v1/responses 发送 stream=true 文本请求、stream=true 工具调用请求、非流式请求各一,对每个 SSE 事件检查:response.usage 五字段齐、output_text 部件带 text+annotations、reasoning item 带 summary[]、function_call item 带 arguments。任一缺失即复现本 bug。

备注

  • 对宽容客户端是纯增量(字段只增不减);usage 缺失→0 的语义变化见「修复」节说明。
  • 审计脚本(wire-audit.ts,约 200 行)未包含在本 PR 中,如需要可以补充或另开 gist。

…wire

The Responses client (async-openai fork 95b52eb) deserializes
ResponseUsage, OutputTextContent, ReasoningItem, SummaryTextContent and
FunctionToolCall without serde defaults: an absent field fails the whole
turn with `missing field `x``.

- usage: always emit input_tokens_details / output_tokens_details
  (upstream Chat usage often omits both; the conditional spread used to
  drop them -> `missing field input_tokens_details`)
- output_text parts: always emit annotations: []
- reasoning items: emit summary: [] on open; summary parts carry text: ""
- function_call items: emit arguments: "" on added (deltas stream after)

Adds a "client-required wire fields" test block pinning the contract,
plus a usage regression test for detail-less upstream Chat usage.
@TriDefender

Copy link
Copy Markdown
Owner

收到,我看下情况。之前这个responses系列接线是codex顺手做的

@TriDefender TriDefender left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reported incompatibility is real: the pinned async-openai fork requires the affected fields, and the PR adds them to the relevant batch and streaming paths. The full Bun suite passed locally (1,108 tests), but the required TypeScript check fails, so this is not ready to merge.

Please also strengthen the streaming tests to assert that each expected added/done event exists before checking its payload; the current loops can pass without inspecting an event. Cover the terminal response output, namespaced function calls, and preservation of nonzero usage details. Consider requiring the inner usage counters in the TypeScript type and using an output-specific type or helper for text parts.

This assessment is based on the pinned client schema, translator paths, and tests; I did not run Grok Build end to end.

Comment thread src/translator/chat-to-responses.ts Resolved
@Catapult291

Copy link
Copy Markdown
Contributor Author

谢谢 review,逐条对账:

  1. Type check:确认 5 处错误均为本次新代码引入,修法见 chat-to-responses.test.ts:56 线程;推送前本地加跑 bun x tsc --noEmit(此前本地只跑 bun test,不做类型检查,所以漏了)。
  2. 流式测试加固:同意。现在的断言是『过滤到才检查』,目标事件缺失时循环体一次都不执行,测试空过——测不出它们本要钉住的回归。会改成先断言目标事件存在(filter 后断言数量/类型集合),再逐条检查 payload,并补上你点的三类覆盖:response.completed 终态 response.output 的字段、namespaced function_call 分支(当前只测了普通分支)、流式路径下非零 cached_tokens / reasoning_tokens 的保留。
  3. 类型收紧:同意把 details 内层的 cached_tokens / reasoning_tokens 改为必填(wire 上现在总是发射这两个对象);annotations 只属于 output text part,会给 output 侧单独的类型或构造 helper,而不是继续宽化共享的 ResponsesContentPart。
  4. usage 补 0 的语义:见 chat-to-responses.ts:239 线程——保留 0 以保住已知总量,同时写明 0 兼指『不可用』,并修正 PR 描述里『语义不变』的说法。

另外:PR 描述里提到的 wire-audit 审计脚本如需要,我可以发 gist,方便你端到端复验。修完这些会再请求 re-review。

@TriDefender

Copy link
Copy Markdown
Owner

麻烦提交前跑完一轮test
话说我是不是该把这个加到contribute.md里面啊。。

Address review on PR TriDefender#77:

- Fix all 5 tsc errors in chat-to-responses.test.ts: declare `annotations`
  in the asserted content-part shape, route union casts through `unknown`,
  add the missing `created` field to the batch fixture.
- Stream tests now assert target events EXIST before inspecting payloads
  (the old filter-loops passed vacuously when the event under test was
  missing), and also cover the terminal `response.output` fields,
  the namespaced function_call branch, and preservation of non-zero
  cached_tokens / reasoning_tokens from upstream usage.
- Tighten types: ResponsesUsage detail counts (cached_tokens /
  reasoning_tokens) are now required, matching the wire; output-text
  annotations moved from the shared ResponsesContentPart to a dedicated
  ResponsesOutputTextPart with required `text` + `annotations`.
- Document the "0 means unavailable as well as zero" semantics of the
  usage fallback in chatUsageToResponsesUsage.

Full suite: bun test 1109 pass / 0 fail; bun x tsc --noEmit clean.
@Catapult291

Catapult291 commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor Author

跑完一轮了,全部绿:

  • bun test:1109 pass / 0 fail
  • bun x tsc --noEmit:干净(上轮 CI 的 5 处类型错误已修,推送前本地加跑了这个)

顺带把这轮 review 里答应对齐的都做掉了(2fa3078):

  1. 流式测试加固:改成先断言目标事件存在(filter 后断言数量/类型集合)再查 payload,不再有空过可能;补了 response.completed 终态 response.output 字段、namespaced function_call 分支、流式非零 cached_tokens / reasoning_tokens 保留三类覆盖。
  2. 类型收紧:ResponsesUsage 两个 details 内层的 cached_tokens / reasoning_tokens 改必填;annotations 移出共享 ResponsesContentPart,新增只用于 output 侧的 ResponsesOutputTextPart(text + annotations 必填)。
  3. usage 补 0 语义:chatUsageToResponsesUsage 注释写明 0 兼指「未上报」与「实测 0」,PR 描述里「不改变语义」的说法也改掉了。

The dedicated output-text part type introduced for the tightened
annotations contract is now used by the message variant of
ResponsesOutputItem, instead of widening through ResponsesContentPart.
@Catapult291

Catapult291 commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor Author

补两个小提交:

  • 9327725:把上一步新增的 ResponsesOutputTextPart 真正接到 ResponsesOutputItem 的 message 变体上(之前只是定义了没接线,那样反而留了个没人用的类型)。接线后 bun test 1109 pass / 0 fail、bun x tsc --noEmit 干净,本地已跑过一轮。

@Catapult291

Copy link
Copy Markdown
Contributor Author

glm 5.3 flash是傻逼,请不要管它说的有的没的(流汗黄豆

@TriDefender TriDefender left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审了 9327725:上次提出的类型检查错误已经修复;流式事件、终态 output、命名空间函数调用和非零用量的覆盖已补充;usage 缺失时补 0 的取舍也已在 PR 描述中说明。独立执行 bun x tsc --noEmit 通过,bun test 为 1109 pass / 0 fail。此前请求修改的问题已解决,没有发现新的阻塞问题。

下面有一处非阻塞的注释措辞建议。此次复审没有实际运行 Grok Build 或重复上游实弹审计。

// SEMANTICS OF 0: an emitted `cached_tokens: 0` / `reasoning_tokens: 0`
// means "upstream did not report this breakdown", NOT a measured zero.
// The Anthropic translation layer only reports cache buckets when the
// upstream sends them and never reports reasoning tokens, so responses

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

非阻塞:这里和 ResponsesUsage 的注释写成“0 表示未上报,而不是实测为 0”过于绝对。代码也会原样保留上游明确上报的 0;建议与 PR 描述一致,写成“0 可能表示未上报,也可能是上游报告的 0,无法区分”。

@TriDefender
TriDefender merged commit 37155a5 into TriDefender:master Nov 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants