diff --git a/litellm/llms/vertex_ai/gemini/transformation.py b/litellm/llms/vertex_ai/gemini/transformation.py index d7b96b4db7b..ed488685f5b 100644 --- a/litellm/llms/vertex_ai/gemini/transformation.py +++ b/litellm/llms/vertex_ai/gemini/transformation.py @@ -3,6 +3,7 @@ Transformation logic from OpenAI format to Gemini format. Why separate file? Make it easy to see how transformation works """ + import json import os from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple, Union, cast @@ -225,7 +226,11 @@ def _process_gemini_media( return _apply_gemini_3_metadata( part, model, media_resolution_enum, video_metadata ) - raise Exception("Invalid image received - {}".format(image_url)) + raise litellm.BadRequestError( + message=f"Invalid image received - {image_url}. Supported formats are http://, https://, gs://, or base64 data.", + model=model, + llm_provider="vertex_ai", + ) except Exception as e: raise e @@ -381,10 +386,11 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915 video_metadata = file_element["file"].get("video_metadata") passed_file = file_id or file_data if passed_file is None: - raise Exception( - "Unknown file type. Please pass in a file_id or file_data" + raise litellm.BadRequestError( + message="Unknown file type. Please pass in a file_id or file_data", + model=model, + llm_provider="vertex_ai", ) - # Convert detail to media_resolution_enum media_resolution_enum = ( _convert_detail_to_media_resolution_enum(detail) @@ -399,12 +405,23 @@ def _gemini_convert_messages_with_history( # noqa: PLR0915 video_metadata=video_metadata, ) _parts.append(_part) - except Exception: - raise Exception( - "Unable to determine mime type for file_id: {}, set this explicitly using message[{}].content[{}].file.format".format( - file_id, msg_i, element_idx - ) - ) + except Exception as e: + if isinstance(e, litellm.ImageFetchError): + # ImageFetchError (e.g., 403/404 on URL) — wrap with model/provider context + raise litellm.BadRequestError( + message=str(e), + model=model, + llm_provider="vertex_ai", + ) from e + elif isinstance(e, litellm.BadRequestError): + # Other BadRequestError (e.g., unsupported format) — preserve original message + raise + else: + raise litellm.BadRequestError( + message=f"Unable to determine mime type for file_id: {file_id}, set this explicitly using message[{msg_i}].content[{element_idx}].file.format. Original error: {str(e)}", + model=model, + llm_provider="vertex_ai", + ) from e user_content.extend(_parts) elif _message_content is not None and isinstance(_message_content, str): _part = PartType(text=_message_content) diff --git a/tests/test_litellm/llms/vertex_ai/gemini/test_gemini_transformation_exception_handling.py b/tests/test_litellm/llms/vertex_ai/gemini/test_gemini_transformation_exception_handling.py new file mode 100644 index 00000000000..6cc951b8624 --- /dev/null +++ b/tests/test_litellm/llms/vertex_ai/gemini/test_gemini_transformation_exception_handling.py @@ -0,0 +1,73 @@ +import pytest +from typing import List, cast +from unittest.mock import patch + +import litellm +from litellm.llms.vertex_ai.gemini.transformation import ( + _gemini_convert_messages_with_history, +) +from litellm.types.llms.openai import AllMessageValues + + +def test_missing_file_id_and_file_data_raises_bad_request_error(): + """When file element has neither file_id nor file_data, a BadRequestError is raised.""" + messages = cast(List[AllMessageValues], [{"role": "user", "content": [{"type": "file", "file": {}}]}]) + with pytest.raises(litellm.BadRequestError) as exc_info: + _gemini_convert_messages_with_history(messages, model="gemini-1.5-pro") + assert "Unknown file type" in str(exc_info.value) + + +def test_image_fetch_error_raises_bad_request_error(): + """ImageFetchError from _process_gemini_media is re-raised as BadRequestError.""" + messages = cast( + List[AllMessageValues], + [{"role": "user", "content": [{"type": "file", "file": {"file_id": "some_id"}}]}], + ) + with patch( + "litellm.llms.vertex_ai.gemini.transformation._process_gemini_media", + side_effect=litellm.ImageFetchError( + message="403 Forbidden", + model="gemini-1.5-pro", + llm_provider="vertex_ai", + ), + ): + with pytest.raises(litellm.BadRequestError) as exc_info: + _gemini_convert_messages_with_history(messages, model="gemini-1.5-pro") + assert "403 Forbidden" in str(exc_info.value) + + +def test_generic_exception_raises_bad_request_error_with_mime_message(): + """Generic exception from _process_gemini_media is wrapped as BadRequestError with MIME message.""" + messages = cast( + List[AllMessageValues], + [{"role": "user", "content": [{"type": "file", "file": {"file_id": "some_id"}}]}], + ) + with patch( + "litellm.llms.vertex_ai.gemini.transformation._process_gemini_media", + side_effect=ValueError("cannot determine mime type"), + ): + with pytest.raises(litellm.BadRequestError) as exc_info: + _gemini_convert_messages_with_history(messages, model="gemini-1.5-pro") + assert "Unable to determine mime type" in str(exc_info.value) + assert "cannot determine mime type" in str(exc_info.value) + + +def test_bad_request_error_from_process_gemini_media_is_re_raised_as_is(): + """BadRequestError from _process_gemini_media is re-raised verbatim (not replaced by generic message).""" + original_message = "Invalid image received - https://example.com/img.png. Supported formats are..." + messages = cast( + List[AllMessageValues], + [{"role": "user", "content": [{"type": "file", "file": {"file_id": "some_id"}}]}], + ) + with patch( + "litellm.llms.vertex_ai.gemini.transformation._process_gemini_media", + side_effect=litellm.BadRequestError( + message=original_message, + model="gemini-1.5-pro", + llm_provider="vertex_ai", + ), + ): + with pytest.raises(litellm.BadRequestError) as exc_info: + _gemini_convert_messages_with_history(messages, model="gemini-1.5-pro") + assert original_message in str(exc_info.value) + assert "Unable to determine mime type" not in str(exc_info.value)