diff --git a/litellm/litellm_core_utils/exception_mapping_utils.py b/litellm/litellm_core_utils/exception_mapping_utils.py index 25ae0269ab..6fc4accc8c 100644 --- a/litellm/litellm_core_utils/exception_mapping_utils.py +++ b/litellm/litellm_core_utils/exception_mapping_utils.py @@ -6,6 +6,7 @@ import httpx import litellm from litellm._logging import verbose_logger +from litellm.types.utils import LlmProviders from ..exceptions import ( APIConnectionError, @@ -1168,9 +1169,8 @@ def exception_type( # type: ignore # noqa: PLR0915 exception_status_code=original_exception.status_code, ) elif ( - custom_llm_provider == "vertex_ai" - or custom_llm_provider == "vertex_ai_beta" - or custom_llm_provider == "gemini" + custom_llm_provider == LlmProviders.VERTEX_AI + or custom_llm_provider == LlmProviders.VERTEX_AI_BETA ): if ( "Vertex AI API has not been used in project" in error_str @@ -1360,7 +1360,7 @@ def exception_type( # type: ignore # noqa: PLR0915 llm_provider=custom_llm_provider, model=model, ) - elif custom_llm_provider == "palm" or custom_llm_provider == "gemini": + elif custom_llm_provider == "palm" or custom_llm_provider == LlmProviders.GEMINI: if "503 Getting metadata" in error_str: # auth errors look like this # 503 Getting metadata from plugin failed with error: Reauthentication is needed. Please run `gcloud auth application-default login` to reauthenticate. @@ -1417,6 +1417,62 @@ def exception_type( # type: ignore # noqa: PLR0915 llm_provider="palm", response=getattr(original_exception, "response", None), ) + if original_exception.status_code == 401: + exception_mapping_worked = True + raise AuthenticationError( + message=f"GeminiException - {error_str}", + llm_provider=custom_llm_provider, + model=model, + response=getattr(original_exception, "response", None), + ) + if original_exception.status_code == 403: + exception_mapping_worked = True + raise PermissionDeniedError( + message=f"GeminiException - {error_str}", + llm_provider=custom_llm_provider, + model=model, + response=getattr(original_exception, "response", None), + ) + if original_exception.status_code == 404: + exception_mapping_worked = True + raise NotFoundError( + message=f"GeminiException - {error_str}", + llm_provider=custom_llm_provider, + model=model, + response=getattr(original_exception, "response", None), + ) + if original_exception.status_code == 408: + exception_mapping_worked = True + raise Timeout( + message=f"GeminiException - {error_str}", + llm_provider=custom_llm_provider, + model=model, + exception_status_code=original_exception.status_code, + ) + if original_exception.status_code == 429: + exception_mapping_worked = True + raise RateLimitError( + message=f"GeminiException - {error_str}", + model=model, + llm_provider=custom_llm_provider, + response=getattr(original_exception, "response", None), + ) + if original_exception.status_code == 500: + exception_mapping_worked = True + raise InternalServerError( + message=f"GeminiException - {error_str}", + model=model, + llm_provider=custom_llm_provider, + response=getattr(original_exception, "response", None), + ) + if original_exception.status_code >= 500: + exception_mapping_worked = True + raise InternalServerError( + message=f"GeminiException - {error_str}", + model=model, + llm_provider=custom_llm_provider, + response=getattr(original_exception, "response", None), + ) # Dailed: Error occurred: 400 Request payload size exceeds the limit: 20000 bytes elif custom_llm_provider == "cloudflare": if "Authentication error" in error_str: diff --git a/tests/llm_translation/test_gemini.py b/tests/llm_translation/test_gemini.py index 8fcc85aba8..5107fa0e6f 100644 --- a/tests/llm_translation/test_gemini.py +++ b/tests/llm_translation/test_gemini.py @@ -3,8 +3,6 @@ import sys import pytest -from litellm.utils import supports_url_context - sys.path.insert( 0, os.path.abspath("../..") ) # Adds the parent directory to the system paths @@ -834,3 +832,132 @@ def test_gemini_reasoning_effort_minimal(): # The important part is that our known models work correctly print(f"Note: Unknown model test skipped due to: {e}") pass + + +def test_gemini_exception_message_format(): + """ + Test that Gemini provider exceptions show as 'GeminiException' not 'VertexAIException'. + + This addresses issue #14586 where Gemini API errors were incorrectly showing as + VertexAIException instead of GeminiException due to incorrect exception mapping. + """ + import httpx + from unittest.mock import Mock + from litellm.litellm_core_utils.exception_mapping_utils import exception_type + from litellm import BadRequestError + + # Mock a typical Gemini API error response + mock_response = Mock(spec=httpx.Response) + mock_response.status_code = 400 + mock_response.text = "Invalid API key provided" + mock_response.headers = {} + + # Create a mock exception that simulates a Gemini API error + mock_exception = httpx.HTTPStatusError( + message="Bad Request", + request=Mock(), + response=mock_response + ) + mock_exception.response = mock_response + mock_exception.status_code = 400 + + # Test the exception mapping for Gemini provider + try: + exception_type( + model="gemini-pro", + original_exception=mock_exception, + custom_llm_provider="gemini", + completion_kwargs={}, + extra_kwargs={} + ) + # Should not reach here - exception should be raised + assert False, "Expected BadRequestError to be raised" + except BadRequestError as e: + # The test should FAIL initially (before fix) because it will show VertexAIException + # After the fix, it should show GeminiException + error_message = str(e) + print(f"Error message: {error_message}") # For debugging + + # This assertion will initially FAIL - that's expected for TDD + assert "GeminiException" in error_message, ( + f"Expected 'GeminiException' in error message, got: {error_message}. " + f"This test should fail before the fix is implemented." + ) + assert "VertexAIException" not in error_message, ( + f"Should not contain 'VertexAIException' in error message, got: {error_message}" + ) + + +@pytest.mark.parametrize("status_code,expected_exception", [ + (400, "BadRequestError"), + (401, "AuthenticationError"), + (403, "PermissionDeniedError"), + (404, "NotFoundError"), + (408, "Timeout"), + (429, "RateLimitError"), + (500, "InternalServerError"), + (502, "InternalServerError"), + (503, "InternalServerError"), +]) +def test_gemini_comprehensive_error_handling(status_code, expected_exception): + """ + Test comprehensive Gemini error handling for all HTTP status codes. + + This ensures that Gemini API errors of different types are properly mapped + to the correct LiteLLM exception types with GeminiException prefix. + """ + import httpx + from unittest.mock import Mock + from litellm.litellm_core_utils.exception_mapping_utils import exception_type + from litellm.exceptions import ( + BadRequestError, AuthenticationError, PermissionDeniedError, NotFoundError, + Timeout, RateLimitError, InternalServerError + ) + + # Mock the appropriate error response + mock_response = Mock(spec=httpx.Response) + mock_response.status_code = status_code + mock_response.text = f"API Error {status_code}" + mock_response.headers = {} + + # Create a mock exception + mock_exception = httpx.HTTPStatusError( + message=f"HTTP {status_code}", + request=Mock(), + response=mock_response + ) + mock_exception.response = mock_response + mock_exception.status_code = status_code + + # Test the exception mapping + try: + exception_type( + model="gemini-pro", + original_exception=mock_exception, + custom_llm_provider="gemini", + completion_kwargs={}, + extra_kwargs={} + ) + assert False, f"Expected {expected_exception} to be raised for status {status_code}" + except Exception as e: + # Verify the correct exception type is raised + exception_classes = { + "BadRequestError": BadRequestError, + "AuthenticationError": AuthenticationError, + "PermissionDeniedError": PermissionDeniedError, + "NotFoundError": NotFoundError, + "Timeout": Timeout, + "RateLimitError": RateLimitError, + "InternalServerError": InternalServerError, + } + expected_class = exception_classes[expected_exception] + assert isinstance(e, expected_class), f"Expected {expected_exception}, got {type(e).__name__}" + + # Verify the error message contains GeminiException + error_message = str(e) + assert "GeminiException" in error_message, ( + f"Expected 'GeminiException' in error message for status {status_code}, got: {error_message}" + ) + assert "VertexAIException" not in error_message, ( + f"Should not contain 'VertexAIException' for status {status_code}, got: {error_message}" + )