From 84a99dfc3188fdefbf9c39abc87b7621d126328e Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 8 Sep 2026 18:24:51 -0700 Subject: [PATCH] fix(azure_ai): surface rejected MAI image params as 400 and drop comments --- .../image_generation/mai_transformation.py | 60 +++++++++---------- .../test_mai_image_generation.py | 31 ++++++---- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/litellm/llms/azure_ai/image_generation/mai_transformation.py b/litellm/llms/azure_ai/image_generation/mai_transformation.py index 3421cc0a4a1..2d9d3246b4c 100644 --- a/litellm/llms/azure_ai/image_generation/mai_transformation.py +++ b/litellm/llms/azure_ai/image_generation/mai_transformation.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Any, Final import httpx +from litellm.exceptions import UnsupportedParamsError from litellm.llms.base_llm.image_generation.transformation import ( BaseImageGenerationConfig, ) @@ -21,16 +22,7 @@ class AzureFoundryMAIImageGenerationConfig(BaseImageGenerationConfig): DEFAULT_WIDTH = 1024 DEFAULT_HEIGHT = 1024 - # The MAI endpoint produces exactly one image per request. Its documented - # body is model/prompt/width/height (plus `image` for edits) — there is no - # count field, and `n` (or the native `sampleCount`) is accepted and - # ignored, so a request for more silently comes back with one. MAX_IMAGES_PER_REQUEST: Final = 1 - - # Provider-side bounds on the generated image. Both are enforced by the - # MAI endpoint, which 400s with "'width' must be at least 768 pixels." - # Only `size` is checked against them: `width`/`height` pass through - # unmapped, which keeps a future model with different bounds reachable. MIN_DIMENSION_PX: Final = 768 MAX_TOTAL_PX: Final = 1024 * 1024 @@ -158,25 +150,27 @@ class AzureFoundryMAIImageGenerationConfig(BaseImageGenerationConfig): if k in supported_params: if k == "size" and v: - self._map_size_param(v, optional_params) - elif k == "n" and v is not None and v > self.MAX_IMAGES_PER_REQUEST: + self._map_size_param(v, optional_params, model) + elif k == "n" and v is not None and int(v) > self.MAX_IMAGES_PER_REQUEST: if not drop_params: - raise ValueError( + raise self._unsupported( + model, f"n={v} is not supported for model {model}. The Azure AI MAI image " f"endpoint returns exactly {self.MAX_IMAGES_PER_REQUEST} image per " "request and ignores any count, so a larger value would silently " "return fewer images than requested. Send one request per image, or " - "set drop_params=True to drop n." + "set drop_params=True to drop n.", ) else: optional_params[k] = v elif k in ("width", "height"): optional_params[k] = v elif not drop_params: - raise ValueError( + raise self._unsupported( + model, f"Parameter {k} is not supported for model {model}. " f"Supported parameters are {supported_params} and width/height. " - f"Set drop_params=True to drop unsupported parameters." + f"Set drop_params=True to drop unsupported parameters.", ) if "width" not in optional_params: @@ -187,7 +181,11 @@ class AzureFoundryMAIImageGenerationConfig(BaseImageGenerationConfig): optional_params.pop("size", None) return optional_params - def _map_size_param(self, size: str, optional_params: dict) -> None: + @staticmethod + def _unsupported(model: str, message: str) -> UnsupportedParamsError: + return UnsupportedParamsError(message=message, llm_provider="azure_ai", model=model) + + def _map_size_param(self, size: str, optional_params: dict, model: str) -> None: size_mapping: Final = { "1024x1024": (1024, 1024), "1792x1024": (1792, 1024), @@ -202,34 +200,32 @@ class AzureFoundryMAIImageGenerationConfig(BaseImageGenerationConfig): try: width, height = map(int, size.lower().split("x")) except ValueError: - raise ValueError(f"Invalid size format: '{size}'. Expected format 'WIDTHxHEIGHT' (e.g., '1024x1024').") + raise self._unsupported( + model, f"Invalid size format: '{size}'. Expected format 'WIDTHxHEIGHT' (e.g., '1024x1024')." + ) else: - raise ValueError( + raise self._unsupported( + model, f"Unsupported size value: '{size}'. " - f"Use a known size (e.g., '1024x1024') or a custom 'WIDTHxHEIGHT' string." + f"Use a known size (e.g., '1024x1024') or a custom 'WIDTHxHEIGHT' string.", ) - self._validate_dimensions(size=size, width=width, height=height) + self._validate_dimensions(model=model, size=size, width=width, height=height) optional_params["width"] = width optional_params["height"] = height - def _validate_dimensions(self, size: str, width: int, height: int) -> None: - """Reject a `size` the MAI endpoint would 400 on. - - Several OpenAI-standard sizes are outside MAI's bounds: 512x512 and - 256x256 fall under the per-side minimum, and 1792x1024 / 1024x1792 - exceed the total pixel budget. Checking here turns an opaque provider - 400 into an error that names the constraint. - """ + def _validate_dimensions(self, model: str, size: str, width: int, height: int) -> None: if width < self.MIN_DIMENSION_PX or height < self.MIN_DIMENSION_PX: - raise ValueError( + raise self._unsupported( + model, f"Unsupported size value: '{size}'. Azure AI MAI image models require width and " - f"height of at least {self.MIN_DIMENSION_PX} pixels." + f"height of at least {self.MIN_DIMENSION_PX} pixels.", ) if width * height > self.MAX_TOTAL_PX: - raise ValueError( + raise self._unsupported( + model, f"Unsupported size value: '{size}'. Azure AI MAI image models accept at most " - f"{self.MAX_TOTAL_PX} total pixels ({width}x{height} is {width * height})." + f"{self.MAX_TOTAL_PX} total pixels ({width}x{height} is {width * height}).", ) def transform_image_generation_response( diff --git a/tests/test_litellm/llms/azure_ai/image_generation/test_mai_image_generation.py b/tests/test_litellm/llms/azure_ai/image_generation/test_mai_image_generation.py index 6cbc79fff2b..29aa5967c9a 100644 --- a/tests/test_litellm/llms/azure_ai/image_generation/test_mai_image_generation.py +++ b/tests/test_litellm/llms/azure_ai/image_generation/test_mai_image_generation.py @@ -4,6 +4,7 @@ import httpx import pytest import litellm +from litellm.exceptions import UnsupportedParamsError from litellm.llms.azure.azure import AzureChatCompletion from litellm.llms.azure.image_generation import get_azure_image_generation_config from litellm.llms.azure.image_generation.http_utils import ( @@ -180,7 +181,7 @@ class TestAzureMAIImageGeneration: def test_map_openai_params_unsupported_size_raises(self): config = AzureFoundryMAIImageGenerationConfig() - with pytest.raises(ValueError, match="Unsupported size value: 'auto'"): + with pytest.raises(UnsupportedParamsError, match="Unsupported size value: 'auto'"): config.map_openai_params( non_default_params={"size": "auto"}, optional_params={}, @@ -190,7 +191,7 @@ class TestAzureMAIImageGeneration: def test_map_openai_params_invalid_custom_size_raises(self): config = AzureFoundryMAIImageGenerationConfig() - with pytest.raises(ValueError, match="Invalid size format: '1024xabc'"): + with pytest.raises(UnsupportedParamsError, match="Invalid size format: '1024xabc'"): config.map_openai_params( non_default_params={"size": "1024xabc"}, optional_params={}, @@ -200,9 +201,8 @@ class TestAzureMAIImageGeneration: @pytest.mark.parametrize("size", ["512x512", "256x256", "700x1400"]) def test_map_openai_params_size_below_minimum_dimension_raises(self, size): - """MAI requires >= 768px per side; the OpenAI size table offered smaller ones.""" config = AzureFoundryMAIImageGenerationConfig() - with pytest.raises(ValueError, match="at least 768 pixels"): + with pytest.raises(UnsupportedParamsError, match="at least 768 pixels"): config.map_openai_params( non_default_params={"size": size}, optional_params={}, @@ -212,9 +212,8 @@ class TestAzureMAIImageGeneration: @pytest.mark.parametrize("size", ["1792x1024", "1024x1792"]) def test_map_openai_params_size_over_total_pixel_budget_raises(self, size): - """MAI caps total pixels at 1024*1024, so both landscape/portrait sizes 400 upstream.""" config = AzureFoundryMAIImageGenerationConfig() - with pytest.raises(ValueError, match="at most 1048576 total pixels"): + with pytest.raises(UnsupportedParamsError, match="at most 1048576 total pixels"): config.map_openai_params( non_default_params={"size": size}, optional_params={}, @@ -223,7 +222,6 @@ class TestAzureMAIImageGeneration: ) def test_map_openai_params_explicit_width_height_not_range_checked(self): - """width/height pass through unmapped, so a future model's bounds stay reachable.""" config = AzureFoundryMAIImageGenerationConfig() optional_params = config.map_openai_params( non_default_params={"width": 1792, "height": 1024}, @@ -234,11 +232,10 @@ class TestAzureMAIImageGeneration: assert optional_params["width"] == 1792 assert optional_params["height"] == 1024 - @pytest.mark.parametrize("n", [2, 4]) + @pytest.mark.parametrize("n", [2, 4, "2"]) def test_map_openai_params_multi_image_n_raises(self, n): - """The MAI endpoint returns one image and ignores any count, so n>1 must not pass silently.""" config = AzureFoundryMAIImageGenerationConfig() - with pytest.raises(ValueError, match="returns exactly 1 image per request"): + with pytest.raises(UnsupportedParamsError, match="returns exactly 1 image per request"): config.map_openai_params( non_default_params={"n": n}, optional_params={}, @@ -266,9 +263,21 @@ class TestAzureMAIImageGeneration: ) assert optional_params["n"] == 1 + @pytest.mark.parametrize("params", [{"n": 2}, {"size": "512x512"}, {"size": "1792x1024"}]) + def test_image_generation_rejected_params_surface_as_400(self, params): + with pytest.raises(litellm.BadRequestError) as exc_info: + litellm.image_generation( + model="azure_ai/MAI-Image-2.5", + prompt="A photograph of a red fox", + api_key="test-key", + api_base="https://my-resource.services.ai.azure.com", + **params, + ) + assert exc_info.value.status_code == 400 + def test_map_openai_params_unsupported_param_raises(self): config = AzureFoundryMAIImageGenerationConfig() - with pytest.raises(ValueError, match="Parameter quality is not supported"): + with pytest.raises(UnsupportedParamsError, match="Parameter quality is not supported"): config.map_openai_params( non_default_params={"quality": "hd"}, optional_params={},