AGENT

token-pool-stream-truncation-fix-2026-04-28

2026/04/28 9 min read AGENT AUTH TOKEN POOL STREAM TRUNCATION FIX

Token Pool 断流修复记录 2026-04-28

  • 状态:已修复并完成线上验证
  • 服务:Token Pool OpenAI 网关流式转发链路
  • 代码仓库:E:\My Project\Token Pool
  • 相关提交:5fc2eb5af17f53
  • 验证请求:29cbe9b2-1f8a-4c11-8f57-ef6e6a0ad2064969a9b5-9b1f-4b76-96fa-06218998210b
  • 结论:这次不是网络真断流,而是“流被正常结束但不完整”

过程摘要

  1. 用户反馈的现象不是 EPIPE、socket close 或 Codex 崩溃,而是回答中途毫无征兆停止。
  2. 排查先回到统一 stream 包装层 src/utils/common.js,确认是否由网关自身提前判定结束。
  3. 第一条根因定位到 handleStreamRequest() 对每一次 nativeIterator.next() 都套了 timeout,导致已开始输出的流在中途 pause 时被误判失败。
  4. 第二条根因定位到 OpenAI Chat 流里把 finish_reason 误当成 transport-level 结束,最终跳过了 data: [DONE]
  5. 两条修复分别落在提交 5fc2eb5af17f53,并在部署后对 Responses 与 Chat Completions 两条链路做了 live 验证。

故障现象

  • Codex / Responses 请求会在正文未完成时直接停止。
  • 客户端通常没有收到显式网络报错。
  • 某些请求会表现为中途失败 completion,或者 stop reason 不是预期的完整结束。
  • OpenAI Chat 流中虽然已经出现 finish_reason: "stop",但客户端仍等不到最终 data: [DONE]

根因

根因 1:mid-stream 停顿被统一包装层误杀

定位文件:E:\My Project\Token Pool\src\utils\common.js

  • 原实现对每次 nativeIterator.next() 都应用 timeout。
  • 这对“首包超时”合理,但对长推理、tool 调用、pause 型模型的中途停顿不合理。
  • 一旦 provider 已经开始输出,后续暂时沉默只代表模型仍在处理,不代表流坏掉。
  • 网关却把这类停顿走进失败分支,主动发送失败 completion,客户端看到的就变成“正常结束但没说完”。

修复动作:

  • 只保留首包 timeout。
  • 流一旦开始输出,就不再用统一 idle timeout 主动截断中途停顿。
  • 增加回归测试 tests/stream-midflight-stall.unit.test.js

对应提交:5fc2eb5

根因 2:finish_reason 被误当成已经发送过 [DONE]

定位文件:E:\My Project\Token Pool\src\utils\common.js

  • OpenAI Chat 的协议里,terminal chunk 与 transport-level 结束标记不是同一件事。
  • finish_reason: "stop" 只表示正文 chunk 已结束,不等于已经发送了 data: [DONE]
  • 原逻辑在看到 finish_reason 后把结束标记提前置位,导致 finally 阶段不再补发 [DONE]
  • 严格客户端因此把这次流视为“不完整结束”。

修复动作:

  • finish_reason[DONE] 的语义拆开处理。
  • 对 OpenAI Chat 流保留 finally 阶段的 [DONE] 补发逻辑。
  • 增加回归测试 tests/openai-stream-done.unit.test.js

对应提交:af17f53

修复流程

  1. 先区分“真断流”与“伪断流”,确认不是网络连接层问题。
  2. 回到统一 stream wrapper,检查是不是网关自己把流结束掉。
  3. 锁定 handleStreamRequest() 的 timeout 策略,修正为只限制首包。
  4. 再检查 OpenAI Chat 协议尾部,确认 finish_reason[DONE] 被错误混用。
  5. 为两条问题各自补充单元测试,避免后续回归。
  6. 推送并部署修复版本,再做 live streaming 验证。

验证结果

本地验证

执行通过的关键测试:

  • tests/stream-midflight-stall.unit.test.js
  • tests/openai-stream-done.unit.test.js
  • tests/openai-responses-core.unit.test.js
  • tests/codex-core.unit.test.js

部署验证

  • 提交 5fc2eb5 对应部署 workflow run:25036848829
  • 提交 af17f53 对应部署 workflow run:25037354728
  • 健康检查:https://pool-console.tengokukk.com/api/health 返回 200

线上流式验证

Responses 链路:

  • requestId:29cbe9b2-1f8a-4c11-8f57-ef6e6a0ad206
  • stopReason:stream_completed
  • streamCompletedtrue
  • 已看到 response.completed

Chat Completions 链路:

  • requestId:4969a9b5-9b1f-4b76-96fa-06218998210b
  • stopReason:stream_completed
  • streamCompletedtrue
  • 已看到正文 chunk、finish_reason 和最终 data: [DONE]

排查结论沉淀

后续再遇到“回答突然闭嘴”类问题,建议按这个顺序判断:

  1. 先看是否存在 EPIPE、socket close、abort 等真断流证据。
  2. 如果 provider 流已经开始输出,先怀疑 gateway 是否误杀了中途停顿。
  3. 再核对协议终止信号是否完整:
    • OpenAI Chat 需要 finish_reason + data: [DONE]
    • Responses 需要 response.completed / response.incomplete / error
  4. 最后才回到 provider 尾包 flush、synthesized completion、finally destroy 等次级因素。

关联文档

  • 详细运行记录已同步写入 E:\My Project\Token Pool\docs\STABILITY-RUNBOOK.md
  • 本记录用于在 Obsidian agent docs 中保存一份可检索的修复摘要