fix(responses): emit terminal error event on MCP tool-execution / follow-up failures - #32566
Conversation
Greptile SummaryThis PR fixes silent stream terminations in the MCP streaming path by ensuring that failures during the initial LLM call, tool execution, or follow-up LLM call all surface as a terminal OpenAI-style
Confidence Score: 5/5Safe to merge — the change is tightly scoped to MCP streaming failure paths and adds no new happy-path logic. All three failure scenarios are correctly handled and tested. The eager-creation guard prevents double LLM calls, the _error_event_emitted flag prevents duplicate terminal events, and no existing test semantics were weakened. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/responses/mcp/mcp_streaming_iterator.py | Core fix: adds _stream_error/_initial_creation_error/_error_event_emitted fields, _make_stream_error_event helper, and wires all three failure scenarios to emit a terminal OpenAI-style error event; also skips the doomed follow-up call when tool execution has already failed. |
| litellm/responses/main.py | Eagerly calls _create_initial_response_iterator() before returning the streaming response so a pre-stream failure becomes a regular HTTP error instead of an HTTP 200 with a broken stream; checks _initial_creation_error and re-raises it if set. |
| tests/test_litellm/responses/mcp/test_mcp_streaming_iterator.py | New test file covering all three failure paths plus happy-path regression guard; all tests use monkeypatch mocks with no real network calls. |
| litellm/proxy/dev_config.yaml | Adds deepwiki MCP server entry for local development and happy-path e2e testing. |
Reviews (2): Last reviewed commit: "fix(responses): emit terminal error even..." | Re-trigger Greptile
| return ErrorEvent( | ||
| type=ResponsesAPIStreamEvents.ERROR, | ||
| sequence_number=0, | ||
| error=ErrorEventError( |
There was a problem hiding this comment.
The
sequence_number for the terminal error event is hardcoded to 0, but other events emitted earlier in the same stream carry positive, incrementing sequence numbers. Clients that enforce monotonically-increasing sequence ordering (or that use the sequence number to deduplicate/reorder events) will see an out-of-order value. Adding a simple counter (e.g. self._next_sequence_number) that is bumped each time an event is yielded and reused here would keep the stream contract intact.
| return ErrorEvent( | |
| type=ResponsesAPIStreamEvents.ERROR, | |
| sequence_number=0, | |
| error=ErrorEventError( | |
| return ErrorEvent( | |
| type=ResponsesAPIStreamEvents.ERROR, | |
| sequence_number=self._next_sequence_number, | |
| error=ErrorEventError( |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
| """Build an OpenAI-style `error` stream event from the stashed internal | ||
| failure, so clients receive a real terminal error instead of a stream | ||
| that silently ends mid-flow.""" | ||
| from litellm.types.llms.openai import ErrorEvent, ErrorEventError |
Merging this PR will degrade performance by 11.8%
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing |
…emitting a broken stream When the initial LLM call inside MCPEnhancedStreamingIterator fails (e.g. an invalid previous_response_id -> provider 400 'No tool output found for function call ...'), the proxy returned HTTP 200 and the stream emitted the pre-generated mcp_list_tools discovery events with no response.created before them. That violates the Responses API streaming contract and crashes SDK stream accumulators (openai-node: "expected 'response.created' event, got response.mcp_list_tools.in_progress"). - aresponses_api_with_mcp now makes the initial call eagerly, before any SSE bytes are written, and re-raises the stashed failure so the client gets a real 4xx/5xx with the provider error body. - If a creation failure still surfaces during iteration, the stream emits a single terminal 'error' event instead of discovery events. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…low-up failures When tool execution failed as a batch, the stream proceeded to a follow-up call carrying function_call items with no outputs — rejected by the provider with 'No tool output found for function call ...' — and when the follow-up call itself failed, the stream simply ended with no terminal event. In both cases the client received HTTP 200 and a stream that looks like a truncated success: tool events, then silence. - Stash tool-execution and follow-up failures on the iterator. - Skip the doomed follow-up call entirely after a tool-execution failure. - Emit a single terminal OpenAI-style 'error' stream event carrying the mapped failure instead of ending silently. Builds on the initial-call failure handling from the previous commit (shares the _stream_error stash and _make_stream_error_event helper). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bd9e139 to
70656be
Compare
|
|
|
Rebased onto
|
|
Consolidated into #32579 (all three MCP gateway fail-loudly fixes in one PR, with the review feedback from this PR incorporated). |
Relevant issues
Fixes #32562
Stacked on #32565 (shares the
_stream_errorstash and_make_stream_error_eventhelper) — review the last commit only; will rebase once #32565 lands.Pre-Submission checklist
Screenshots / Proof of Fix
These failures require fault injection mid-stream (batch tool-execution failure / follow-up provider failure), so the before/after captures drive the real
MCPEnhancedStreamingIteratorwith only the failing dependency mocked; the happy-path proof is fully e2e with real LLM + MCP calls.Before (main @
999637883c), tools succeed then follow-up call 400s — stream ends with no terminal event:Batch tool-execution failure additionally attempted the doomed follow-up call (
follow-up model call attempted: True), which the provider rejects with"No tool output found for function call ...".After (this branch @
bd9e13914e) — terminalerrorevent, and the doomed follow-up is skipped:Happy-path regression proof, fully e2e (this branch @
bd9e13914e, real proxy + real OpenAI + real deepwiki MCP call): full two-phase flow (response.created×2, executedmcp_call, finaloutput_text), 0errorevents.Type
🐛 Bug Fix
Changes
function_callitems with no outputs and be rejected by the provider).errorstream event carrying the mapped failure instead of ending the stream silently.Tests:
tests/test_litellm/responses/mcp/test_mcp_streaming_iterator.py— tool-execution failure emits error event and skips follow-up, follow-up failure emits error event, multi-round happy path stays error-free. Fulltests/test_litellm/responses/suite passes (343 tests).