Skip to content

fix(responses): emit terminal error event on MCP tool-execution / follow-up failures - #32566

Closed
thibault-linktree wants to merge 2 commits into
BerriAI:litellm_oss_stagingfrom
thibault-linktree:litellm_fix_mcp_gateway_midstream_failure_error_event
Closed

fix(responses): emit terminal error event on MCP tool-execution / follow-up failures#32566
thibault-linktree wants to merge 2 commits into
BerriAI:litellm_oss_stagingfrom
thibault-linktree:litellm_fix_mcp_gateway_midstream_failure_error_event

Conversation

@thibault-linktree

Copy link
Copy Markdown
Contributor

Relevant issues

Fixes #32562

Stacked on #32565 (shares the _stream_error stash and _make_stream_error_event helper) — review the last commit only; will rebase once #32565 lands.

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review

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 MCPEnhancedStreamingIterator with 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:

response.output_item.added
response.mcp_list_tools.in_progress
response.mcp_list_tools.completed
response.completed            (initial response: output=[function_call])
response.mcp_call.in_progress
response.mcp_call_arguments.delta
response.mcp_call_arguments.done
response.mcp_call.completed
response.output_item.done
<< stream ended >>

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) — terminal error event, and the doomed follow-up is skipped:

... same events as above ...
error  code=400 message=litellm.BadRequestError: {"error": {"message": "No tool output found for function call ..."}}
<< stream ended >>
follow-up model call attempted: False   (tool-execution failure case)

Happy-path regression proof, fully e2e (this branch @ bd9e13914e, real proxy + real OpenAI + real deepwiki MCP call): full two-phase flow (response.created ×2, executed mcp_call, final output_text), 0 error events.

Type

🐛 Bug Fix

Changes

  • Stash tool-execution and follow-up failures on the iterator instead of dropping them after the stderr log.
  • After a batch tool-execution failure, skip the follow-up call entirely (it would carry function_call items with no outputs and be rejected by the provider).
  • When the follow-up cannot start, emit a single terminal OpenAI-style error stream 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. Full tests/test_litellm/responses/ suite passes (343 tests).

@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This 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 error event rather than an abrupt end-of-stream; it also eagerly surfaces initial-call failures as HTTP-level errors before any SSE bytes are written.

  • mcp_streaming_iterator.py: introduces _stream_error/_initial_creation_error/_error_event_emitted fields and a _make_stream_error_event helper; wires all three failure paths to emit the error event and skips the doomed follow-up call when tool execution has already failed.
  • responses/main.py: calls _create_initial_response_iterator() eagerly before returning the streaming response so an invalid previous_response_id (or other pre-stream failure) becomes a normal HTTP 4xx/5xx instead of an HTTP 200 with a broken stream.
  • New tests in tests/test_litellm/responses/mcp/test_mcp_streaming_iterator.py cover all three failure scenarios and a happy-path regression guard using monkeypatched mocks (no real network calls).

Confidence Score: 5/5

Safe 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.

Important Files Changed

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

Comment on lines +398 to +401
return ErrorEvent(
type=ResponsesAPIStreamEvents.ERROR,
sequence_number=0,
error=ErrorEventError(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
return ErrorEvent(
type=ResponsesAPIStreamEvents.ERROR,
sequence_number=0,
error=ErrorEventError(
return ErrorEvent(
type=ResponsesAPIStreamEvents.ERROR,
sequence_number=self._next_sequence_number,
error=ErrorEventError(

@codecov

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.96774% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/responses/main.py 0.00% 8 Missing ⚠️
litellm/responses/mcp/mcp_streaming_iterator.py 95.65% 1 Missing ⚠️

📢 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
@codspeed-hq

codspeed-hq Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will degrade performance by 11.8%

❌ 1 regressed benchmark
✅ 29 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
test_completion_simple_message 4.2 ms 4.8 ms -11.8%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing thibault-linktree:litellm_fix_mcp_gateway_midstream_failure_error_event (bd9e139) with main (9996378)

Open in CodSpeed

thibault-linktree and others added 2 commits July 9, 2026 14:43
…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>
@thibault-linktree
thibault-linktree force-pushed the litellm_fix_mcp_gateway_midstream_failure_error_event branch from bd9e139 to 70656be Compare July 9, 2026 04:50
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
4 out of 5 committers have signed the CLA.

✅ fernando-izar
✅ seph-barker
✅ Sameerlite
✅ thibault-linktree
❌ cursoragent
You have signed the CLA already but the status is still pending? Let us recheck it.

@thibault-linktree
thibault-linktree changed the base branch from main to litellm_oss_staging July 9, 2026 04:50
@thibault-linktree

Copy link
Copy Markdown
Contributor Author

Rebased onto litellm_oss_staging (still stacked on #32565 — review the last commit only). Review feedback addressed:

  • CodeQL cyclic import: ErrorEvent/ErrorEventError now imported at module level alongside the other litellm.types.llms.openai imports (no new function-level import)
  • On sequence_number: kept the constant 1 rather than a per-iterator counter — the terminal error event is fatal by definition and every SDK we tested treats error events positionally (openai-node raises immediately without consulting sequence numbers); threading a monotonic counter through the passthrough chunks would touch every yield site for no observable behaviour change. Happy to add the counter if maintainers prefer.
  • The CodSpeed regression flag (test_completion_simple_message, 4.2ms → 4.8ms) looks like simulation noise: this PR touches only mcp_streaming_iterator.py failure paths, which are not on the completion code path.

@greptileai

@thibault-linktree

Copy link
Copy Markdown
Contributor Author

Consolidated into #32579 (all three MCP gateway fail-loudly fixes in one PR, with the review feedback from this PR incorporated).

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.

3 participants