mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-02 02:11:58 +00:00
Cover Responses stream error events in CI path
This commit is contained in:
parent
4b9f465082
commit
f46231ddf0
2 changed files with 171 additions and 179 deletions
|
|
@ -19,20 +19,16 @@ from datetime import datetime
|
|||
from typing import Any, Dict, Optional
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../.."))
|
||||
|
||||
import litellm
|
||||
from litellm.constants import STREAM_SSE_DONE_STRING
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig
|
||||
from litellm.responses.streaming_iterator import BaseResponsesAPIStreamingIterator
|
||||
from litellm.responses.utils import ResponsesAPIRequestUtils
|
||||
from litellm.types.llms.openai import (
|
||||
ErrorEvent,
|
||||
ErrorEventError,
|
||||
ResponseCompletedEvent,
|
||||
ResponseFailedEvent,
|
||||
ResponseIncompleteEvent,
|
||||
|
|
@ -578,181 +574,6 @@ class TestBaseResponsesAPIStreamingIterator:
|
|||
submit_args = mock_executor.submit.call_args
|
||||
assert submit_args[0][0] == mock_logging_obj.failure_handler
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_error_event_raises_litellm_exception(self):
|
||||
"""
|
||||
OpenAI can send a top-level `error` event before response.failed. The
|
||||
iterator should raise that as a LiteLLM exception instead of yielding it
|
||||
as a normal stream chunk.
|
||||
"""
|
||||
from litellm.responses.streaming_iterator import ResponsesAPIStreamingIterator
|
||||
|
||||
error_chunk = {
|
||||
"type": "error",
|
||||
"sequence_number": 2,
|
||||
"error": {
|
||||
"type": "invalid_request_error",
|
||||
"code": "context_length_exceeded",
|
||||
"message": "Input exceeds the model context window.",
|
||||
"param": "input",
|
||||
},
|
||||
}
|
||||
|
||||
async def mock_aiter_bytes():
|
||||
yield f"data: {json.dumps(error_chunk)}\n\n".encode("utf-8")
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.headers = {}
|
||||
mock_response.aiter_bytes = mock_aiter_bytes
|
||||
|
||||
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_config.transform_streaming_response.return_value = ErrorEvent(
|
||||
type=ResponsesAPIStreamEvents.ERROR,
|
||||
sequence_number=2,
|
||||
error=ErrorEventError(
|
||||
type="invalid_request_error",
|
||||
code="context_length_exceeded",
|
||||
message="Input exceeds the model context window.",
|
||||
param="input",
|
||||
),
|
||||
)
|
||||
|
||||
iterator = ResponsesAPIStreamingIterator(
|
||||
response=mock_response,
|
||||
model="gpt-5.4-mini",
|
||||
responses_api_provider_config=mock_config,
|
||||
logging_obj=mock_logging_obj,
|
||||
custom_llm_provider="openai",
|
||||
)
|
||||
|
||||
with (
|
||||
pytest.raises(litellm.ContextWindowExceededError) as exc_info,
|
||||
patch(
|
||||
"litellm.responses.streaming_iterator.run_async_function"
|
||||
) as mock_run_async,
|
||||
patch("litellm.responses.streaming_iterator.executor") as mock_executor,
|
||||
):
|
||||
await iterator.__anext__()
|
||||
|
||||
assert "context window" in str(exc_info.value)
|
||||
assert iterator.finished is True
|
||||
mock_run_async.assert_called_once()
|
||||
assert (
|
||||
mock_run_async.call_args.kwargs["async_function"]
|
||||
== mock_logging_obj.async_failure_handler
|
||||
)
|
||||
mock_executor.submit.assert_called_once()
|
||||
|
||||
def test_sync_streaming_error_event_raises_litellm_exception(self):
|
||||
"""
|
||||
Sync Responses API streams should raise top-level `error` events too.
|
||||
"""
|
||||
from litellm.responses.streaming_iterator import (
|
||||
SyncResponsesAPIStreamingIterator,
|
||||
)
|
||||
|
||||
error_chunk = {
|
||||
"type": "error",
|
||||
"sequence_number": 2,
|
||||
"error": {
|
||||
"type": "rate_limit_error",
|
||||
"code": "rate_limit_exceeded",
|
||||
"message": "Too many requests.",
|
||||
"param": None,
|
||||
},
|
||||
}
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.headers = {}
|
||||
mock_response.iter_bytes.return_value = [
|
||||
f"data: {json.dumps(error_chunk)}\n\n".encode("utf-8")
|
||||
]
|
||||
|
||||
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_config.transform_streaming_response.return_value = ErrorEvent(
|
||||
type=ResponsesAPIStreamEvents.ERROR,
|
||||
sequence_number=2,
|
||||
error=ErrorEventError(
|
||||
type="rate_limit_error",
|
||||
code="rate_limit_exceeded",
|
||||
message="Too many requests.",
|
||||
param=None,
|
||||
),
|
||||
)
|
||||
|
||||
iterator = SyncResponsesAPIStreamingIterator(
|
||||
response=mock_response,
|
||||
model="gpt-5.4-mini",
|
||||
responses_api_provider_config=mock_config,
|
||||
logging_obj=mock_logging_obj,
|
||||
custom_llm_provider="openai",
|
||||
)
|
||||
|
||||
with (
|
||||
pytest.raises(litellm.RateLimitError),
|
||||
patch(
|
||||
"litellm.responses.streaming_iterator.run_async_function"
|
||||
) as mock_run_async,
|
||||
patch("litellm.responses.streaming_iterator.executor") as mock_executor,
|
||||
):
|
||||
next(iterator)
|
||||
|
||||
assert iterator.finished is True
|
||||
mock_run_async.assert_called_once()
|
||||
mock_executor.submit.assert_called_once()
|
||||
|
||||
def test_error_event_exception_mapping(self):
|
||||
"""Provider error metadata should keep useful LiteLLM exception types."""
|
||||
mock_logging_obj = Mock(spec=LiteLLMLoggingObj)
|
||||
mock_logging_obj.model_call_details = {"litellm_params": {}}
|
||||
iterator = BaseResponsesAPIStreamingIterator(
|
||||
response=httpx.Response(
|
||||
400, request=httpx.Request("POST", "https://api.example.test")
|
||||
),
|
||||
model="gpt-5.4-mini",
|
||||
responses_api_provider_config=Mock(spec=BaseResponsesAPIConfig),
|
||||
logging_obj=mock_logging_obj,
|
||||
custom_llm_provider="openai",
|
||||
)
|
||||
|
||||
auth_event = ErrorEvent(
|
||||
type=ResponsesAPIStreamEvents.ERROR,
|
||||
sequence_number=1,
|
||||
error=ErrorEventError(
|
||||
type="authentication_error",
|
||||
code="invalid_api_key",
|
||||
message="Invalid API key.",
|
||||
param=None,
|
||||
),
|
||||
)
|
||||
default_event = ErrorEvent(
|
||||
type=ResponsesAPIStreamEvents.ERROR,
|
||||
sequence_number=2,
|
||||
error=ErrorEventError(
|
||||
type="invalid_request_error",
|
||||
code="bad_request",
|
||||
message="Bad request.",
|
||||
param="input",
|
||||
),
|
||||
)
|
||||
|
||||
assert isinstance(
|
||||
iterator._exception_from_error_event(auth_event),
|
||||
litellm.AuthenticationError,
|
||||
)
|
||||
assert isinstance(
|
||||
iterator._exception_from_error_event(default_event),
|
||||
litellm.BadRequestError,
|
||||
)
|
||||
|
||||
def test_process_chunk_response_incomplete_calls_success_handler(self):
|
||||
"""
|
||||
Test that a RESPONSE_INCOMPLETE event routes to success handlers.
|
||||
|
|
|
|||
171
tests/test_litellm/responses/test_streaming_error_events.py
Normal file
171
tests/test_litellm/responses/test_streaming_error_events.py
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import json
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig
|
||||
from litellm.responses.streaming_iterator import (
|
||||
BaseResponsesAPIStreamingIterator,
|
||||
ResponsesAPIStreamingIterator,
|
||||
SyncResponsesAPIStreamingIterator,
|
||||
)
|
||||
from litellm.types.llms.openai import (
|
||||
ErrorEvent,
|
||||
ErrorEventError,
|
||||
ResponsesAPIStreamEvents,
|
||||
)
|
||||
|
||||
|
||||
def _mock_logging_obj() -> Mock:
|
||||
logging_obj = Mock(spec=LiteLLMLoggingObj)
|
||||
logging_obj.model_call_details = {"litellm_params": {}}
|
||||
logging_obj.async_failure_handler = Mock()
|
||||
logging_obj.failure_handler = Mock()
|
||||
return logging_obj
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_responses_stream_error_event_raises_litellm_exception():
|
||||
error_chunk = {
|
||||
"type": "error",
|
||||
"sequence_number": 2,
|
||||
"error": {
|
||||
"type": "invalid_request_error",
|
||||
"code": "context_length_exceeded",
|
||||
"message": "Input exceeds the model context window.",
|
||||
"param": "input",
|
||||
},
|
||||
}
|
||||
|
||||
async def mock_aiter_bytes():
|
||||
yield f"data: {json.dumps(error_chunk)}\n\n".encode("utf-8")
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.headers = {}
|
||||
mock_response.aiter_bytes = mock_aiter_bytes
|
||||
mock_logging_obj = _mock_logging_obj()
|
||||
mock_config = Mock(spec=BaseResponsesAPIConfig)
|
||||
mock_config.transform_streaming_response.return_value = ErrorEvent(
|
||||
type=ResponsesAPIStreamEvents.ERROR,
|
||||
sequence_number=2,
|
||||
error=ErrorEventError(
|
||||
type="invalid_request_error",
|
||||
code="context_length_exceeded",
|
||||
message="Input exceeds the model context window.",
|
||||
param="input",
|
||||
),
|
||||
)
|
||||
iterator = ResponsesAPIStreamingIterator(
|
||||
response=mock_response,
|
||||
model="gpt-5.4-mini",
|
||||
responses_api_provider_config=mock_config,
|
||||
logging_obj=mock_logging_obj,
|
||||
custom_llm_provider="openai",
|
||||
)
|
||||
|
||||
with (
|
||||
pytest.raises(litellm.ContextWindowExceededError),
|
||||
patch(
|
||||
"litellm.responses.streaming_iterator.run_async_function"
|
||||
) as mock_run_async,
|
||||
patch("litellm.responses.streaming_iterator.executor") as mock_executor,
|
||||
):
|
||||
await iterator.__anext__()
|
||||
|
||||
assert iterator.finished is True
|
||||
mock_run_async.assert_called_once()
|
||||
mock_executor.submit.assert_called_once()
|
||||
|
||||
|
||||
def test_sync_responses_stream_error_event_raises_litellm_exception():
|
||||
error_chunk = {
|
||||
"type": "error",
|
||||
"sequence_number": 2,
|
||||
"error": {
|
||||
"type": "rate_limit_error",
|
||||
"code": "rate_limit_exceeded",
|
||||
"message": "Too many requests.",
|
||||
"param": None,
|
||||
},
|
||||
}
|
||||
|
||||
mock_response = Mock()
|
||||
mock_response.headers = {}
|
||||
mock_response.iter_bytes.return_value = [
|
||||
f"data: {json.dumps(error_chunk)}\n\n".encode("utf-8")
|
||||
]
|
||||
mock_logging_obj = _mock_logging_obj()
|
||||
mock_config = Mock(spec=BaseResponsesAPIConfig)
|
||||
mock_config.transform_streaming_response.return_value = ErrorEvent(
|
||||
type=ResponsesAPIStreamEvents.ERROR,
|
||||
sequence_number=2,
|
||||
error=ErrorEventError(
|
||||
type="rate_limit_error",
|
||||
code="rate_limit_exceeded",
|
||||
message="Too many requests.",
|
||||
param=None,
|
||||
),
|
||||
)
|
||||
iterator = SyncResponsesAPIStreamingIterator(
|
||||
response=mock_response,
|
||||
model="gpt-5.4-mini",
|
||||
responses_api_provider_config=mock_config,
|
||||
logging_obj=mock_logging_obj,
|
||||
custom_llm_provider="openai",
|
||||
)
|
||||
|
||||
with (
|
||||
pytest.raises(litellm.RateLimitError),
|
||||
patch(
|
||||
"litellm.responses.streaming_iterator.run_async_function"
|
||||
) as mock_run_async,
|
||||
patch("litellm.responses.streaming_iterator.executor") as mock_executor,
|
||||
):
|
||||
next(iterator)
|
||||
|
||||
assert iterator.finished is True
|
||||
mock_run_async.assert_called_once()
|
||||
mock_executor.submit.assert_called_once()
|
||||
|
||||
|
||||
def test_responses_stream_error_event_exception_mapping_fallbacks():
|
||||
iterator = BaseResponsesAPIStreamingIterator(
|
||||
response=httpx.Response(
|
||||
400, request=httpx.Request("POST", "https://api.example.test")
|
||||
),
|
||||
model="gpt-5.4-mini",
|
||||
responses_api_provider_config=Mock(spec=BaseResponsesAPIConfig),
|
||||
logging_obj=_mock_logging_obj(),
|
||||
custom_llm_provider="openai",
|
||||
)
|
||||
|
||||
auth_event = ErrorEvent(
|
||||
type=ResponsesAPIStreamEvents.ERROR,
|
||||
sequence_number=1,
|
||||
error=ErrorEventError(
|
||||
type="authentication_error",
|
||||
code="invalid_api_key",
|
||||
message="Invalid API key.",
|
||||
param=None,
|
||||
),
|
||||
)
|
||||
default_event = ErrorEvent(
|
||||
type=ResponsesAPIStreamEvents.ERROR,
|
||||
sequence_number=2,
|
||||
error=ErrorEventError(
|
||||
type="invalid_request_error",
|
||||
code="bad_request",
|
||||
message="Bad request.",
|
||||
param="input",
|
||||
),
|
||||
)
|
||||
|
||||
assert isinstance(
|
||||
iterator._exception_from_error_event(auth_event), litellm.AuthenticationError
|
||||
)
|
||||
assert isinstance(
|
||||
iterator._exception_from_error_event(default_event), litellm.BadRequestError
|
||||
)
|
||||
Loading…
Add table
Reference in a new issue