流式工具调用要求协议收尾信号
Project CI / Repository checks (pull_request) Failing after 1m26s
Project CI / Frontend tests (pull_request) Successful in 2m27s
Project CI / Backend tests (pull_request) Successful in 4m40s
Project CI / Native shell tests (pull_request) Failing after 7m34s

字节流干净结束不等于协议收尾:代理超时、网关掐断和 HTTP/2 提前
END_STREAM 都表现为干净 EOF。此前只要工具参数恰好是合法 JSON 就会
返回并执行该调用,且并行槽位在断流后会被静默丢弃。

新增 ParsedStreamEvent::is_completion 与 StreamAccumulation::
completion_observed,按协议判定收尾:Chat 用非空 finish_reason 或
[DONE],Responses 用 response.completed,Anthropic 用带 stop_reason
的 message_delta 或 message_stop。不能统一用 [DONE],MiniMax 兼容层
不发该标记。message_stop 改为产生只带 is_completion 的事件,保持不
伪造 finish_reason 覆盖真实 end_turn 的既有约束。

聚合出过工具 slot 但未观察到收尾信号时返回 Deserialize;判定使用未
固化槽位,使参数恰好闭合的截断仍按截断归因。StreamUnavailable 语义
不变,仍只表示事件形状不受支持。本轮只收严工具路径,纯文本缺收尾信
号仍返回成功并打 warn。

真实端点验证:MiniMax Anthropic 兼容层 stop_reason=tool_use、
MiniMax OpenAI 兼容层 finish_reason=tool_calls,均通过门禁。
This commit is contained in:
2026-07-27 05:36:32 +00:00
parent 1580b3daf3
commit 445559bfea
2 changed files with 240 additions and 9 deletions
@@ -259,7 +259,9 @@ npm run check:server-rs-ddd
流式收尾时,缺少工具 id / name、或非空 arguments 不是完整 JSON,均返回 `Deserialize`;空 arguments 默认归一为 `{}`。这只是 JSON 语法完整性检查,不是按工具 `parameters` 执行 JSON Schema 校验。非流式 Anthropic 缺失 `tool_use.input` 时也归一为 `{}`;其它协议的非流式 arguments 仍按上游字段解析。
错误边界固定如下:`StreamUnavailable` 只表示流式响应已给出 `tool_use` / `tool_calls` 完成原因但没有聚合出任何工具 slot,供调用方回退非流式;`EmptyResponse` 表示最终文本和工具调用都为空,纯工具响应合法;`Deserialize` 覆盖 JSON / SSE / UTF-8 解析失败、缺少 `choices[0]`、流式工具身份缺失和流式参数不完整。Anthropic 仍不支持 `web_search`、图片内容和纯 system 消息,必须至少有一条非 system 文本消息。
流式工具调用必须来自已收尾的流:只要聚合出过工具 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,改动前必须先确认所有在用网关的文本收尾行为。流在任何工具分片到达前就断掉时槽位为空,门禁无从触发,这是已知残留缺口。
错误边界固定如下:`StreamUnavailable` 只表示流式响应已给出 `tool_use` / `tool_calls` 完成原因但没有聚合出任何工具 slot,供调用方回退非流式,它不承担截断语义;`EmptyResponse` 表示最终文本和工具调用都为空,纯工具响应合法;`Deserialize` 覆盖 JSON / SSE / UTF-8 解析失败、缺少 `choices[0]`、流式工具身份缺失、流式参数不完整,以及上述工具流未收尾截断。Anthropic 仍不支持 `web_search`、图片内容和纯 system 消息,必须至少有一条非 system 文本消息。
- 图片生成:VectorEngine `gpt-image-2` 图片 provider 归属 `platform-image`,密钥只在后端环境变量中;`api-server` 内的 `openai_image_generation.rs` 只是兼容调用面和外部失败审计桥接,不再承载 provider 协议实现。实际外部生成运行记录统一落 `tracking_event`,`event_key = external_generation_run`,metadata 记录开始 / 结束时间、耗时、状态、成功标记、失败原因、provider task id 和结果摘要,不再写回过时的 `ai_task`。DashScope 只按仍在使用的历史能力单独处理,不作为 GPT-image-2 兜底。VectorEngine `/v1/images/generations` 和 `/v1/images/edits` 上游 POST 使用 `libcurl` 发送;`reqwest` 只保留给参考图 URL 下载和响应中图片 URL 下载。`/v1/images/edits` 的 multipart 参考图必须作为 libcurl 文件上传 part 发送,字段名为 `image`,实现上使用 `Form::buffer(file_name, bytes)` 并设置 `Content-Type`;不能只用 `contents(...).filename(...)`,否则上游会把请求转码为缺少图片并返回 `image is required`。`request_send` 阶段的 curl timeout / connect error 按可重试传输错误处理,最多尝试 5 次,并使用指数退避加短抖动;排障时优先看 `attempt`、`max_attempts`、`retry_delay_ms`、`reference_image_bytes_total` 和 `request_params`,不要把 `SendRequest` 当成上游业务错误。
- 抠图输入以私有 OSS 作为内存生命周期边界:生成原图和角色动作抽取帧上传时消费图片字节所有权,上传完成后不保留原图缓冲;手动去背景直接解析并校验已有 OSS object key,不下载原图。BgFilter 必须为 object key 签发 600 秒 GET URL 并通过 multipart `image_url` 提交,不用 `file` 重传;flat 链路进入阿里云 fallback 时由 `platform-matting` URL 接口单独下载并上传 `AuthorizeFileUpload` 临时对象,在推理前释放下载缓冲,继续 fallback 到本地键色时再单独下载一次原图,本地产出后释放本次原图下载缓冲。签名 URL 不得写入日志、审计或持久化。