📡 fix: Explain Model Streams That Die Mid-Response - #16415
Conversation
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a706dec4a7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review the latest head |
|
Codex Review: Didn't find any major issues. Delightful! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Summary
When a model provider accepts an agent request (HTTP 200) and the response then dies in transit, the turn ends with "The model provider could not complete this request." followed by the raw transport word
terminated. Two very different failures produce that same text, and neither tells the reader what happened or what to do.The first is a provider hanging up mid-stream. The DeepSeek report in #16372 shows
api.deepseek.comclosing the connection after about 4 minutes of reasoning ("other side closed"). The second is a provider that stops sending anything while keeping the connection open. On a production deployment, an OpenAI-compatible gateway's/v1/responsesreturned 200 and then sent nothing until LibreChat's own 15-minute model response body timeout (modelResponseBodyTimeoutMs, #16201) cut it off. That happened three times in 30 hours, each costing a user a 15-minute wait.This change classifies both failures from the undici error in the transport error's cause chain and gives each its own typed payload:
model_stream_closedandmodel_stream_stalled. The client renders each as a localized sentence that says what happened, that anything already received was kept, and what to try next. The terminal log line now carrieserrorType: stream_closedorstream_stalledin place of_OTHER, so log queries can tell these failures apart from other unclassified ones. A user pressing Stop still goes down the cancellation path unchanged, because an abort never carries these codes.Automatically retrying a call that died mid-stream needs a change in
@librechat/agents: the SDK owns the model call and has no hook that can reset the partially streamed step. That is left to a follow-up.Related to #16372
How it works
Fetch reports both failures as the same
TypeError: terminated. Only the undici error in itscauseseparates them:getModelStreamFailurewalks a bounded cause chain (the same depth and hostile-accessor handling asisStepLimitError). A stall wins over a close, because the timeout is what ended the request. The terminal observer only consults it after the existing LangChain classification, and only for errors the model callback tracked, so failures outside the model call keep their current handling.Type of change
Testing
The classifier is tested against real transport failures, not hand-built error shapes. A local HTTP server sends one SSE event and then either destroys the socket, goes silent past a 150 ms undici body timeout, or is aborted by the caller. The resulting errors are checked when read raw, and when streamed through
ChatOpenAIfrom@librechat/agentsfor both Chat Completions and the Responses API (the gateway's path). The abort case confirms that Stop stays unclassified.Tested environments/configuration:
@librechat/agents3.9.6Automated tests:
packages/api:npx jest src/agents/errors.spec.ts src/agents/failures(104 passed)client:npx jest src/components/Messages/Content/__tests__/Error.spec.tsx(140 passed)api:npx jest server/controllers/agents/client.test.js -t "upstream|Upstream|provider"(24 passed)npx tsc --noEmitinpackages/apiandclient, and eslint on the changed filesScreenshots / recordings
Not captured. The two new types render through the existing single-sentence error copy path, the same one as
model_rate_limit, with no layout or styling change. Both cases are added to the seeded error gallery (config/create-error-convo.js) for a visual check.