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:
Shivaang 2026-02-27 23:30:01 -05:00
parent bffce842a1
commit 8a9c14cbe4
2 changed files with 182 additions and 0 deletions

View file

@ -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,

View file

@ -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