From 89bae5323315059d0e51de808b060c14d4b30b26 Mon Sep 17 00:00:00 2001 From: tinysolver Date: Sat, 3 Oct 2026 12:32:52 +0900 Subject: [PATCH] fix(openrouter): let aspect_ratio or resolution win over a pixel size on /images On the chat path, an image_config from litellm_params replaced the one built from size, so a caller that also sent size "1024x1024" still got the configured aspect ratio or image size. /images answers a pixel size next to a mismatched aspect_ratio or resolution with a 400 ('size "1024x1024" conflicts with resolution "2K".', seen on 2026-10-03), so the same config started failing When the body carries aspect_ratio or resolution, an explicit pixel size is now left out. A tier size such as "2K" still goes with aspect_ratio --- .../image_generation/transformation.py | 12 +++++- ...est_openrouter_image_gen_transformation.py | 43 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/litellm/llms/openrouter/image_generation/transformation.py b/litellm/llms/openrouter/image_generation/transformation.py index 4ca577e9b69..96e49248711 100644 --- a/litellm/llms/openrouter/image_generation/transformation.py +++ b/litellm/llms/openrouter/image_generation/transformation.py @@ -16,6 +16,7 @@ Response shape: } """ +import re from types import MappingProxyType from typing import TYPE_CHECKING, Any, Final @@ -53,6 +54,8 @@ LEGACY_IMAGE_CONFIG_FIELDS: Final = MappingProxyType({"aspect_ratio": "aspect_ra NON_BODY_PARAMS: Final = frozenset( {"model", "prompt", "messages", "modalities", "stream", "image_config", "extra_headers"} ) +PIXEL_SIZE: Final = re.compile(r"\d+x\d+") +SIZE_OVERRIDING_FIELDS: Final = frozenset({"aspect_ratio", "resolution"}) class OpenRouterImageGenerationConfig(BaseImageGenerationConfig): @@ -180,9 +183,12 @@ class OpenRouterImageGenerationConfig(BaseImageGenerationConfig): """ image_config is the request shape of the older chat-based path. Its fields map onto the /images names so existing configs keep working, and explicit top-level values win + + A configured aspect_ratio or resolution wins over an OpenAI pixel size, the way image_config + won over size on the chat path, because /images answers that pair with a 400 """ legacy_image_config: Final = optional_params.get("image_config") or {} - return { + body: Final[dict[str, object]] = { "model": model, "prompt": prompt, **{ @@ -192,6 +198,10 @@ class OpenRouterImageGenerationConfig(BaseImageGenerationConfig): }, **{key: value for key, value in optional_params.items() if key not in NON_BODY_PARAMS}, } + drop_pixel_size: Final = not SIZE_OVERRIDING_FIELDS.isdisjoint(body) and ( + PIXEL_SIZE.fullmatch(str(body.get("size", ""))) is not None + ) + return {key: value for key, value in body.items() if not (drop_pixel_size and key == "size")} def transform_image_generation_response( self, diff --git a/tests/unit/llms/openrouter/image_generation/test_openrouter_image_gen_transformation.py b/tests/unit/llms/openrouter/image_generation/test_openrouter_image_gen_transformation.py index 71c8e31b892..5933393014e 100644 --- a/tests/unit/llms/openrouter/image_generation/test_openrouter_image_gen_transformation.py +++ b/tests/unit/llms/openrouter/image_generation/test_openrouter_image_gen_transformation.py @@ -236,6 +236,33 @@ def test_transform_request_maps_legacy_image_config_and_explicit_fields_win( assert body == {"model": HYBRID_MODEL, "prompt": PROMPT, **expected_fields} +# On 2026-10-03 POST https://openrouter.ai/api/v1/images returned 400 for size "1024x1024" with aspect_ratio +# "3:2" (openai/gpt-image-1-mini) and with resolution "2K" (google/gemini-2.5-flash-image). The size field in +# https://openrouter.ai/openapi.json says a tier size such as "2K" combines with aspect_ratio +@pytest.mark.parametrize( + ("optional_params", "expected_fields"), + [ + ({"size": "1024x1024", "image_config": {"aspect_ratio": "16:9"}}, {"aspect_ratio": "16:9"}), + ({"size": "1024x1024", "resolution": "4K"}, {"resolution": "4K"}), + ({"size": "1024x1024", "aspect_ratio": "1:1"}, {"aspect_ratio": "1:1"}), + ({"size": "2K", "aspect_ratio": "16:9"}, {"size": "2K", "aspect_ratio": "16:9"}), + ({"size": "1024x1024"}, {"size": "1024x1024"}), + ], +) +def test_transform_request_lets_aspect_ratio_or_resolution_win_over_a_pixel_size( + optional_params: dict[str, object], expected_fields: dict[str, object] +): + body = CONFIG.transform_image_generation_request( + model=HYBRID_MODEL, + prompt=PROMPT, + optional_params=optional_params, + litellm_params={}, + headers={}, + ) + + assert body == {"model": HYBRID_MODEL, "prompt": PROMPT, **expected_fields} + + def test_transform_response_returns_every_image_in_order(): response = _transform_response(httpx.Response(200, json=_images_response("aW1hZ2Ux", "aW1hZ2Uy"))) @@ -332,6 +359,22 @@ def test_hybrid_image_text_model_uses_the_same_images_endpoint(): assert json.loads(request.content) == {"model": HYBRID_MODEL, "prompt": PROMPT} +def test_legacy_image_config_with_an_openai_pixel_size_sends_only_the_aspect_ratio(): + recorder = RequestRecorder(_images_response("aW1hZ2Ux")) + + litellm.image_generation( + model=f"openrouter/{HYBRID_MODEL}", + prompt=PROMPT, + size="1024x1024", + image_config={"aspect_ratio": "16:9"}, + api_key="sk-test", + client=_client(recorder), + ) + + (request,) = recorder.requests + assert json.loads(request.content) == {"model": HYBRID_MODEL, "prompt": PROMPT, "aspect_ratio": "16:9"} + + def test_legacy_chat_completions_api_base_still_reaches_the_images_endpoint(): recorder = RequestRecorder(_images_response("aW1hZ2Ux"))