Token Pool 断流修复记录 2026-04-28
- 状态:已修复并完成线上验证
- 服务:
Token PoolOpenAI 网关流式转发链路 - 代码仓库:
E:\My Project\Token Pool - 相关提交:
5fc2eb5、af17f53 - 验证请求:
29cbe9b2-1f8a-4c11-8f57-ef6e6a0ad206、4969a9b5-9b1f-4b76-96fa-06218998210b - 结论:这次不是网络真断流,而是“流被正常结束但不完整”
过程摘要
- 用户反馈的现象不是
EPIPE、socket close 或 Codex 崩溃,而是回答中途毫无征兆停止。 - 排查先回到统一 stream 包装层
src/utils/common.js,确认是否由网关自身提前判定结束。 - 第一条根因定位到
handleStreamRequest()对每一次nativeIterator.next()都套了 timeout,导致已开始输出的流在中途 pause 时被误判失败。 - 第二条根因定位到 OpenAI Chat 流里把
finish_reason误当成 transport-level 结束,最终跳过了data: [DONE]。 - 两条修复分别落在提交
5fc2eb5和af17f53,并在部署后对 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
修复流程
- 先区分“真断流”与“伪断流”,确认不是网络连接层问题。
- 回到统一 stream wrapper,检查是不是网关自己把流结束掉。
- 锁定
handleStreamRequest()的 timeout 策略,修正为只限制首包。 - 再检查 OpenAI Chat 协议尾部,确认
finish_reason与[DONE]被错误混用。 - 为两条问题各自补充单元测试,避免后续回归。
- 推送并部署修复版本,再做 live streaming 验证。
验证结果
本地验证
执行通过的关键测试:
tests/stream-midflight-stall.unit.test.jstests/openai-stream-done.unit.test.jstests/openai-responses-core.unit.test.jstests/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 streamCompleted:true- 已看到
response.completed
Chat Completions 链路:
- requestId:
4969a9b5-9b1f-4b76-96fa-06218998210b - stopReason:
stream_completed streamCompleted:true- 已看到正文 chunk、
finish_reason和最终data: [DONE]
排查结论沉淀
后续再遇到“回答突然闭嘴”类问题,建议按这个顺序判断:
- 先看是否存在
EPIPE、socket close、abort 等真断流证据。 - 如果 provider 流已经开始输出,先怀疑 gateway 是否误杀了中途停顿。
- 再核对协议终止信号是否完整:
- OpenAI Chat 需要
finish_reason+data: [DONE] - Responses 需要
response.completed/response.incomplete/error
- OpenAI Chat 需要
- 最后才回到 provider 尾包 flush、synthesized completion、
finally destroy等次级因素。
关联文档
- 详细运行记录已同步写入
E:\My Project\Token Pool\docs\STABILITY-RUNBOOK.md - 本记录用于在 Obsidian agent docs 中保存一份可检索的修复摘要