This commit is contained in:
Pawan Shahane 2026-10-03 20:11:58 +08:00 • committed by GitHub
commit a5318939f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 108 additions and 15 deletions

View file

@ -708,6 +708,13 @@ def image_variation(
return response
def _as_image_list(image: FileTypes | list[FileTypes] | None) -> list[FileTypes]:
"""A missing image stays an empty list so each provider config can reject it on its own terms."""
if isinstance(image, list):
return image
return [] if image is None else [image]
@client
def image_edit(
image: FileTypes | list[FileTypes] | None = None,
@ -760,7 +767,7 @@ def image_edit(
_is_async: Final = kwargs.pop("async_call", False) is True
# add images / or return a single image
images: Final = image if isinstance(image, list) else ([image] if image is not None else [])
images: Final = _as_image_list(image)
headers_from_kwargs: Final = kwargs.get("headers")
merged_extra_headers: Final[dict[str, object]] = {}
@ -962,9 +969,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,
@ -996,13 +1003,20 @@ async def aimage_edit(
loop: Final = asyncio.get_event_loop()
kwargs["async_call"] = True
if model is None:
raise litellm.BadRequestError(
message="model is required for image edits",
model="",
llm_provider=custom_llm_provider or "",
)
# 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)
)
images: Final = image if isinstance(image, list) else [image]
images: Final = _as_image_list(image)
func: Final = partial(
image_edit,

View file

@ -98,11 +98,19 @@ class AzureFoundryFlux2ImageEditConfig(OpenAIImageEditConfig):
raise ValueError("FLUX 2 image edit requires a prompt.")
if image is None:
raise ValueError("FLUX 2 image edit requires an image.")
raise litellm.BadRequestError(
message="FLUX 2 image edit requires an image.",
model=model,
llm_provider="azure_ai",
)
images: Final = tuple(image) if isinstance(image, list) else (image,)
if not images:
raise ValueError("FLUX 2 image edit requires at least one image.")
raise litellm.BadRequestError(
message="FLUX 2 image edit requires at least one image.",
model=model,
llm_provider="azure_ai",
)
max_reference_images: Final = 10 if "flex" in model.lower() else 8
if len(images) > max_reference_images:
raise ValueError(f"{model} supports at most {max_reference_images} reference images.")

View file

@ -19,6 +19,7 @@ from typing import TYPE_CHECKING, Any, Final
import httpx
from litellm._logging import verbose_logger
from litellm.exceptions import BadRequestError
from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig
from litellm.llms.bedrock.common_utils import BedrockError
from litellm.types.images.main import ImageEditOptionalRequestParams
@ -129,10 +130,14 @@ def _nova_canvas_task_body(
}
def _file_types_to_b64(image: FileTypes | None) -> str:
def _file_types_to_b64(image: FileTypes | None, model: str) -> str:
"""Encode OpenAI image input to base64 string for Nova Canvas."""
if image is None:
raise ValueError("Nova Canvas image edit requires an image input")
raise BadRequestError(
message="Nova Canvas image edit requires an image input",
model=model,
llm_provider="bedrock",
)
if hasattr(image, "read") and callable(getattr(image, "read", None)):
if hasattr(image, "seek"):
image.seek(0)
@ -314,12 +319,12 @@ class BedrockAmazonNovaCanvasImageEditConfig(BaseImageEditConfig):
headers: dict,
) -> tuple[dict, Any]:
op: Final = dict(image_edit_optional_request_params)
image_b64: Final = _file_types_to_b64(image)
image_b64: Final = _file_types_to_b64(image, model=model)
mask_raw: Final = op.pop("mask", None)
mask_b64: str | None = None
if mask_raw is not None:
mask_b64 = _file_types_to_b64(mask_raw)
mask_b64 = _file_types_to_b64(mask_raw, model=model)
_size: Final = op.pop("size", None)
width = op.pop("width", None)

View file

@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, Final, cast
import httpx
from httpx._types import RequestFiles
from litellm.exceptions import BadRequestError
from litellm.images.utils import ImageEditRequestUtils
from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig
from litellm.llms.gemini.common_utils import (
@ -94,7 +95,11 @@ class GeminiImageEditConfig(BaseImageEditConfig):
) -> tuple[dict[str, object], RequestFiles | None]:
inline_parts: Final = self._prepare_inline_image_parts(image) if image else []
if not inline_parts:
raise ValueError("Gemini image edit requires at least one image.")
raise BadRequestError(
message="Gemini image edit requires at least one image.",
model=model,
llm_provider="gemini",
)
# Build parts list with image and prompt (if provided)
parts: Final = inline_parts.copy()

View file

@ -166,7 +166,11 @@ class VertexAIGeminiImageEditConfig(BaseImageEditConfig, VertexLLM):
) -> tuple[dict[str, object], RequestFiles | None]:
inline_parts: Final = self._prepare_inline_image_parts(image) if image else []
if not inline_parts:
raise ValueError("Vertex AI Gemini image edit requires at least one image.")
raise litellm.BadRequestError(
message="Vertex AI Gemini image edit requires at least one image.",
model=model,
llm_provider="vertex_ai",
)
# Build parts list with image and prompt (if provided)
text_parts: Final[list[HttpxPartType]] = [{"text": prompt}] if prompt is not None and prompt != "" else []

View file

@ -0,0 +1,51 @@
"""
Regression tests for https://github.com/BerriAI/litellm/issues/42185
/v1/images/edits with no `image` part raised a raw TypeError out of aimage_edit,
which the proxy surfaced as a 500. A 500 tells OpenAI-SDK clients to retry a
request that can never succeed, so a missing image has to read as a 400.
"""
import httpx
import pytest
import litellm
PNG_BYTES: bytes = b"\x89PNG\r\n\x1a\nfakepng"
@pytest.mark.asyncio
async def test_aimage_edit_without_image_is_a_bad_request_not_a_type_error():
with pytest.raises(litellm.BadRequestError) as exc_info:
await litellm.aimage_edit(
model="gemini/gemini-3.1-flash-image",
prompt="make it blue",
api_key="fake-key-never-used",
)
assert exc_info.value.status_code == 400
@pytest.mark.asyncio
async def test_aimage_edit_still_reaches_the_provider_when_an_image_is_supplied(monkeypatch):
sent: dict = {}
async def fake_post(self, *args, **kwargs):
sent["json"] = kwargs.get("json")
return httpx.Response(
200,
json={"candidates": [{"content": {"parts": [{"inlineData": {"mimeType": "image/png", "data": "aW1n"}}]}}]},
request=httpx.Request("POST", "https://generativelanguage.googleapis.com"),
)
monkeypatch.setattr("litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post", fake_post)
await litellm.aimage_edit(
model="gemini/gemini-3.1-flash-image",
prompt="make it blue",
image=PNG_BYTES,
api_key="fake-key-never-used",
)
parts = sent["json"]["contents"][0]["parts"]
assert any("inlineData" in part for part in parts)

View file

@ -244,7 +244,9 @@ class TestGeminiImageEditTransformation:
def test_transform_image_edit_request_without_image_raises(self) -> None:
optional_params = {}
with pytest.raises(ValueError, match='Gemini image edit requires at least one image\\.'):
with pytest.raises(
litellm.BadRequestError, match='Gemini image edit requires at least one image\\.'
) as exc_info:
self.config.transform_image_edit_request(
model=self.model,
prompt=self.prompt,
@ -254,6 +256,8 @@ class TestGeminiImageEditTransformation:
headers={},
)
assert exc_info.value.status_code == 400
def test_use_multipart_form_data_returns_false(self) -> None:
"""
Gemini uses JSON requests, not multipart/form-data.

View file

@ -8,6 +8,8 @@ from unittest.mock import MagicMock, patch
import httpx
import pytest
import litellm
from litellm.llms.vertex_ai.image_edit.vertex_gemini_transformation import (
VertexAIGeminiImageEditConfig,
)
@ -132,7 +134,7 @@ class TestVertexAIGeminiImageEditTransformation:
"""Test that missing image raises ValueError"""
optional_params = {}
with pytest.raises(ValueError, match="requires at least one image"):
with pytest.raises(litellm.BadRequestError, match="requires at least one image"):
self.config.transform_image_edit_request(
model=self.model,
prompt=self.prompt,