From cb9a30ec24f43a9f1caa3fc1f47ed88d98b7c531 Mon Sep 17 00:00:00 2001 From: Ericz Date: Fri, 25 Sep 2026 16:31:37 -0400 Subject: [PATCH] fix(dashscope): hoist extra_body into parameters for image generation DashScope reads provider params (watermark, negative_prompt, prompt_extend, seed) at the top level of `parameters`, but they arrived wrapped in `extra_body` and were silently ignored. Fixes #43207. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../image_generation/transformation.py | 12 ++- tests/unit/test_dashscope_image_generation.py | 89 +++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/litellm/llms/dashscope/image_generation/transformation.py b/litellm/llms/dashscope/image_generation/transformation.py index ffa60a3d9bc..351d24721f6 100644 --- a/litellm/llms/dashscope/image_generation/transformation.py +++ b/litellm/llms/dashscope/image_generation/transformation.py @@ -138,9 +138,15 @@ class DashScopeImageGenerationConfig(BaseImageGenerationConfig): """ Transform OpenAI-style image generation request to DashScope multimodal-generation format. """ - parameters: Final[dict] = {} - for k, v in optional_params.items(): - parameters[k] = v + parameters: Final[dict] = {k: v for k, v in optional_params.items() if k != "extra_body"} + # DashScope is in `openai_compatible_providers`, so non-OpenAI params + # (watermark, negative_prompt, prompt_extend, seed, ...) arrive wrapped in + # `extra_body`. DashScope reads them at the top level of `parameters`, so + # hoist them there; mapped OpenAI params (n, size) take precedence. + extra_body: Final = optional_params.get("extra_body") + if isinstance(extra_body, dict): + for k, v in extra_body.items(): + parameters.setdefault(k, v) return { "model": model, diff --git a/tests/unit/test_dashscope_image_generation.py b/tests/unit/test_dashscope_image_generation.py index 6f91fe9a0e0..e890acad42c 100644 --- a/tests/unit/test_dashscope_image_generation.py +++ b/tests/unit/test_dashscope_image_generation.py @@ -149,6 +149,51 @@ class TestDashScopeImageGenerationConfig: ) assert req["parameters"] == {} + def test_transform_request_hoists_extra_body_into_parameters(self): + """Provider-specific params (watermark, negative_prompt, ...) arrive wrapped in + `extra_body`; DashScope reads them at the top level of `parameters`.""" + req = self.cfg.transform_image_generation_request( + model="qwen-image-2.0-pro", + prompt="sunset over the ocean", + optional_params={ + "size": "1024*1024", + "n": 1, + "extra_body": { + "watermark": False, + "negative_prompt": "text, logo", + "prompt_extend": False, + "seed": 42, + }, + }, + litellm_params={}, + headers={}, + ) + assert req["parameters"] == { + "size": "1024*1024", + "n": 1, + "watermark": False, + "negative_prompt": "text, logo", + "prompt_extend": False, + "seed": 42, + } + + def test_transform_request_extra_body_does_not_clobber_mapped_params(self): + optional_params = { + "size": "1024*1024", + "n": 2, + "extra_body": {"size": "512*512", "n": 4, "watermark": True}, + } + req = self.cfg.transform_image_generation_request( + model="qwen-image-2.0-pro", + prompt="sunset over the ocean", + optional_params=optional_params, + litellm_params={}, + headers={}, + ) + assert req["parameters"] == {"size": "1024*1024", "n": 2, "watermark": True} + # the caller's optional_params are left untouched + assert "extra_body" in optional_params + # --------------------------------------------------------------------------- # 4. Response transformation # --------------------------------------------------------------------------- @@ -455,3 +500,47 @@ def test_litellm_image_generation_dashscope_end_to_end(model: str): assert "input" in body assert "messages" in body["input"] assert body["parameters"]["size"] == "1024*1024" + + +def test_litellm_image_generation_dashscope_provider_params_reach_parameters(): + """Non-OpenAI params passed to litellm.image_generation must land at the top + level of DashScope's `parameters`, not nested under `parameters.extra_body`.""" + mock_response_body = { + "output": { + "choices": [ + { + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": [{"image": "https://example.com/test.png"}], + }, + } + ] + }, + "usage": {"width": 1024, "height": 1024, "image_count": 1}, + } + + with patch( + "litellm.llms.custom_httpx.llm_http_handler.HTTPHandler.post" + ) as mock_post: + mock_http_response = MagicMock() + mock_http_response.json.return_value = mock_response_body + mock_http_response.status_code = 200 + mock_http_response.headers = {} + mock_post.return_value = mock_http_response + + litellm.image_generation( + model="dashscope/qwen-image-2.0-pro", + prompt="a puppy playing on green grass", + api_key="sk-test-key", + size="1024x1024", + watermark=False, + negative_prompt="text, logo", + ) + + body = mock_post.call_args.kwargs["json"] + assert body["parameters"] == { + "size": "1024*1024", + "watermark": False, + "negative_prompt": "text, logo", + }