From 035dd3e98ce1052233b5c85955962f7b7ddcc5ae Mon Sep 17 00:00:00 2001 From: Techboy bebop Date: Sun, 20 Sep 2026 23:08:17 -0400 Subject: [PATCH 1/3] fix(images): return 400 when /images/edits omits image aimage_edit required a positional image, so the proxy returned HTTP 500 with a raw TypeError when multipart files were absent. Make image optional and raise BadRequestError so OpenAI-compatible clients get HTTP 400. Fixes #42185 Co-authored-by: Cursor Agent --- litellm/images/main.py | 22 +++++-- .../images/test_aimage_edit_missing_image.py | 64 +++++++++++++++++++ 2 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 tests/test_litellm/images/test_aimage_edit_missing_image.py 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" From 0be14e72ed970a8741104a57b966b894ba5c8b27 Mon Sep 17 00:00:00 2001 From: Techboy bebop Date: Sun, 20 Sep 2026 23:08:17 -0400 Subject: [PATCH 2/3] fix(images): satisfy type-discipline and patch coverage for missing image 400 Annotate the BadRequestError body dict as mutable-ok so LIT002 stays within budget, and assert the OpenAI-shaped body/param on every missing-image 400 path. Co-authored-by: Cursor Agent --- litellm/images/main.py | 2 +- .../images/test_aimage_edit_missing_image.py | 72 ++++++++++++++----- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/litellm/images/main.py b/litellm/images/main.py index 685ec245a3a..1236c61750f 100644 --- a/litellm/images/main.py +++ b/litellm/images/main.py @@ -1005,7 +1005,7 @@ async def aimage_edit( message=message, model=model or "unknown", llm_provider=custom_llm_provider or "openai", - body={ + body={ # mutable-ok: BadRequestError requires an OpenAI-shaped error body dict "message": message, "type": "invalid_request_error", "param": "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 index 5aee0f4ad31..e1434982db2 100644 --- a/tests/test_litellm/images/test_aimage_edit_missing_image.py +++ b/tests/test_litellm/images/test_aimage_edit_missing_image.py @@ -12,6 +12,34 @@ import pytest import litellm from litellm.images.main import aimage_edit +_MISSING_IMAGE_MESSAGE = "Missing required parameter: 'image'." +_MISSING_IMAGE_BODY = { + "message": _MISSING_IMAGE_MESSAGE, + "type": "invalid_request_error", + "param": "image", + "code": "missing_required_parameter", +} + + +def _assert_missing_image_400( + err: litellm.BadRequestError, + *, + model: str, + llm_provider: str, +) -> None: + message = str(err) + assert err.status_code == 400 + assert _MISSING_IMAGE_MESSAGE in message + assert "positional argument" not in message + assert "TypeError" not in message + assert err.model == model + assert err.llm_provider == llm_provider + assert err.param == "image" + body = err.body + assert isinstance(body, Mapping) + for key, value in _MISSING_IMAGE_BODY.items(): + assert body[key] == value + @pytest.mark.asyncio @pytest.mark.parametrize( @@ -27,15 +55,11 @@ async def test_aimage_edit_missing_image_raises_bad_request(kwargs: Mapping[str, 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" + _assert_missing_image_400( + exc_info.value, + model="openai/gpt-image-1", + llm_provider="openai", + ) @pytest.mark.asyncio @@ -43,10 +67,11 @@ async def test_aimage_edit_direct_import_missing_image_raises_bad_request() -> N 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) + _assert_missing_image_400( + exc_info.value, + model="gpt-image-1", + llm_provider="openai", + ) @pytest.mark.asyncio @@ -58,7 +83,20 @@ async def test_aimage_edit_missing_image_keeps_custom_llm_provider() -> None: custom_llm_provider="azure", ) - err = exc_info.value - assert err.status_code == 400 - assert err.llm_provider == "azure" - assert err.model == "gpt-image-1" + _assert_missing_image_400( + exc_info.value, + model="gpt-image-1", + llm_provider="azure", + ) + + +@pytest.mark.asyncio +async def test_aimage_edit_missing_image_defaults_model_to_unknown() -> None: + with pytest.raises(litellm.BadRequestError) as exc_info: + await aimage_edit(prompt="add a hat") + + _assert_missing_image_400( + exc_info.value, + model="unknown", + llm_provider="openai", + ) From 036908ae471670098728797a3fdc8128d74e5902 Mon Sep 17 00:00:00 2001 From: Techboy bebop Date: Sun, 20 Sep 2026 23:08:17 -0400 Subject: [PATCH 3/3] test(images): collect missing-image 400 tests in core-utils shard misc CI lists missing paths and collects zero tests, so patch coverage never saw the aimage_edit guard. Keep the suite under litellm_core_utils where core-utils actually runs. Co-authored-by: Cursor Agent --- .../test_aimage_edit_missing_image.py | 2 ++ 1 file changed, 2 insertions(+) rename tests/test_litellm/{images => litellm_core_utils}/test_aimage_edit_missing_image.py (95%) diff --git a/tests/test_litellm/images/test_aimage_edit_missing_image.py b/tests/test_litellm/litellm_core_utils/test_aimage_edit_missing_image.py similarity index 95% rename from tests/test_litellm/images/test_aimage_edit_missing_image.py rename to tests/test_litellm/litellm_core_utils/test_aimage_edit_missing_image.py index e1434982db2..f1f64698fe3 100644 --- a/tests/test_litellm/images/test_aimage_edit_missing_image.py +++ b/tests/test_litellm/litellm_core_utils/test_aimage_edit_missing_image.py @@ -1,5 +1,7 @@ """Regression tests for https://github.com/BerriAI/litellm/issues/42185 +Lives under litellm_core_utils so the core-utils CI shard collects it (misc currently lists missing paths and collects zero tests). + 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.