Skip to content

fix: propagate passthrough stream errors and handle spill cleanup races - #491

Merged
qxcnm merged 5 commits into
qxcnm:mainfrom
shiziyang2000-bit:fix/passthrough-stream-error
Oct 10, 2026
Merged

qxcnm merged 5 commits into
qxcnm:mainfrom
shiziyang2000-bit:fix/passthrough-stream-error

Conversation

@shiziyang2000-bit

@shiziyang2000-bit shiziyang2000-bit commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

上游 Chat/Generic 或 Anthropic 原生透传流提前结束、读错误或超时时,客户端此前只得到静默断流。本 PR 保留已交付的内容并发送明确错误:Generic 使用 OpenAI SSE error,AnthropicNative 使用原生 event: error。

  • 有效但未闭合的 JSON 尾帧补齐事件分隔,半截 JSON 尾部丢弃;生成的错误可由真实 SSE 解析器独立读取。
  • EOF、读取错误、idle timeout、通道断开统一处理,文本/tool_calls/Anthropic message_stop 已正常终止后不追加错误。
  • 工具/MCP 的 completed/failed/error 不替代响应终止。真正的响应失败、生命周期错误字段和 failed/incomplete/cancelled 状态仍优先于完成事件名。
  • 变更针对 PassthroughSseUsageReader;原生 /v1/responses 使用独立 reader,本 PR 不宣称修复所有 Responses 透传路径。
  • CI 触发了既有日志溢写枚举与后台清理的竞态。独立提交仅跳过目录枚举后已消失文件的 metadata NotFound,其他 I/O 错误继续返回,并增加 Unix 真实删除时序回归。

验证:passthrough 48 项、http_bridge 155 项通过,两项本地网关 Anthropic message_stop 与 Chat tool_calls 集成通过,格式及 diff 检查通过。新增流测试使用生产异步 pump 和 eventsource-stream 解析,覆盖两种协议、尾部换行/半截 JSON、四种异常结束、文本/工具正常结束、MCP 子事件及矛盾的完成状态。最终 core、联合工作区和 CI 结果见维护者评论;没有调用外部模型或生产账户。

@shiziyang2000-bit

Copy link
Copy Markdown
Contributor Author

您好,感谢维护。

这个 PR 是 #487 的同类问题在 Passthrough 分支(原生 /v1/responses、/v1/chat/completions 直通)上的修复:上游没有终止事件就断开时,之前同样是静默关闭,客户端只能看到半截内容加 Stream ended without finish_reason。

  • 改动:PassthroughSseUsageReader 在 EOF / 读错误 / 空闲超时 / 通道断开时发一帧显式错误(同帧兼容 Responses 与 chat 两种形状),保留已收到内容,绝不伪造成功标记;正常路径不变。
  • 验证:新增 3 个定向回归;passthrough 37 passed、gateway::http_bridge:: 142 passed,0 failed;本地真实链路对照(修复前:泛化错误;修复后:上游真实原因)。
  • 生产 7 天日志里该分支确实出现过 terminal_seen=false 的样本,所以不是理论问题。

想麻烦评估是否能合入,并随下一个版本发布?需要调整我随时改。

@qxcnm

qxcnm commented Oct 3, 2026

Copy link
Copy Markdown
Owner

透传断流时向客户端发送明确错误有实际价值,尤其能保留已收到的内容并说明失败原因。审查当前 head ff5753d3735cfa9424ab173a48733ae7e72daaeb 后,暂缓合并,主要需要补齐事件边界和终止状态处理。

  1. 错误帧可能与未闭合的尾帧合并,客户端仍读不到有效错误。 common.rs:187 在 EOF 会把没有空行结束的尾数据作为 Frame 发出;passthrough.rs:195 原样转发后,第 212 行追加新 data: 错误帧。例如上游最后只发送 data: {"choices":[{"delta":{"content":"partial"},"finish_reason":null}]}\n,没有第二个换行,新增错误就成为同一 SSE event 的第二个 data 行;按 SSE 规则合并后的 data 是两个 JSON 文档,JSON 解析失败。末尾连换行都没有时,则直接形成 }data:。这项已按源码对应的输出规则构造 wire 并验证 JSON 解析失败;尚未运行真实网关或新增 reader 回归。
  2. 有效终止后仍可能追加错误。 passthrough.rs:197 的 EOF 分支检查了 terminal_seen,但第 217–248 行的读取错误、idle timeout、通道断开分支没有同样处理。已经转发有效 finish_reason、[DONE] 或 Anthropic message_stop 后,随后网络异常会再输出错误帧;对仍等待 usage 或流结束的客户端可能造成成功后报错。这是源码审查结论,尚未执行对应时序回归。
  3. 需要按协议生成 AnthropicNative 错误。 passthrough.rs:162 的新错误生成器没有根据 self.protocol 分支,一律发送仅含 data: 的 OpenAI 形状;这个 reader 同时支持 AnthropicNative。建议为该模式使用 event: error 和对应的原生错误 payload,并验证客户端能识别。当前只是协议/源码审查,未做 Anthropic 客户端端到端验证。

建议按以下步骤修复:

  1. 在这个 reader 内识别没有闭合的尾事件,确保合成错误属于独立事件。对于仍是完整、可解析 payload 的尾帧,可以补足事件分隔再发送;对于半截 JSON,丢弃尚未构成有效事件的尾部,再发送独立错误,保留之前完整转发的内容。仅在半截 JSON 后补空行会让客户端先解析无效 JSON,仍达不到目的。至少测试尾部没有换行、只有一个换行、完整空行,以及半截 JSON,断流后的错误必须能由真正的 SSE 解析器独立读取。不要只检查输出字符串里出现了 "error"。
  2. 统一 EOF、读错误、idle timeout 和断联的结束逻辑:有效响应级终止已转发时,不再补造错误;尚未终止时只发送一次错误。增加正常文本/tool_calls 结束后接 transport error 的时序测试,并覆盖正常 Anthropic message_stop。
  3. 按 PassthroughSseProtocol 生成错误,补充 Generic 和 AnthropicNative 的失败解析测试;现有 partial-content 保留与成功流原样传递测试继续保留。
  4. 修正范围说明并格式化代码。当前 delivery.rs:846 的原生 /v1/responses 使用 OpenAIResponsesPassthroughSseReader,本 PR 改的是另一 reader;请将描述/评论中的“两种原生端点均已修复”收窄到实际命中的路径。这是范围边界,无需为此扩大本 PR 到所有 reader。当前 Rust CI 也停在新增测试的格式检查,需要恢复绿灯。

必要验证命令:

cargo test --locked -p codexmanager-service --lib passthrough -- --test-threads=1
cargo test --locked -p codexmanager-service --lib gateway::http_bridge:: -- --test-threads=1
cargo test --locked -p codexmanager-service --test gateway_logs gateway_aggregate_compatible_messages_passthrough_accepts_message_stop -- --test-threads=1
cargo test --locked -p codexmanager-service --test gateway_logs hybrid_aggregate_only_streams_chat_completions_tool_calls_from_aggregate_api -- --test-threads=1
cargo fmt --all -- --check
git diff --check

上述新增边界用例还需要实际执行;建议再用本地故障上游通过网关验证 chat 透传的正常完成、工具完成、未闭合尾帧和断流错误,并用实际 SSE 客户端解析结果。PR 作者此前提供的本地验证说明有参考价值,但本次审查没有独立复跑该链路,也未调用外部模型。

@qxcnm qxcnm changed the title fix(gateway): propagate upstream stream termination on passthrough streams fix: propagate passthrough stream errors and handle spill cleanup races Oct 10, 2026
@qxcnm

qxcnm commented Oct 10, 2026

Copy link
Copy Markdown
Owner

已补齐修复并推送原 PR 分支,最终提交 7b5b2f216eb9f75813bf64479bf536273beaefd3,复审通过。

  • 完整可解析的未闭合尾 JSON 补齐 SSE 分隔,半截 JSON 丢弃;错误属于独立事件,保留此前完整内容。
  • EOF、读错误、空闲超时、断联统一处理;正常文本、tool_calls、Anthropic message_stop 结束后不会追加错误。
  • Generic 与 AnthropicNative 分别生成对应协议错误。工具/MCP completed/failed/error 不替代整轮终止;真正的响应错误或失败状态仍优先。
  • CI 暴露了既有日志溢写枚举与清理线程的文件删除竞态,已用独立提交修复:只跳过 metadata NotFound,其他 I/O 错误继续返回,并新增 Unix 真实删除时序回归。

验证:流修复的 passthrough 48/48、http_bridge 155/155 通过;本地网关 Anthropic message_stop 与 Chat tool_calls 两项集成通过;格式和 diff 检查通过。最终 head 的三个 CI 全部成功:run 38055714103。已核对 Linux 日志,新增删除时序用例和原失败 queue_clear 用例均实际执行通过。

联合检查中的四项模型数量旧断言已在当前 main 逐项复现,并由 #492 补齐;四项修复的完整联合验证继续在 #492 收尾。本 PR 的修复范围已在描述中明确为 PassthroughSseUsageReader,不宣称覆盖原生 /v1/responses 的所有 reader。没有调用外部模型或生产账户。

该改动能让客户端正确识别透传断流原因,符合项目需要,可以合并。

@qxcnm
qxcnm merged commit e6775a5 into qxcnm:main Oct 10, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants