diff --git a/litellm/responses/main.py b/litellm/responses/main.py index a627531e994..dfec5606b03 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -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, diff --git a/tests/test_litellm/responses/test_responses_exception_handling.py b/tests/test_litellm/responses/test_responses_exception_handling.py new file mode 100644 index 00000000000..f07ed932dc3 --- /dev/null +++ b/tests/test_litellm/responses/test_responses_exception_handling.py @@ -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