文档:同步流式终态协议说明
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
+3 -3
View File
@@ -32,12 +32,12 @@
| `OpenAiResponses` | `tools[].type=function`,函数内为 `name` / `description` / `parameters` / `strict` | `"auto"` / `"required"` | `output[].type=function_call` | `output_index`;`output_item.added` 提供身份,`function_call_arguments.delta` 拼接,`.done` 覆盖完整参数 |
| `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` 拼接参数 |
Responses 如果只发送 `response.completed`,解析器会从其中的 `response.output[]` 恢复 `function_call`;恢复时使用 output 数组下标作为 slot。三种协议的并行工具调用只在平台层做 slot 聚合,不代表工具会在平台层并发执行。
Responses 如果只发送 `response.completed` 或 `response.incomplete`,解析器会从其中的 `response.output[]` 恢复 `function_call`;恢复时使用 output 数组下标作为 slot。`response.incomplete` 表示上游没有完成本轮生成:其中的工具调用即使参数是完整 JSON 也返回 `Deserialize`,纯正文则保留为可用的降级结果。三种协议的并行工具调用只在平台层做 slot 聚合,不代表工具会在平台层并发执行。
## 4. 流式与参数契约
1. `LlmStreamDelta` 只包含 `accumulated_text`、`delta_text` 和 `finish_reason`,工具调用不会进入 `on_delta`;纯工具响应允许 `text` 为空。
2. 工具片段按协议索引聚合:Chat 使用 `delta.tool_calls[].index`,Responses 使用 `output_index`,Anthropic 使用 content block `index`。Responses 的 `.done` 和 `response.completed` 完整 arguments 是权威值,可以覆盖之前的分片拼接。
2. 工具片段按协议索引聚合:Chat 使用 `delta.tool_calls[].index`,Responses 使用 `output_index`,Anthropic 使用 content block `index`。Responses 的 `.done`、`response.completed` 和 `response.incomplete` 中的完整 arguments 是权威值,可以覆盖之前的分片拼接。
3. 流结束固化工具调用时,缺少 id 或函数名返回 `Deserialize`;空参数默认保存为 `{}`;非空参数必须能反序列化为完整 JSON,截断或半截 JSON 不会交给业务层。这里是 JSON 语法完整性检查,不是针对 `parameters` 的 JSON Schema 业务校验。
4. 非流式工具调用采用不同的参数边界:缺失或空白 `arguments` 统一归一为 `{}`;Chat / Responses 的非空畸形 `arguments` 不在平台层做 JSON 校验、修复或静默丢弃,而是保留参数内容(仅按统一归一策略去除首尾空白),连同 call id 和函数名交给调用方的 repair 循环。Anthropic `tool_use.input` 缺失时同样按 `{}` 归一;调用方不能把非流式参数自动假定为统一 schema 校验通过。
@@ -87,6 +87,6 @@ Responses 如果只发送 `response.completed`,解析器会从其中的 `respo
## 9. 验收证据边界
1. `cargo test -p platform-llm` 的确定性用例把 checked-in SSE fixture 交给 parser,验证归一后的文本、工具调用、slot 聚合、Responses completed-only 恢复、参数 JSON 完整性和错误边界。fixture 可以来源于真实端点抓包,但测试不保存原始 SSE,也不逐事件与端点报文比较,因此不能证明抓包转录无偏差。
1. `cargo test -p platform-llm` 的确定性用例把 checked-in SSE fixture 交给 parser,验证归一后的文本、工具调用、slot 聚合、Responses 仅有 completed / incomplete 终态事件时的恢复、参数 JSON 完整性和错误边界。fixture 可以来源于真实端点抓包,但测试不保存原始 SSE,也不逐事件与端点报文比较,因此不能证明抓包转录无偏差。
2. `tests/live_stream_tool_calls.rs` 是默认 `#[ignore]` 的真实端点工具调用 smoke。它只验证最终归一结果中的工具名、id 和完整参数 JSON;文本增量字符数仅用于打印观测,工具调用不进入 `on_delta`,也没有原始 SSE 录制或逐事件对比能力。
3. 因此验收应分别称为“固定 SSE fixture parser 覆盖”和“真实端点归一工具调用 smoke”,不能把后者描述为原始 SSE fidelity 或转录一致性证明。
+11 -10
View File
@@ -610,9 +610,10 @@ struct ParsedStreamEvent {
finish_reason: Option<String>,
usage: Option<LlmTokenUsage>,
is_terminal: bool,
// 本事件是协议层的收尾信号。它和 is_terminal 不同:is_terminal 只有 Chat 的 [DONE]
// 会置位,而 MiniMax 这类网关根本不发 [DONE],只能靠各协议自己的完成事件判断。
// 也和 finish_reason 分开:Anthropic 的 message_stop 是收尾信号但不带 stop_reason,
// 本事件是协议层的收尾信号。它和 is_terminal 不同:Chat 的非空 finish_reason
// 与 Anthropic 带 stop_reason 的 message_delta 能证明流已收尾,但不会直接终止读取;
// Chat 的 [DONE]、Responses 的 completed / incomplete 与 Anthropic 的 message_stop
// 才会同时置 is_terminal。它也和 finish_reason 分开:message_stop 不带 stop_reason,
// 不能借它写 finish_reason,否则会覆盖 message_delta 给出的真实 end_turn。
is_completion: bool,
tool_fragments: Vec<ToolCallFragment>,
@@ -2828,8 +2829,8 @@ fn parse_responses_sse_event(data: &str) -> Result<Option<ParsedStreamEvent>, Ll
// 并给出 finish_reason,工具调用交由 reject_incomplete_tool_calls 统一拒绝,
// 正文仍按降级结果返回,与 Chat 的 length 口径一致。忽略它会同时造成三件事:
// 不终止读取循环(网关不关连接就等到调用方超时)、流式 Responses 永远产生不出
// incomplete 这个 finish_reason(截断拒绝规则形同虚设)、completed-only 型网关
// 的工具调用被静默丢掉。
// incomplete 这个 finish_reason(截断拒绝规则形同虚设)、只在整体终态事件中携带
// 工具调用的网关响应被静默丢掉。
"response.completed" | "response.incomplete" => Ok(Some(ParsedStreamEvent {
finish_reason: Some(
if event_type == "response.incomplete" {
@@ -2967,9 +2968,9 @@ fn anthropic_block_slot(parsed: &serde_json::Value) -> Option<u64> {
}
// 槽位是并行工具分片唯一的归并依据,缺失时必须失败关闭而不是跳过或猜测:跳过会静默
// 丢掉整个调用(只剩一个调用时才会被 StreamUnavailable 断言兜住,丢一半就毫无察觉,
// 而 Responses 的 finish_reason 恒为 completed,那道断言对它永远不触发),猜测则会把
// 两个不同调用合并成一个混合体。
// 丢掉整个调用(只剩一个调用时才可能被 StreamUnavailable 断言兜住,丢一半就毫无察觉;
// Responses 的整体终态原因是 completed / incomplete,也不会触发只识别 tool_use /
// tool_calls 的那道断言),猜测则会把两个不同调用合并成一个混合体。
fn missing_tool_slot_error(protocol: &str, event: &str, field: &str) -> LlmError {
LlmError::Deserialize(format!(
"LLM {protocol} 流式工具事件缺少槽位字段 {field}:event={event}"
@@ -4944,8 +4945,8 @@ mod tests {
#[tokio::test]
async fn stream_run_rejects_responses_tool_calls_without_completion_signal() {
// Responses 的整体收尾只有 response.completed;单 item 的
// function_call_arguments.done 不能顶替它。
// Responses 的整体收尾只有 response.completed / response.incomplete;单 item
// 的 function_call_arguments.done 不能顶替它。
let server_url = spawn_mock_server(vec![MockResponse {
status_line: "200 OK",
content_type: "text/event-stream; charset=utf-8",