Skip to content

fix(gateway): propagate upstream stream termination to chat completions clients - #487

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

qxcnm merged 4 commits into
qxcnm:mainfrom
shiziyang2000-bit:fix/stream-error-propagation

Conversation

@shiziyang2000-bit

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

Copy link
Copy Markdown
Contributor

当上游 Responses 流提前结束、读取失败或超时,chat-completions 客户端此前只能看到静默断流。本 PR 将具体原因转换为标准 SSE error,保留此前已收到的内容,不伪造成功的 finish_reason 或 [DONE]。

失败判断限定到响应级错误事件、响应错误字段和失败状态;MCP 调用或工具列表失败及其工具错误详情不会截断随后成功的回答。显式失败、取消、不完整响应、event-only/无效 JSON 错误帧和正常文本/tool_calls 路径均有回归覆盖。

验证:cargo fmt --all -- --check、git diff --check;http_bridge 153 项、upstream 252 项全部通过(单线程执行)。包含合成 SSE 与本地传输测试,未调用外部模型或生产链路。

@shiziyang2000-bit

Copy link
Copy Markdown
Contributor Author

您好,感谢维护 Codex-Manager。

这个 PR 修的是流式回答中途结束时的错误传递:客户端只会看到 Stream ended without finish_reason,而上游的真实原因(读错误 / 空闲超时 / 提前 EOF / 断开)被丢掉。

  • 改动:ChatCompletionsFromResponses 读取器在异常结束时向下游发一帧显式 error,绝不伪造 finish_reason 或 [DONE],已收到的内容保留;正常成功路径不变。
  • 验证:本地隔离网关 + 本地故障上游做了修复前/后对照(修复前客户端只得到泛化错误;修复后得到上游真实原因),cargo test -p codexmanager-service --lib gateway::http_bridge:: 0 failed。
  • 复现方式与证据都写在 PR 描述里。

想麻烦看下是否可以合入,并在下一个版本里带上?如果实现位置或风格需要调整,我可以按你们的意见改。

@qxcnm

qxcnm commented Oct 3, 2026

Copy link
Copy Markdown
Owner

这项修复有实际价值:上游提前结束、读取失败或空闲超时时,把具体原因传给 chat 客户端,并保留已经收到的内容,比静默断流更便于排障。审查当前 head fd37ed43fae39cdfc855a841acc0d430ab71e47d 后,暂缓合并,主要需要修复下面的响应终止误判。

已实际复现:单个 MCP 工具失败会截断随后成功的回答。 chat_completions.rs:442 使用 Generic SSE 分类器,并在第 451–465、494 行将其错误结果直接转为结束;而 aggregate/sse_frame.rs:120 把所有 *.failed 都判为终止错误。

以下三个事件依次输入这个 reader:

data: {"type":"response.mcp_call.failed","item_id":"mcp_review","output_index":0,"sequence_number":1}

data: {"type":"response.output_text.delta","delta":"The tool is unavailable; here is the fallback answer."}

data: {"type":"response.completed","response":{"id":"resp_review","status":"completed","usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}}

预期是保留后续回答,输出 finish_reason: "stop" 和 [DONE],collector 没有响应级错误。独立工作区新增的 review_mcp_tool_failure_does_not_abort_completed_response 已实际运行,退出码 101;当前实现只输出 upstream_response_failed,后续回答丢失。官方 response.mcp_call.failed 文档 定义的是单个工具调用失败,不能替代整个 response.failed 的语义。

建议按以下步骤修复:

  1. 将此 reader 的失败判断限定为响应级终止事件和明确的响应级失败状态,例如 error、response.failed、response.incomplete、取消事件,以及 response.status 的失败状态。可以增加专用判断或复用 Responses 专用解析,再保留 event-only、无效 JSON 错误帧的处理;避免直接使用任意 *.failed 后缀作为结束依据,也不要把工具子事件的错误当作整轮失败。
  2. 把上述反例加入常规回归测试,同时覆盖 response.mcp_list_tools.failed 后继续完成的情况。保留现有 EOF、读错误、idle timeout、显式响应失败、正常文本和 tool_calls 测试;真正响应失败仍只输出错误,不伪造成功结束。
  3. 格式化新增代码,并在最新 main 上重新跑相关检查。当前 Rust CI 在 cargo fmt --all -- --check 阶段失败,尚不能作为 Rust 测试通过的证据。

必要验证命令如下;第一条测试需要将上述反例保留在 PR 中,修复后应通过:

cargo test --locked -p codexmanager-service --lib review_mcp_tool_failure_does_not_abort_completed_response -- --nocapture --test-threads=1
cargo test --locked -p codexmanager-service --lib gateway::http_bridge:: -- --test-threads=1
cargo test --locked -p codexmanager-service --lib gateway::upstream:: -- --test-threads=1
cargo fmt --all -- --check
git diff --check

本次没有调用外部模型。失败证据来自实际执行的合成 SSE reader 回归,不代表生产链路已验证;修改后还应保留成功文本/工具流与非流式桥接的回归覆盖。

@qxcnm

qxcnm commented Oct 10, 2026

Copy link
Copy Markdown
Owner

已直接补齐修复并同步当前 main,提交 48d5cf72b375f8646fa5f2c6fe9228982b818ba5。

  • 限定响应级失败判断,response.mcp_call.failed / response.mcp_list_tools.failed 及其工具错误详情不再截断后续成功回答。
  • 新增 MCP 失败回归,覆盖带/不带错误详情、带/不带 SSE event 行;保留 EOF、读错误、超时、明确响应失败及正常文本/tool_calls/非流式转换覆盖。
  • http_bridge 153/153、upstream 252/252 通过;格式及 diff 检查通过,最新提交的全部 CI 通过。

审查通过,可以合并。验证来自合成 SSE 和本地上游传输测试,没有调用外部模型或生产账户。

@qxcnm
qxcnm merged commit fabd7ba 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