fix(images): forward n/response_format/size/user in image_variation

`image_variation()` declares `n`, `response_format`, `size` and `user`, but
built the request with a hard-coded `optional_params={}` for both the OpenAI
and Topaz branches, so none of them ever reached a provider.
`litellm.image_variation(image=f, n=4, size="512x512")` silently returned one
image at the provider default size.

The mapping layer already existed and was simply never called:
`ProviderConfigManager.get_provider_image_variation_config` returns a config
whose `map_openai_params` handles these (OpenAI passes them through; Topaz
renames `size` to `output_width`/`output_height` and `response_format` to
`output_format`). Build `optional_params` through it, as `image_generation`
does.

`n` and `response_format` become `| None = None`, matching
`image_generation`, so an omitted argument stays omitted instead of pinning
`n=1`/`response_format="url"` onto every provider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
revanth-045 2026-09-29 15:42:21 +05:30
parent 85dc7cb62e
commit 1344deedf8
2 changed files with 108 additions and 4 deletions

View file

@ -616,8 +616,8 @@ async def aimage_variation(*args, **kwargs) -> ImageResponse:
def image_variation(
image: FileTypes,
model: str = "dall-e-2", # set to dall-e-2 by default - like OpenAI.
n: int = 1,
response_format: Literal["url", "b64_json"] = "url",
n: int | None = None,
response_format: Literal["url", "b64_json"] | None = None,
size: str | None = None,
user: str | None = None,
**kwargs,
@ -662,6 +662,33 @@ def image_variation(
api_key: Final = provider_config.get_api_key(litellm_params.get("api_key", None))
api_base = provider_config.get_api_base(litellm_params.get("api_base", None))
# Map the OpenAI-style arguments onto the provider's request. Without this
# every provider was called with an empty dict, so `n`, `response_format`,
# `size` and `user` never left this function.
image_variation_config: Final = ProviderConfigManager.get_provider_image_variation_config(
model=model,
provider=llm_provider,
)
non_default_params: Final = {
key: value
for key, value in {
"n": n,
"response_format": response_format,
"size": size,
"user": user,
}.items()
if value is not None
}
optional_params: dict = {}
if image_variation_config is not None and non_default_params:
drop_params: Final = kwargs.get("drop_params")
optional_params = image_variation_config.map_openai_params(
non_default_params=non_default_params,
optional_params=optional_params,
model=model,
drop_params=bool(litellm.drop_params if drop_params is None else drop_params),
)
if image_variation_provider == LITELLM_IMAGE_VARIATION_PROVIDERS.OPENAI:
if api_key is None:
raise ValueError("API key is required for OpenAI image variations")
@ -677,7 +704,7 @@ def image_variation(
timeout=litellm_params.get("timeout", None),
custom_llm_provider=custom_llm_provider,
logging_obj=litellm_logging_obj,
optional_params={},
optional_params=optional_params,
litellm_params=litellm_params,
)
elif image_variation_provider == LITELLM_IMAGE_VARIATION_PROVIDERS.TOPAZ:
@ -695,7 +722,7 @@ def image_variation(
timeout=litellm_params.get("timeout", None) or DEFAULT_REQUEST_TIMEOUT,
custom_llm_provider=custom_llm_provider,
logging_obj=litellm_logging_obj,
optional_params={},
optional_params=optional_params,
litellm_params=litellm_params,
client=client,
)

View file

@ -0,0 +1,77 @@
"""`image_variation()` must forward its OpenAI-style arguments to the provider.
`n`, `response_format`, `size` and `user` are declared on the public function
but were dropped before the request was built.
"""
from unittest.mock import patch
import litellm
from litellm.images.main import image_variation
def _image_response() -> "litellm.utils.ImageResponse":
return litellm.utils.ImageResponse(
created=1234567890,
data=[{"url": "https://example.com/image.png"}],
)
def _optional_params(mock_call) -> dict:
return mock_call.call_args.kwargs["optional_params"]
class TestImageVariationOptionalParams:
@patch("litellm.images.main.openai_image_variations")
def test_openai_receives_the_requested_params(self, mock_openai) -> None:
mock_openai.image_variations.return_value = _image_response()
image_variation(
image=b"fake-image-bytes",
model="openai/dall-e-2",
n=4,
size="512x512",
response_format="b64_json",
user="user-123",
api_key="sk-test",
api_base="https://api.openai.com/v1",
)
optional_params = _optional_params(mock_openai.image_variations)
assert optional_params["n"] == 4
assert optional_params["size"] == "512x512"
assert optional_params["response_format"] == "b64_json"
assert optional_params["user"] == "user-123"
@patch("litellm.images.main.openai_image_variations")
def test_omitted_params_are_not_invented(self, mock_openai) -> None:
"""Nothing the caller left out may be sent, so provider defaults stand."""
mock_openai.image_variations.return_value = _image_response()
image_variation(
image=b"fake-image-bytes",
model="openai/dall-e-2",
api_key="sk-test",
api_base="https://api.openai.com/v1",
)
assert _optional_params(mock_openai.image_variations) == {}
@patch("litellm.images.main.base_llm_aiohttp_handler")
def test_topaz_maps_size_and_response_format(self, mock_handler) -> None:
"""Topaz renames these, which only happens if map_openai_params runs."""
mock_handler.image_variations.return_value = _image_response()
image_variation(
image=b"fake-image-bytes",
model="topaz/Standard V2",
size="1024x768",
response_format="b64_json",
api_key="topaz-key",
api_base="https://api.topazlabs.com",
)
optional_params = _optional_params(mock_handler.image_variations)
assert optional_params["output_width"] == "1024"
assert optional_params["output_height"] == "768"
assert optional_params["output_format"] == "b64_json"