fix(azure_ai): surface rejected MAI image params as 400 and drop comments

This commit is contained in:
mateo-berri 2026-09-08 18:24:51 -07:00
parent 808787659b
commit 84a99dfc31
2 changed files with 48 additions and 43 deletions

View file

@ -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(

View file

@ -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={},