diff --git a/litellm/images/main.py b/litellm/images/main.py index 6a94e7c8df2..685ec245a3a 100644 --- a/litellm/images/main.py +++ b/litellm/images/main.py @@ -965,9 +965,9 @@ def image_edit( @client async def aimage_edit( - image: FileTypes | list[FileTypes], - model: str, - prompt: str, + image: FileTypes | list[FileTypes] | None = None, + model: str | None = None, + prompt: str | None = None, mask: str | None = None, n: int | None = None, quality: str | ImageGenerationRequestQuality | None = None, @@ -999,10 +999,24 @@ async def aimage_edit( loop: Final = asyncio.get_event_loop() kwargs["async_call"] = True + if image is None or (isinstance(image, list) and len(image) == 0): + message = "Missing required parameter: 'image'." + raise litellm.BadRequestError( + message=message, + model=model or "unknown", + llm_provider=custom_llm_provider or "openai", + body={ + "message": message, + "type": "invalid_request_error", + "param": "image", + "code": "missing_required_parameter", + }, + ) + # get custom llm provider so we can use this for mapping exceptions if custom_llm_provider is None: _, custom_llm_provider, _, _ = litellm.get_llm_provider( - model=model, api_base=local_vars.get("base_url", None) + model=model or DEFAULT_IMAGE_ENDPOINT_MODEL, api_base=local_vars.get("base_url", None) ) images: Final = image if isinstance(image, list) else [image] diff --git a/tests/test_litellm/images/test_aimage_edit_missing_image.py b/tests/test_litellm/images/test_aimage_edit_missing_image.py new file mode 100644 index 00000000000..5aee0f4ad31 --- /dev/null +++ b/tests/test_litellm/images/test_aimage_edit_missing_image.py @@ -0,0 +1,64 @@ +"""Regression tests for https://github.com/BerriAI/litellm/issues/42185 + +POST /v1/images/edits without a multipart `image` used to TypeError inside +aimage_edit (required positional arg) and surface as HTTP 500. A missing client +field must be BadRequestError 400. +""" + +from collections.abc import Mapping + +import pytest + +import litellm +from litellm.images.main import aimage_edit + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "kwargs", + [ + {"model": "openai/gpt-image-1", "prompt": "add a hat"}, + {"model": "openai/gpt-image-1", "prompt": "add a hat", "image": None}, + {"model": "openai/gpt-image-1", "prompt": "add a hat", "image": []}, + ], + ids=["omitted", "none", "empty_list"], +) +async def test_aimage_edit_missing_image_raises_bad_request(kwargs: Mapping[str, object]) -> None: + with pytest.raises(litellm.BadRequestError) as exc_info: + await litellm.aimage_edit(**kwargs) + + err = exc_info.value + message = str(err) + assert err.status_code == 400 + assert "Missing required parameter: 'image'" in message + assert "positional argument" not in message + assert "TypeError" not in message + assert getattr(err, "param", None) == "image" + assert err.model == "openai/gpt-image-1" + assert err.llm_provider == "openai" + + +@pytest.mark.asyncio +async def test_aimage_edit_direct_import_missing_image_raises_bad_request() -> None: + with pytest.raises(litellm.BadRequestError) as exc_info: + await aimage_edit(model="gpt-image-1", prompt="add a hat") + + err = exc_info.value + assert err.status_code == 400 + assert "Missing required parameter: 'image'" in str(err) + assert "positional argument" not in str(err) + + +@pytest.mark.asyncio +async def test_aimage_edit_missing_image_keeps_custom_llm_provider() -> None: + with pytest.raises(litellm.BadRequestError) as exc_info: + await litellm.aimage_edit( + model="gpt-image-1", + prompt="add a hat", + custom_llm_provider="azure", + ) + + err = exc_info.value + assert err.status_code == 400 + assert err.llm_provider == "azure" + assert err.model == "gpt-image-1"