From 8357d05615bdbe5bbb4f3df77d504ad0a5f52444 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Fri, 23 Jan 2026 18:42:33 +0530 Subject: [PATCH] Fix: Responses API logging error for StopIteration --- litellm/responses/streaming_iterator.py | 6 + ...t_base_responses_api_streaming_iterator.py | 120 ++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/litellm/responses/streaming_iterator.py b/litellm/responses/streaming_iterator.py index 7abd4f90f2f..6d0c4abac81 100644 --- a/litellm/responses/streaming_iterator.py +++ b/litellm/responses/streaming_iterator.py @@ -379,6 +379,9 @@ class ResponsesAPIStreamingIterator(BaseResponsesAPIStreamingIterator): return result # If result is None, continue the loop to get the next chunk + except StopAsyncIteration: + # Normal end of stream - don't log as failure + raise except httpx.HTTPError as e: # Handle HTTP errors self.finished = True @@ -474,6 +477,9 @@ class SyncResponsesAPIStreamingIterator(BaseResponsesAPIStreamingIterator): return result # If result is None, continue the loop to get the next chunk + except StopIteration: + # Normal end of stream - don't log as failure + raise except httpx.HTTPError as e: # Handle HTTP errors self.finished = True diff --git a/tests/llm_responses_api_testing/test_base_responses_api_streaming_iterator.py b/tests/llm_responses_api_testing/test_base_responses_api_streaming_iterator.py index b81161881ea..860445d875d 100644 --- a/tests/llm_responses_api_testing/test_base_responses_api_streaming_iterator.py +++ b/tests/llm_responses_api_testing/test_base_responses_api_streaming_iterator.py @@ -309,3 +309,123 @@ class TestBaseResponsesAPIStreamingIterator: pytest.fail(f"_handle_logging_completed_response failed with pickle error: {e}") raise + @pytest.mark.asyncio + async def test_stop_async_iteration_not_logged_as_failure(self): + """ + Test that StopAsyncIteration is NOT logged as a failure. + + This test verifies that when streaming completes normally with StopAsyncIteration, + the _handle_failure method is NOT called, preventing false error logs in Langfuse + and other logging integrations. + + """ + from litellm.responses.streaming_iterator import ResponsesAPIStreamingIterator + + # Mock dependencies + mock_response = Mock() + mock_response.headers = {} + + # Create an async iterator that raises StopAsyncIteration after yielding one chunk + async def mock_aiter_lines(): + yield 'data: {"type": "response.output_text.delta", "delta": "test"}' + # Normal end of stream - raise StopAsyncIteration + + mock_response.aiter_lines = mock_aiter_lines + + mock_logging_obj = Mock(spec=LiteLLMLoggingObj) + mock_logging_obj.model_call_details = {"litellm_params": {}} + mock_logging_obj.async_failure_handler = Mock() + mock_logging_obj.failure_handler = Mock() + + mock_config = Mock(spec=BaseResponsesAPIConfig) + mock_delta_event = Mock() + mock_delta_event.type = ResponsesAPIStreamEvents.OUTPUT_TEXT_DELTA + mock_delta_event.delta = "test" + mock_config.transform_streaming_response.return_value = mock_delta_event + + # Create the iterator instance + iterator = ResponsesAPIStreamingIterator( + response=mock_response, + model="gpt-4", + responses_api_provider_config=mock_config, + logging_obj=mock_logging_obj, + litellm_metadata={"model_info": {"id": "model_123"}}, + custom_llm_provider="openai" + ) + + # Consume the iterator until StopAsyncIteration + chunks_received = [] + try: + async for chunk in iterator: + chunks_received.append(chunk) + except StopAsyncIteration: + pass # This is expected + + # Verify we got the chunk + assert len(chunks_received) == 1 + + # CRITICAL: Verify that failure handlers were NOT called + # StopAsyncIteration is a normal end of stream, not a failure + mock_logging_obj.async_failure_handler.assert_not_called() + mock_logging_obj.failure_handler.assert_not_called() + + def test_stop_iteration_not_logged_as_failure(self): + """ + Test that StopIteration is NOT logged as a failure in sync iterator. + + This test verifies that when streaming completes normally with StopIteration, + the _handle_failure method is NOT called, preventing false error logs in Langfuse + and other logging integrations. + + Regression test for: https://github.com/BerriAI/litellm/issues/XXXXX + """ + from litellm.responses.streaming_iterator import SyncResponsesAPIStreamingIterator + + # Mock dependencies + mock_response = Mock() + mock_response.headers = {} + + # Create a sync iterator that raises StopIteration after yielding one chunk + def mock_iter_lines(): + yield 'data: {"type": "response.output_text.delta", "delta": "test"}' + # Normal end of stream - raise StopIteration + + mock_response.iter_lines = mock_iter_lines + + mock_logging_obj = Mock(spec=LiteLLMLoggingObj) + mock_logging_obj.model_call_details = {"litellm_params": {}} + mock_logging_obj.async_failure_handler = Mock() + mock_logging_obj.failure_handler = Mock() + + mock_config = Mock(spec=BaseResponsesAPIConfig) + mock_delta_event = Mock() + mock_delta_event.type = ResponsesAPIStreamEvents.OUTPUT_TEXT_DELTA + mock_delta_event.delta = "test" + mock_config.transform_streaming_response.return_value = mock_delta_event + + # Create the iterator instance + iterator = SyncResponsesAPIStreamingIterator( + response=mock_response, + model="gpt-4", + responses_api_provider_config=mock_config, + logging_obj=mock_logging_obj, + litellm_metadata={"model_info": {"id": "model_123"}}, + custom_llm_provider="openai" + ) + + # Consume the iterator until StopIteration + chunks_received = [] + try: + for chunk in iterator: + chunks_received.append(chunk) + except StopIteration: + pass # This is expected + + # Verify we got the chunk + assert len(chunks_received) == 1 + + # CRITICAL: Verify that failure handlers were NOT called + # StopIteration is a normal end of stream, not a failure + mock_logging_obj.async_failure_handler.assert_not_called() + mock_logging_obj.failure_handler.assert_not_called() +