From 58b713d19db17b16edcbb5a4283a7ced9e3ab7ff Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 1 Sep 2025 15:26:27 -0700 Subject: [PATCH] [Bug fix] Misclassified 500 error on invalid image_url in /chat/completions request (#14149) * add ImageFetchError * docs ImageFetchError * fix ImageFetchError * test_completion_with_invalid_image_url --- docs/my-website/docs/exception_mapping.md | 1 + litellm/__init__.py | 1 + litellm/exceptions.py | 23 +++++++++++ .../prompt_templates/image_handling.py | 13 +++--- .../litellm_core_utils/test_image_handling.py | 41 +++++++++++++++++++ 5 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 tests/test_litellm/litellm_core_utils/test_image_handling.py diff --git a/docs/my-website/docs/exception_mapping.md b/docs/my-website/docs/exception_mapping.md index 13eda5b405a..2342f444e17 100644 --- a/docs/my-website/docs/exception_mapping.md +++ b/docs/my-website/docs/exception_mapping.md @@ -12,6 +12,7 @@ All exceptions can be imported from `litellm` - e.g. `from litellm import BadReq | 400 | UnsupportedParamsError | litellm.BadRequestError | Raised when unsupported params are passed | | 400 | ContextWindowExceededError| litellm.BadRequestError | Special error type for context window exceeded error messages - enables context window fallbacks | | 400 | ContentPolicyViolationError| litellm.BadRequestError | Special error type for content policy violation error messages - enables content policy fallbacks | +| 400 | ImageFetchError | litellm.BadRequestError | Raised when there are errors fetching or processing images | | 400 | InvalidRequestError | openai.BadRequestError | Deprecated error, use BadRequestError instead | | 401 | AuthenticationError | openai.AuthenticationError | | 403 | PermissionDeniedError | openai.PermissionDeniedError | diff --git a/litellm/__init__.py b/litellm/__init__.py index fb280c34101..79865c83513 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -1261,6 +1261,7 @@ from .exceptions import ( AuthenticationError, InvalidRequestError, BadRequestError, + ImageFetchError, NotFoundError, RateLimitError, ServiceUnavailableError, diff --git a/litellm/exceptions.py b/litellm/exceptions.py index 153230518cc..77fb9c1faef 100644 --- a/litellm/exceptions.py +++ b/litellm/exceptions.py @@ -153,6 +153,29 @@ class BadRequestError(openai.BadRequestError): # type: ignore _message += f", LiteLLM Max Retries: {self.max_retries}" return _message +class ImageFetchError(BadRequestError): + def __init__( + self, + message, + model=None, + llm_provider=None, + response: Optional[httpx.Response] = None, + litellm_debug_info: Optional[str] = None, + max_retries: Optional[int] = None, + num_retries: Optional[int] = None, + body: Optional[dict] = None, + ): + super().__init__( + message=message, + model=model, + llm_provider=llm_provider, + response=response, + litellm_debug_info=litellm_debug_info, + max_retries=max_retries, + num_retries=num_retries, + body=body, + ) + class UnprocessableEntityError(openai.UnprocessableEntityError): # type: ignore def __init__( diff --git a/litellm/litellm_core_utils/prompt_templates/image_handling.py b/litellm/litellm_core_utils/prompt_templates/image_handling.py index a9ff14d6c82..4fa10e42111 100644 --- a/litellm/litellm_core_utils/prompt_templates/image_handling.py +++ b/litellm/litellm_core_utils/prompt_templates/image_handling.py @@ -17,7 +17,7 @@ in_memory_cache = InMemoryCache(max_size_in_memory=MAX_IMGS_IN_MEMORY) def _process_image_response(response: Response, url: str) -> str: if response.status_code != 200: - raise Exception( + raise litellm.ImageFetchError( f"Error: Unable to fetch image from URL. Status code: {response.status_code}, url={url}" ) @@ -57,9 +57,11 @@ async def async_convert_url_to_base64(url: str) -> str: try: response = await client.get(url, follow_redirects=True) return _process_image_response(response, url) + except litellm.ImageFetchError: + raise except Exception: pass - raise Exception( + raise litellm.ImageFetchError( f"Error: Unable to fetch image from URL after 3 attempts. url={url}" ) @@ -74,10 +76,11 @@ def convert_url_to_base64(url: str) -> str: try: response = client.get(url, follow_redirects=True) return _process_image_response(response, url) + except litellm.ImageFetchError: + raise except Exception as e: verbose_logger.exception(e) - # print(e) pass - raise Exception( - f"Error: Unable to fetch image from URL after 3 attempts. url={url}" + raise litellm.ImageFetchError( + f"Error: Unable to fetch image from URL after 3 attempts. url={url}", ) diff --git a/tests/test_litellm/litellm_core_utils/test_image_handling.py b/tests/test_litellm/litellm_core_utils/test_image_handling.py new file mode 100644 index 00000000000..64ae81b5763 --- /dev/null +++ b/tests/test_litellm/litellm_core_utils/test_image_handling.py @@ -0,0 +1,41 @@ +import pytest +from httpx import Request, Response + +import litellm +from litellm.litellm_core_utils.prompt_templates.image_handling import ( + convert_url_to_base64, +) + + +class DummyClient: + def get(self, url, follow_redirects=True): + return Response(status_code=404, request=Request("GET", url)) + + +def test_invalid_image_url_raises_bad_request(monkeypatch): + monkeypatch.setattr(litellm, "module_level_client", DummyClient()) + with pytest.raises(litellm.ImageFetchError) as excinfo: + convert_url_to_base64("https://invalid.example/image.png") + assert "Unable to fetch image" in str(excinfo.value) + + +def test_completion_with_invalid_image_url(monkeypatch): + monkeypatch.setattr(litellm, "module_level_client", DummyClient()) + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "hi"}, + { + "type": "image_url", + "image_url": {"url": "https://invalid.example/image.png"}, + }, + ], + } + ] + with pytest.raises(litellm.ImageFetchError) as excinfo: + litellm.completion( + model="gemini/gemini-pro", messages=messages, api_key="test" + ) + assert excinfo.value.status_code == 400 + assert "Unable to fetch image" in str(excinfo.value)