文档:同步流式终态协议说明
Project CI / Frontend tests (pull_request) Failing after 28s
Project CI / Repository checks (pull_request) Failing after 59s
Project CI / Backend tests (pull_request) Successful in 5m20s
Project CI / Native shell tests (pull_request) Failing after 8m22s

补充 Responses incomplete 的完成与终止语义

修正尾部错误保留和工具槽位失败关闭说明

同步 platform-llm 源码注释与 README
This commit is contained in:
2026-07-27 09:41:11 +00:00
parent f8d3f81263
commit a00893a0ce
3 changed files with 20 additions and 19 deletions
@@ -252,10 +252,10 @@ npm run check:server-rs-ddd
| API kind | 请求工具形态 | `tool_choice` | 非流式响应 | 流式事件与 slot |
| --- | --- | --- | --- | --- |
| `OpenAiChat` | `tools[].type=function`,函数字段为 `name` / `description` / `parameters` / `strict` | `"auto"` / `"required"` | `choices[0].message.tool_calls` | `delta.tool_calls[].index` |
| `OpenAiResponses` | `tools[].type=function`,函数字段为 `name` / `description` / `parameters` / `strict` | `"auto"` / `"required"` | `output[].type=function_call` | `output_index``response.completed.response.output[]` 可作为 completed-only 兜底 |
| `OpenAiResponses` | `tools[].type=function`,函数字段为 `name` / `description` / `parameters` / `strict` | `"auto"` / `"required"` | `output[].type=function_call` | `output_index``response.completed` / `response.incomplete``response.output[]` 可作为仅有终态事件时的兜底 |
| `Anthropic` | 顶层 `tools[]``name` / `description` / `input_schema`,无 `function` 包装层和 `strict` | `{ "type": "auto" }` / `{ "type": "any" }``Required → any` | `content[].type=tool_use``input` 序列化为 `arguments` | content block `index``content_block_start` + `input_json_delta` |
流式 `on_delta` 只发送文本增量、累计文本和完成原因,工具调用不进入回调。平台层按协议 slot 聚合并行工具片段:Chat 使用 `delta.tool_calls[].index`Responses 使用 `output_index`Anthropic 使用 content block `index`。Responses 的 `function_call_arguments.done``response.completed` 中的完整 arguments 覆盖此前分片;completed-only 恢复以 `response.output[]` 数组下标作为 slot。该聚合只负责解析,不表示工具执行并发。
流式 `on_delta` 只发送文本增量、累计文本和完成原因,工具调用不进入回调。平台层按协议 slot 聚合并行工具片段:Chat 使用 `delta.tool_calls[].index`Responses 使用 `output_index`Anthropic 使用 content block `index`。Responses 的 `function_call_arguments.done``response.completed``response.incomplete` 中的完整 arguments 覆盖此前分片;仅有终态事件时的恢复以 `response.output[]` 数组下标作为 slot。该聚合只负责解析,不表示工具执行并发。
工具调用归一只有一份策略,流式与非流式、三种协议共用:协议层只把各自 DTO 映射成统一中间形态,接受与否全部由归一层判定。**被识别为工具调用(Chat 的 `tool_calls[]` 成员、Responses 的 `type=function_call`、Anthropic 的 `type=tool_use`)后,字段不全一律返回 `Deserialize`,不得静默丢弃。** 缺少 id 或函数名报错;arguments 缺省或空白归一为 `{}`(零参函数合法)。
@@ -265,13 +265,13 @@ arguments 是否必须是完整 JSON **按流式与非流式区分,两者的
工具调用还必须来自**没有被上游宣告为未完成**的响应。上游给出明确的截断 / 过滤 / 失败终态时,即使参数恰好闭合成合法 JSON 也必须返回 `Deserialize`:字节完整不代表模型把本轮工具计划表达完了,而下游拿到 `tool_calls` 就会真的去执行,格式修复循环对"参数合法但内容被砍断"完全无从察觉。已知终态为——Chat 的 `length` / `content_filter`Responses 的 `incomplete` / `failed` / `cancelled`Anthropic 的 `max_tokens` / `pause_turn` / `refusal`。这里必须用黑名单而非白名单,未知值与缺失一律放行,否则会误杀不发或自定义该字段的兼容网关。该检查**只在存在工具调用时生效**:正文被 `max_tokens` 截断仍是可用的降级结果,一并拒绝会打死所有触及输出上限的长文本回答。与非流式畸形参数透传同时成立时,本检查优先。
流式工具调用必须来自已收尾的流:只要聚合出过工具 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,改动前必须先确认所有在用网关的文本收尾行为。流在任何工具分片到达前就断掉时槽位为空,门禁无从触发,这是已知残留缺口。
流式工具调用必须来自已收尾的流:只要聚合出过工具 slot,收尾时就必须已观察到本协议的完成信号,否则按截断返回 `Deserialize`。完成信号按协议判定——Chat 为非空 `choices[].finish_reason``data: [DONE]`Responses 为 `response.completed``response.incomplete`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``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 网关不主动关流)时,只标记完成会让读取一路等到调用方超时。
各协议的最终事件必须同时终止读取循环,不能只标记完成: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`(上面那条截断拒绝规则对它形同虚设)、只在整体终态事件中携带的工具调用被静默丢掉。服务端在最终事件后保持连接(keep-alive、SSE 网关不主动关流)时,只标记完成会让读取一路等到调用方超时。
工具事件的协议槽位缺失时必须失败关闭,不得跳过也不得按事件内位置猜测:槽位是并行分片唯一的归并依据。跳过会静默丢掉整个调用——只剩一个调用时才`StreamUnavailable` 断言兜住,丢一半毫无察觉,而 Responses 的 `finish_reason` 恒为 `completed`那道断言对它永远不触发;猜测则会把两个不同调用合并成一个混合体(后者的 id / name 覆盖前者,arguments 被拼接)。判定字段为 Chat 的 `delta.tool_calls[].index`、Responses 的 `output_index`、Anthropic 的 content block `index`。该约束只覆盖工具事件,纯文本增量不依赖槽位,不受影响。
工具事件的协议槽位缺失时必须失败关闭,不得跳过也不得按事件内位置猜测:槽位是并行分片唯一的归并依据。跳过会静默丢掉整个调用——只剩一个调用时才可能`StreamUnavailable` 断言兜住,丢一半毫无察觉Responses 的整体终态原因是 `completed` / `incomplete`也不会触发只识别 `tool_use` / `tool_calls` 的那道断言。猜测则会把两个不同调用合并成一个混合体(后者的 id / name 覆盖前者,arguments 被拼接)。判定字段为 Chat 的 `delta.tool_calls[].index`、Responses 的 `output_index`、Anthropic 的 content block `index`。该约束只覆盖工具事件,纯文本增量不依赖槽位,不受影响。
反过来,已经收尾的流遇到尾部传输 / 解析错误时必须保留结果,不能重跑 Provider。判断“有没有值得保留的东西”要看正文或工具调用任一非空,不能只看正文——纯工具调用响应的正文本来就是空的(MiniMax 的 Anthropic 工具流恒定如此),只看正文会让这类响应每次都被丢弃,白白多跑一轮往返。保留的安全性由“协议完成信号已到 + `finish_reason` 已到 + 工具参数完整 + 错误属可容忍尾部错误”共同保证,与正常路径判据一致。Anthropic 的 `message_stop` Responses 的 `response.completed` 目前只标记完成、不标记流终止,因此收尾事件之后仍会读到 EOF,这条尾部路径是常态而非边缘情况
反过来,已经收尾的流遇到尾部传输 / 解析错误时必须保留结果,不能重跑 Provider。判断“有没有值得保留的东西”要看正文或工具调用任一非空,不能只看正文——纯工具调用响应的正文本来就是空的(MiniMax 的 Anthropic 工具流恒定如此),只看正文会让这类响应每次都被丢弃,白白多跑一轮往返。保留的安全性由“协议完成信号已到 + `finish_reason` 已到 + 工具参数完整 + 错误属可容忍尾部错误”共同保证,与正常路径判据一致。Chat 的非空 `finish_reason` 与 Anthropic 带 `stop_reason``message_delta` 会标记完成但不直接终止读取,因此在后续终止事件缺失时仍可能进入这条尾部保留路径;Chat 的 `[DONE]`Anthropic 的 `message_stop` 以及 Responses 的 `response.completed` / `response.incomplete` 已经直接终止读取
错误边界固定如下:`StreamUnavailable` 只表示流式响应已给出 `tool_use` / `tool_calls` 完成原因但没有聚合出任何工具 slot,供调用方回退非流式,它不承担截断语义;`EmptyResponse` 表示最终文本和工具调用都为空,纯工具响应合法;`Deserialize` 覆盖 JSON / SSE / UTF-8 解析失败、缺少 `choices[0]`、流式工具身份缺失、流式参数不完整,以及上述工具流未收尾截断。Anthropic 仍不支持 `web_search`、图片内容和纯 system 消息,必须至少有一条非 system 文本消息。