mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
Merge pull request #19649 from BerriAI/litellm_fix_responses_api_logging_eror
Fix: Responses API logging error for StopIteration
This commit is contained in:
commit
a240eb7630
2 changed files with 126 additions and 0 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue