diff --git a/litellm/images/main.py b/litellm/images/main.py index 7dc68dafecc..eb994fd7956 100644 --- a/litellm/images/main.py +++ b/litellm/images/main.py @@ -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, ) diff --git a/tests/unit/images/test_image_variation_optional_params.py b/tests/unit/images/test_image_variation_optional_params.py new file mode 100644 index 00000000000..99a577795fc --- /dev/null +++ b/tests/unit/images/test_image_variation_optional_params.py @@ -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"