保真模式下流以 finish_reason+usage 正常结束但缺 [DONE],被判失败并重试到耗尽(ClinDB-ReviewBench、dissect3) #31

Closed
opened 2026-10-15 09:57:36 +08:00 by iomgaa · 1 comment
Owner

背景

版本:polygateway 1.3.9。ClinDB-ReviewBench 与 dissect3 都用 chat(..., response_mode="openai") 流式调用(pi 经本地代理,见 #29)。与 #30 同一类:保真模式把上游的正常结局当成失败去重试;本条是另一种情形。

现象

经一个 OpenAI 兼容中转站(magpie,模型名 group/auto-glm-5-3-flash,上游 glm-5.3-flash)流式调用时,上游经常这样结束:

  • 最后一块里 choices[0].finish_reason 为 stop / length / tool_calls,同一块带完整 usage;
  • 随后连接正常关闭(干净 EOF,没有读取错误),不发 data: [DONE]。

此时 transports/openai_compat.py:668-671 抛 TransientError("保真响应缺 [DONE],禁止打捞")。这条判断不看 SourceConfig.missing_done;错误可重试,于是重拨到 max_attempts 耗尽,最后报 AllSourcesExhausted ... retry_exhausted。

证据(两个项目独立观察)

  • ClinDB-ReviewBench:curl 直连该中转站,glm 流式回复在 finish_reason 为 stop 和 length 时都没有 [DONE](十来次,一次都没有);经 PolyGateway 的 glm 调用全部失败,一次在 max_attempts=10 下挂了 1216 秒。同一中转站上 minimax 的回复带 [DONE],正常。
  • dissect3:20 个运行里 glm 成功 4672 次、最终失败 466 次,其中 38 次的最终原因是本条(每次已重拨 10 次)。独立探针(max_tokens: 50、include_usage):HTTP 200,末块为 {"choices":[{"index":0,"finish_reason":"length","delta":{...}}],"usage":{...}},随后断开,无 [DONE]。

所以这个上游有时发 [DONE]、有时不发;不发的那些在 PolyGateway 里一律失败。

为什么算缺陷

  1. 回复是完整的。 结束原因与用量都到了,连接正常关闭,不是中途断流。magpie 对 Chat 流明确采用同一口径(internal/gateway/gateway.go:4238:「A Chat stream may end without [DONE] and be whole, so only a read that failed counts」);OpenAI 官方 SDK 也以连接结束作为流结束。
  2. 重试救不回来,还白花钱。 上游这次不发 [DONE],重拨后很可能还是不发;每次重拨都生成一份完整回复、照常计费、又被丢掉,而调用方因为最终判失败,这些用量一条都记不到。
  3. 调用方无从配置。 missing_done 在 openai 模式下不生效。

建议的修法

openai 模式下,没有收到 [DONE] 时,同时满足以下三点即判为完整、正常交回:

  • 每个 choice 都收到了非空 finish_reason;
  • 收到了 usage(PolyGateway 已强制 stream_options.include_usage,openai_compat.py:423,所以总能检查);
  • 迭代因干净 EOF 结束,不是 httpx 读取异常,也不是活性超时。

以下维持现状、仍判失败,不放宽「禁止打捞」:没有 finish_reason 就断;有 finish_reason 无 usage;读取出错;收到 error 帧。

建议在结果里标出结束方式,例如 stream_end: "done" | "eof_after_finish",让调用方能记录和统计,不悄悄混在一起。

验收(假 SSE 服务即可复现,不花钱)

  1. 末块 finish_reason: "stop" + usage,然后关闭,无 [DONE]:成功,completion 完整,用量按实测,stream_end="eof_after_finish",attempts=1。
  2. 同 1,finish_reason: "tool_calls",带完整工具调用:成功,工具调用完整。
  3. 只有内容块、无 finish_reason 即关闭:仍失败(与现状同)。
  4. 有 finish_reason、无 usage 即关闭:失败。
  5. 中途读取出错(httpx 抛 RemoteProtocolError 等):失败。
  6. 带 [DONE] 的正常流:行为不变,stream_end="done"。
## 背景 版本:polygateway 1.3.9。ClinDB-ReviewBench 与 dissect3 都用 `chat(..., response_mode="openai")` 流式调用(pi 经本地代理,见 #29)。与 #30 同一类:保真模式把上游的正常结局当成失败去重试;本条是另一种情形。 ## 现象 经一个 OpenAI 兼容中转站(magpie,模型名 `group/auto-glm-5-3-flash`,上游 glm-5.3-flash)流式调用时,上游经常这样结束: - 最后一块里 `choices[0].finish_reason` 为 `stop` / `length` / `tool_calls`,同一块带完整 `usage`; - 随后连接正常关闭(干净 EOF,没有读取错误),**不发 `data: [DONE]`**。 此时 `transports/openai_compat.py:668-671` 抛 `TransientError("保真响应缺 [DONE],禁止打捞")`。这条判断不看 `SourceConfig.missing_done`;错误可重试,于是重拨到 `max_attempts` 耗尽,最后报 `AllSourcesExhausted ... retry_exhausted`。 ## 证据(两个项目独立观察) - **ClinDB-ReviewBench**:curl 直连该中转站,glm 流式回复在 `finish_reason` 为 `stop` 和 `length` 时都没有 `[DONE]`(十来次,一次都没有);经 PolyGateway 的 glm 调用全部失败,一次在 `max_attempts=10` 下挂了 1216 秒。同一中转站上 minimax 的回复带 `[DONE]`,正常。 - **dissect3**:20 个运行里 glm 成功 4672 次、最终失败 466 次,其中 38 次的最终原因是本条(每次已重拨 10 次)。独立探针(`max_tokens: 50`、`include_usage`):HTTP 200,末块为 `{"choices":[{"index":0,"finish_reason":"length","delta":{...}}],"usage":{...}}`,随后断开,无 `[DONE]`。 所以这个上游有时发 `[DONE]`、有时不发;不发的那些在 PolyGateway 里一律失败。 ## 为什么算缺陷 1. **回复是完整的。** 结束原因与用量都到了,连接正常关闭,不是中途断流。magpie 对 Chat 流明确采用同一口径(`internal/gateway/gateway.go:4238`:「A Chat stream may end without [DONE] and be whole, so only a read that failed counts」);OpenAI 官方 SDK 也以连接结束作为流结束。 2. **重试救不回来,还白花钱。** 上游这次不发 `[DONE]`,重拨后很可能还是不发;每次重拨都生成一份完整回复、照常计费、又被丢掉,而调用方因为最终判失败,这些用量一条都记不到。 3. **调用方无从配置。** `missing_done` 在 openai 模式下不生效。 ## 建议的修法 openai 模式下,没有收到 `[DONE]` 时,**同时满足**以下三点即判为完整、正常交回: - 每个 choice 都收到了非空 `finish_reason`; - 收到了 `usage`(PolyGateway 已强制 `stream_options.include_usage`,`openai_compat.py:423`,所以总能检查); - 迭代因干净 EOF 结束,不是 httpx 读取异常,也不是活性超时。 以下维持现状、仍判失败,不放宽「禁止打捞」:没有 `finish_reason` 就断;有 `finish_reason` 无 `usage`;读取出错;收到 error 帧。 建议在结果里标出结束方式,例如 `stream_end: "done" | "eof_after_finish"`,让调用方能记录和统计,不悄悄混在一起。 ## 验收(假 SSE 服务即可复现,不花钱) 1. 末块 `finish_reason: "stop"` + `usage`,然后关闭,无 `[DONE]`:成功,`completion` 完整,用量按实测,`stream_end="eof_after_finish"`,`attempts=1`。 2. 同 1,`finish_reason: "tool_calls"`,带完整工具调用:成功,工具调用完整。 3. 只有内容块、无 `finish_reason` 即关闭:仍失败(与现状同)。 4. 有 `finish_reason`、无 `usage` 即关闭:失败。 5. 中途读取出错(httpx 抛 `RemoteProtocolError` 等):失败。 6. 带 `[DONE]` 的正常流:行为不变,`stream_end="done"`。
Author
Owner

已在 1.3.10 修复并发布(registry 可安装:polygateway==1.3.10;Release:https://gitea.iomgaa.online/iomgaa/PolyGateway/releases/tag/v1.3.10)。与 #30 同根,统一修复。

修法:流完整性改以上游声明为准——收到 [DONE] → stream_end="done";未发 [DONE] 但 finish_reason 已到、其同帧或之后收到的 usage 为实测、连接干净关闭 → 完整,stream_end="eof_after_finish",用量记 measured。两种模式都修(文本模式默认 MISSING_DONE=retry 原本同样重试到耗尽);missing_done 只管真截断。读取异常/活性超时/error 帧照旧失败;finish 为 network_error/error 仍判瞬时。LLMResponse.stream_end 与遥测 stream_end 列可统计结束方式。

验收(假 SSE,先红后绿):issue 6 条全部覆盖(tests/unit/test_completion_transport.py),另加"usage 早于 finish""finish 后 usage 为 {}""network_error"及经 GatewayClient 的 attempts=1。全套件 2000 passed(main 合并后)。

真实中转对照(magpie / group/auto-glm-5-3-flash,2026-10-15):

1.3.9 1.3.10
原始线路 12 次 200 响应均无 [DONE],finish 后带 usage 同
文本模式 常规 2/2 AllSourcesExhausted retry_exhausted(最长 220s) 6/6 成功,eof_after_finish,measured
保真模式 常规 2/2 AllSourcesExhausted 6/6 成功,eof_after_finish

(1.3.10 有 2 次 attempts=2 是首次尝试撞 TIMEOUT_S=180 的上游卡顿,同期直连亦有一次 ReadTimeout,属正常超时重试。)

设计 research-wiki/designs/2026-10-14-issue30-31-completion-verdict-design.md。

已在 **1.3.10** 修复并发布(registry 可安装:`polygateway==1.3.10`;Release:https://gitea.iomgaa.online/iomgaa/PolyGateway/releases/tag/v1.3.10)。与 #30 同根,统一修复。 **修法**:流完整性改以上游声明为准——收到 `[DONE]` → `stream_end="done"`;未发 `[DONE]` 但 finish_reason 已到、其同帧或之后收到的 usage 为实测、连接干净关闭 → 完整,`stream_end="eof_after_finish"`,用量记 measured。**两种模式都修**(文本模式默认 `MISSING_DONE=retry` 原本同样重试到耗尽);`missing_done` 只管真截断。读取异常/活性超时/error 帧照旧失败;finish 为 `network_error`/`error` 仍判瞬时。`LLMResponse.stream_end` 与遥测 `stream_end` 列可统计结束方式。 **验收(假 SSE,先红后绿)**:issue 6 条全部覆盖(`tests/unit/test_completion_transport.py`),另加"usage 早于 finish""finish 后 usage 为 {}""network_error"及经 GatewayClient 的 attempts=1。全套件 2000 passed(main 合并后)。 **真实中转对照(magpie / group/auto-glm-5-3-flash,2026-10-15)**: | | 1.3.9 | 1.3.10 | |---|---|---| | 原始线路 | 12 次 200 响应均无 `[DONE]`,finish 后带 usage | 同 | | 文本模式 常规 | 2/2 `AllSourcesExhausted retry_exhausted`(最长 220s) | 6/6 成功,`eof_after_finish`,measured | | 保真模式 常规 | 2/2 `AllSourcesExhausted` | 6/6 成功,`eof_after_finish` | (1.3.10 有 2 次 attempts=2 是首次尝试撞 `TIMEOUT_S=180` 的上游卡顿,同期直连亦有一次 ReadTimeout,属正常超时重试。) 设计 `research-wiki/designs/2026-10-14-issue30-31-completion-verdict-design.md`。
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: iomgaa/PolyGateway#31