补齐 Responses 的 incomplete 终态
Project CI / Frontend tests (pull_request) Failing after 27s
Project CI / Repository checks (pull_request) Failing after 54s
Project CI / Backend tests (pull_request) Successful in 4m55s
Project CI / Native shell tests (pull_request) Failing after 8m17s

Responses 的整体收尾信号有两个,此前只处理了 completed,incomplete 落入
通配分支被整个忽略。真实端点抓包确认:撞到 max_output_tokens 时上游只发
response.incomplete、不发 completed,载荷与 completed 同构,带完整 output[],
item 标 status=incomplete,incomplete_details.reason 给出原因。

忽略它造成三件事:不终止读取循环,网关不主动关连接就等到调用方超时;流式
Responses 永远产生不出 incomplete 这个 finish_reason,上一轮加的截断拒绝规则
对它形同虚设;completed-only 型网关的工具调用被静默丢掉。

与 completed 合并到同一分支,只在 finish_reason 上区分。incomplete 由此自动
走 reject_incomplete_tool_calls:工具调用拒绝,正文按降级结果返回,与 Chat 的
length 口径一致。

三个用例的事件序列转录自真实抓包,退回通配分支时三条全部失败。
This commit is contained in:
2026-07-27 08:51:53 +00:00
parent 58283e7a93
commit fc53ee5c9c
2 changed files with 83 additions and 4 deletions
@@ -267,7 +267,7 @@ arguments 是否必须是完整 JSON **按流式与非流式区分,两者的
流式工具调用必须来自已收尾的流:只要聚合出过工具 slot,收尾时就必须已观察到本协议的完成信号,否则按截断返回 `Deserialize`。完成信号按协议判定——Chat 为非空 `choices[].finish_reason``data: [DONE]`Responses 为 `response.completed`Anthropic 为带 `stop_reason``message_delta``message_stop`。不能用 `data: [DONE]` 作为统一判据:MiniMax 兼容层不发该标记,只发 `finish_reason`。也不能只用 “参数是合法 JSON” 当完成证明——顶层花括号闭合只说明单个参数对象字节完整,说明不了模型是否还要发下一个工具块,更说明不了上游随后会不会报 `max_tokens` 或 error;代理超时、网关自行掐断和 HTTP/2 提前 `END_STREAM` 都表现为干净 EOF,与正常收尾在字节层无法区分。该门禁当前只覆盖工具路径;纯文本响应缺完成信号仍按成功返回并打 warn,改动前必须先确认所有在用网关的文本收尾行为。流在任何工具分片到达前就断掉时槽位为空,门禁无从触发,这是已知残留缺口。
各协议的最终事件必须同时终止读取循环,不能只标记完成:Chat 的 `data: [DONE]`、Responses 的 `response.completed`、Anthropic 的 `message_stop` 都置终止位。服务端在最终事件后保持连接(keep-alive、SSE 网关不主动关流)时,只标记完成会让读取一路等到调用方超时。
各协议的最终事件必须同时终止读取循环,不能只标记完成:Chat 的 `data: [DONE]`、Responses 的 `response.completed``response.incomplete`、Anthropic 的 `message_stop` 都置终止位。Responses 的整体收尾信号有两个——撞到 `max_output_tokens` 时上游**只发 `response.incomplete`、不发 `response.completed`**(真实端点抓包确认),其载荷与 completed 同构,同样带完整 `output[]`item 上标 `status=incomplete``incomplete_details.reason` 给出原因。漏掉它会同时造成三件事:不终止读取循环、流式 Responses 永远产生不出 `incomplete` 这个 `finish_reason`(上面那条截断拒绝规则对它形同虚设)、completed-only 型网关的工具调用被静默丢掉。服务端在最终事件后保持连接(keep-alive、SSE 网关不主动关流)时,只标记完成会让读取一路等到调用方超时。
工具事件的协议槽位缺失时必须失败关闭,不得跳过也不得按事件内位置猜测:槽位是并行分片唯一的归并依据。跳过会静默丢掉整个调用——只剩一个调用时才会被 `StreamUnavailable` 断言兜住,丢一半毫无察觉,而 Responses 的 `finish_reason` 恒为 `completed`,那道断言对它永远不触发;猜测则会把两个不同调用合并成一个混合体(后者的 id / name 覆盖前者,arguments 被拼接)。判定字段为 Chat 的 `delta.tool_calls[].index`、Responses 的 `output_index`、Anthropic 的 content block `index`。该约束只覆盖工具事件,纯文本增量不依赖槽位,不受影响。