mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
fix(responses): skip double exception mapping for already-typed errors
The Responses API bridge wraps litellm.completion(), which already maps provider exceptions to litellm types (BadRequestError, NotFoundError, etc.). The catch-all except blocks in responses/main.py then pass these already-mapped exceptions through exception_type() again unnecessarily. Add an isinstance guard before each exception_type() call in all 12 except blocks so litellm exceptions are re-raised directly, avoiding the redundant trip through the 2000+ line mapping function. Non-litellm exceptions (TypeError, ValueError from transformation code) still go through exception_type() as before. This matches the same guard pattern used inside exception_type() itself (exception_mapping_utils.py lines 240-244). Fixes #22121 (addresses suggested fix 1 of 4)
This commit is contained in:
parent
bffce842a1
commit
8a9c14cbe4
2 changed files with 182 additions and 0 deletions
|
|
@ -515,6 +515,8 @@ async def aresponses(
|
|||
|
||||
return response
|
||||
except Exception as e:
|
||||
if any(isinstance(e, exc_type) for exc_type in litellm.LITELLM_EXCEPTION_TYPES):
|
||||
raise e
|
||||
raise litellm.exception_type(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
|
|
@ -776,6 +778,8 @@ def responses(
|
|||
|
||||
return response
|
||||
except Exception as e:
|
||||
if any(isinstance(e, exc_type) for exc_type in litellm.LITELLM_EXCEPTION_TYPES):
|
||||
raise e
|
||||
raise litellm.exception_type(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
|
|
@ -841,6 +845,8 @@ async def adelete_responses(
|
|||
response = init_response
|
||||
return response
|
||||
except Exception as e:
|
||||
if any(isinstance(e, exc_type) for exc_type in litellm.LITELLM_EXCEPTION_TYPES):
|
||||
raise e
|
||||
raise litellm.exception_type(
|
||||
model=None,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
|
|
@ -936,6 +942,8 @@ def delete_responses(
|
|||
|
||||
return response
|
||||
except Exception as e:
|
||||
if any(isinstance(e, exc_type) for exc_type in litellm.LITELLM_EXCEPTION_TYPES):
|
||||
raise e
|
||||
raise litellm.exception_type(
|
||||
model=None,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
|
|
@ -1015,6 +1023,8 @@ async def aget_responses(
|
|||
)
|
||||
return response
|
||||
except Exception as e:
|
||||
if any(isinstance(e, exc_type) for exc_type in litellm.LITELLM_EXCEPTION_TYPES):
|
||||
raise e
|
||||
raise litellm.exception_type(
|
||||
model=None,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
|
|
@ -1124,6 +1134,8 @@ def get_responses(
|
|||
|
||||
return response
|
||||
except Exception as e:
|
||||
if any(isinstance(e, exc_type) for exc_type in litellm.LITELLM_EXCEPTION_TYPES):
|
||||
raise e
|
||||
raise litellm.exception_type(
|
||||
model=None,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
|
|
@ -1186,6 +1198,8 @@ async def alist_input_items(
|
|||
response = init_response
|
||||
return response
|
||||
except Exception as e:
|
||||
if any(isinstance(e, exc_type) for exc_type in litellm.LITELLM_EXCEPTION_TYPES):
|
||||
raise e
|
||||
raise litellm.exception_type(
|
||||
model=None,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
|
|
@ -1271,6 +1285,8 @@ def list_input_items(
|
|||
|
||||
return response
|
||||
except Exception as e:
|
||||
if any(isinstance(e, exc_type) for exc_type in litellm.LITELLM_EXCEPTION_TYPES):
|
||||
raise e
|
||||
raise litellm.exception_type(
|
||||
model=None,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
|
|
@ -1336,6 +1352,8 @@ async def acancel_responses(
|
|||
response = init_response
|
||||
return response
|
||||
except Exception as e:
|
||||
if any(isinstance(e, exc_type) for exc_type in litellm.LITELLM_EXCEPTION_TYPES):
|
||||
raise e
|
||||
raise litellm.exception_type(
|
||||
model=None,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
|
|
@ -1431,6 +1449,8 @@ def cancel_responses(
|
|||
|
||||
return response
|
||||
except Exception as e:
|
||||
if any(isinstance(e, exc_type) for exc_type in litellm.LITELLM_EXCEPTION_TYPES):
|
||||
raise e
|
||||
raise litellm.exception_type(
|
||||
model=None,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
|
|
@ -1509,6 +1529,8 @@ async def acompact_responses(
|
|||
|
||||
return response
|
||||
except Exception as e:
|
||||
if any(isinstance(e, exc_type) for exc_type in litellm.LITELLM_EXCEPTION_TYPES):
|
||||
raise e
|
||||
raise litellm.exception_type(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
|
|
@ -1644,6 +1666,8 @@ def compact_responses(
|
|||
|
||||
return response
|
||||
except Exception as e:
|
||||
if any(isinstance(e, exc_type) for exc_type in litellm.LITELLM_EXCEPTION_TYPES):
|
||||
raise e
|
||||
raise litellm.exception_type(
|
||||
model=model,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,158 @@
|
|||
"""
|
||||
Tests for Responses API exception handling.
|
||||
|
||||
Verifies that exceptions already mapped by litellm.completion() are not
|
||||
double-mapped when they propagate through the Responses API bridge.
|
||||
|
||||
Regression tests for https://github.com/BerriAI/litellm/issues/22121
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(
|
||||
0, os.path.abspath("../../..")
|
||||
) # Adds the parent directory to the system path
|
||||
|
||||
import litellm
|
||||
from litellm.exceptions import (
|
||||
BadRequestError,
|
||||
NotFoundError,
|
||||
RateLimitError,
|
||||
)
|
||||
|
||||
|
||||
class TestResponsesExceptionPreservation:
|
||||
"""
|
||||
Tests that the Responses API bridge re-raises litellm exceptions
|
||||
directly instead of double-mapping them through exception_type().
|
||||
"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aresponses_preserves_bad_request_error(self):
|
||||
"""
|
||||
When the completion bridge raises a BadRequestError, aresponses()
|
||||
should re-raise it as-is instead of collapsing it into
|
||||
APIConnectionError.
|
||||
"""
|
||||
original_error = BadRequestError(
|
||||
message="Invalid model parameter",
|
||||
model="test-model",
|
||||
llm_provider="openai",
|
||||
)
|
||||
|
||||
with patch(
|
||||
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config",
|
||||
return_value=None,
|
||||
), patch(
|
||||
"litellm.responses.main.litellm_completion_transformation_handler.response_api_handler",
|
||||
side_effect=original_error,
|
||||
):
|
||||
with pytest.raises(BadRequestError) as exc_info:
|
||||
await litellm.aresponses(
|
||||
model="openai/gpt-4o",
|
||||
input="test",
|
||||
)
|
||||
assert exc_info.value is original_error
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aresponses_preserves_rate_limit_error(self):
|
||||
"""
|
||||
When the completion bridge raises a RateLimitError, aresponses()
|
||||
should re-raise it as-is.
|
||||
"""
|
||||
original_error = RateLimitError(
|
||||
message="Rate limit exceeded",
|
||||
model="test-model",
|
||||
llm_provider="openai",
|
||||
)
|
||||
|
||||
with patch(
|
||||
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config",
|
||||
return_value=None,
|
||||
), patch(
|
||||
"litellm.responses.main.litellm_completion_transformation_handler.response_api_handler",
|
||||
side_effect=original_error,
|
||||
):
|
||||
with pytest.raises(RateLimitError) as exc_info:
|
||||
await litellm.aresponses(
|
||||
model="openai/gpt-4o",
|
||||
input="test",
|
||||
)
|
||||
assert exc_info.value is original_error
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aresponses_preserves_not_found_error(self):
|
||||
"""
|
||||
When the completion bridge raises a NotFoundError, aresponses()
|
||||
should re-raise it as-is.
|
||||
"""
|
||||
original_error = NotFoundError(
|
||||
message="Model not found",
|
||||
model="test-model",
|
||||
llm_provider="openai",
|
||||
)
|
||||
|
||||
with patch(
|
||||
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config",
|
||||
return_value=None,
|
||||
), patch(
|
||||
"litellm.responses.main.litellm_completion_transformation_handler.response_api_handler",
|
||||
side_effect=original_error,
|
||||
):
|
||||
with pytest.raises(NotFoundError) as exc_info:
|
||||
await litellm.aresponses(
|
||||
model="openai/gpt-4o",
|
||||
input="test",
|
||||
)
|
||||
assert exc_info.value is original_error
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aresponses_still_maps_non_litellm_exceptions(self):
|
||||
"""
|
||||
Non-litellm exceptions (e.g. ValueError from transformation code)
|
||||
should still be mapped through exception_type(). This ensures we
|
||||
only skip mapping for already-mapped exceptions.
|
||||
"""
|
||||
with patch(
|
||||
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config",
|
||||
return_value=None,
|
||||
), patch(
|
||||
"litellm.responses.main.litellm_completion_transformation_handler.response_api_handler",
|
||||
side_effect=ValueError("unexpected transformation error"),
|
||||
):
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
await litellm.aresponses(
|
||||
model="openai/gpt-4o",
|
||||
input="test",
|
||||
)
|
||||
# Should NOT be a ValueError -- it should be mapped to a litellm type
|
||||
assert not isinstance(exc_info.value, ValueError)
|
||||
|
||||
def test_responses_preserves_bad_request_error_sync(self):
|
||||
"""
|
||||
Sync variant: when the completion bridge raises a BadRequestError,
|
||||
responses() should re-raise it as-is.
|
||||
"""
|
||||
original_error = BadRequestError(
|
||||
message="Invalid model parameter",
|
||||
model="test-model",
|
||||
llm_provider="openai",
|
||||
)
|
||||
|
||||
with patch(
|
||||
"litellm.responses.main.ProviderConfigManager.get_provider_responses_api_config",
|
||||
return_value=None,
|
||||
), patch(
|
||||
"litellm.responses.main.litellm_completion_transformation_handler.response_api_handler",
|
||||
side_effect=original_error,
|
||||
):
|
||||
with pytest.raises(BadRequestError) as exc_info:
|
||||
litellm.responses(
|
||||
model="openai/gpt-4o",
|
||||
input="test",
|
||||
)
|
||||
assert exc_info.value is original_error
|
||||
Loading…
Add table
Reference in a new issue