#12687: fix: handle empty LLM stream response with failover
agents
stale
## Summary
Fixes the issue where "request ended without sending any chunks" error from @mariozechner/pi-ai was shown directly to users without proper handling.
**Problem:**
- When LLM APIs return empty SSE streams, the raw error was exposed to users
- No failover retry was triggered
- Known to occur with MiniMax, OpenAI Codex, and Anthropic providers
**Solution:**
- Added `isEmptyStreamError()` detection function
- Classified as "timeout" for failover (transient API issue)
- User-friendly message: "The AI service returned an empty response. Please try again."
- Applied to both `formatAssistantErrorText()` and `sanitizeUserFacingText()`
## Changes
- Modified: [errors.ts](src/agents/pi-embedded-helpers/errors.ts)
- Added `isEmptyStreamError()` helper (L661-663)
- Updated `classifyFailoverReason()` to trigger failover (L655-657)
- Updated `formatAssistantErrorText()` with user message (L390-392)
- Updated `sanitizeUserFacingText()` with user message (L448-450)
## Test Plan
- [x] Code compiles successfully (`bun run build`)
- [x] Verified changes in errors.ts
- [ ] Manual testing: Confirm empty stream triggers failover
- [ ] Manual testing: Confirm user sees friendly message if all failovers fail
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- greptile_comment -->
<h2>Greptile Overview</h2>
<h3>Greptile Summary</h3>
This PR adds detection for the pi-ai empty SSE stream error (`"request ended without sending any chunks"`), maps it to a transient failover reason ("timeout"), and rewrites the user-facing text to a friendlier message in both assistant-error formatting and general user-facing sanitization.
Key paths touched are `formatAssistantErrorText()` (assistant message errors), `sanitizeUserFacingText()` (generic text cleaning), and `classifyFailoverReason()` (retry/failover classification).
<h3>Confidence Score: 4/5</h3>
- Mostly safe to merge, but there is a remaining path where the raw empty-stream error can still reach users without being sanitized.
- Changes are small and localized, and the failover classification looks consistent with existing timeout handling. However, `sanitizeUserFacingText()` only rewrites the empty-stream message when the text matches ERROR_PREFIX_RE, so prefix-less errors can still leak the raw message (the primary issue being addressed).
- src/agents/pi-embedded-helpers/errors.ts
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#12314: fix: treat HTTP 5xx server errors as failover-worthy
by hsssgdtc · 2026-02-09
77.6%
#15109: fix: distinguish transient API errors from billing errors
by jwchmodx · 2026-02-13
77.1%
#21491: fix: classify Google 503 UNAVAILABLE as transient failover [AI-assi...
by ZPTDclaw · 2026-02-20
76.8%
#11821: fix(auth): trigger failover on 401 status code from expired OAuth t...
by AnonO6 · 2026-02-08
76.7%
#13820: feat(agents): retry empty-stream once before fallback
by Louise-Qiuqiu · 2026-02-11
76.4%
#4300: Gateway: prevent OpenAI-compatible client crash on SSE termination
by perryraskin · 2026-01-30
75.5%
#9173: Fix: Improve error messaging for API rate limits and billing errors
by vishaltandale00 · 2026-02-04
75.3%
#4097: fix: classify AWS SSO token errors as auth for model fallback (AI-a...
by guyelia · 2026-01-29
75.2%
#4495: Fix: emit final assistant event when reply tags hide stream
by ukeate · 2026-01-30
74.7%
#8661: fix: display rate limit errors correctly instead of as context over...
by dbottme · 2026-02-04
74.5%