mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
feat(dashscope): support qwen-image-3.0 and qwen-image-3.0-pro image generation
Register both models, route image requests to the multimodal generation endpoint instead of the chat compatible-mode base, and pass OpenAI n through as DashScope n so multi-image requests return every image.
This commit is contained in:
parent
3eba0b332a
commit
f7c9c87280
4 changed files with 141 additions and 12 deletions
|
|
@ -11,7 +11,7 @@ Request format:
|
|||
"input": {
|
||||
"messages": [{"role": "user", "content": [{"text": "<prompt>"}]}]
|
||||
},
|
||||
"parameters": {"size": "1024*1024", ...}
|
||||
"parameters": {"size": "1024*1024", "n": 1, ...}
|
||||
}
|
||||
|
||||
Response format:
|
||||
|
|
@ -19,7 +19,7 @@ Response format:
|
|||
"output": {
|
||||
"choices": [{"message": {"content": [{"image": "<url>"}]}}]
|
||||
},
|
||||
"usage": {"input_tokens": 0, "output_tokens": 0, "width": 1024, "height": 1024, "image_count": 1}
|
||||
"usage": {"output_width": 1024, "output_height": 1024, "output_image_count": 1}
|
||||
}
|
||||
"""
|
||||
|
||||
|
|
@ -46,6 +46,9 @@ else:
|
|||
|
||||
DEFAULT_API_BASE: Final = "https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
|
||||
|
||||
# get_llm_provider resolves every dashscope route to the chat/embed base, which cannot serve images
|
||||
CHAT_COMPATIBLE_MODE_PATH: Final = "/compatible-mode/v1"
|
||||
|
||||
# Maps OpenAI size strings (WxH) to DashScope size strings (W*H)
|
||||
OPENAI_TO_DASHSCOPE_SIZE: Final[dict] = {
|
||||
"256x256": "256*256",
|
||||
|
|
@ -59,7 +62,8 @@ OPENAI_TO_DASHSCOPE_SIZE: Final[dict] = {
|
|||
|
||||
class DashScopeImageGenerationConfig(BaseImageGenerationConfig):
|
||||
"""
|
||||
Configuration for DashScope image generation (qwen-image-2.0, qwen-image-2.0-pro).
|
||||
Configuration for DashScope image generation (qwen-image-2.0, qwen-image-2.0-pro,
|
||||
qwen-image-3.0, qwen-image-3.0-pro).
|
||||
"""
|
||||
|
||||
def get_supported_openai_params(self, model: str) -> list[OpenAIImageGenerationOptionalParams]:
|
||||
|
|
@ -82,8 +86,8 @@ class DashScopeImageGenerationConfig(BaseImageGenerationConfig):
|
|||
if k == "size":
|
||||
# Convert "WxH" → "W*H"
|
||||
mapped["size"] = OPENAI_TO_DASHSCOPE_SIZE.get(v, v.replace("x", "*"))
|
||||
elif k == "n":
|
||||
mapped["image_count"] = v
|
||||
else:
|
||||
mapped[k] = v
|
||||
return mapped
|
||||
|
||||
def get_complete_url(
|
||||
|
|
@ -95,7 +99,10 @@ class DashScopeImageGenerationConfig(BaseImageGenerationConfig):
|
|||
litellm_params: dict,
|
||||
stream: bool | None = None,
|
||||
) -> str:
|
||||
return api_base or get_secret_str("DASHSCOPE_API_BASE_IMAGE") or DEFAULT_API_BASE
|
||||
image_api_base: Final = (
|
||||
api_base if api_base and not api_base.rstrip("/").endswith(CHAT_COMPATIBLE_MODE_PATH) else None
|
||||
)
|
||||
return image_api_base or get_secret_str("DASHSCOPE_API_BASE_IMAGE") or DEFAULT_API_BASE
|
||||
|
||||
def validate_environment(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -14647,6 +14647,22 @@
|
|||
"/v1/images/generations"
|
||||
]
|
||||
},
|
||||
"dashscope/qwen-image-3.0": {
|
||||
"litellm_provider": "dashscope",
|
||||
"mode": "image_generation",
|
||||
"source": "https://www.alibabacloud.com/help/en/model-studio/models",
|
||||
"supported_endpoints": [
|
||||
"/v1/images/generations"
|
||||
]
|
||||
},
|
||||
"dashscope/qwen-image-3.0-pro": {
|
||||
"litellm_provider": "dashscope",
|
||||
"mode": "image_generation",
|
||||
"source": "https://www.alibabacloud.com/help/en/model-studio/models",
|
||||
"supported_endpoints": [
|
||||
"/v1/images/generations"
|
||||
]
|
||||
},
|
||||
"databricks/databricks-bge-large-en": {
|
||||
"cache_creation_input_token_cost": 1.0003e-07,
|
||||
"cache_read_input_token_cost": 1.0003e-07,
|
||||
|
|
|
|||
|
|
@ -14647,6 +14647,22 @@
|
|||
"/v1/images/generations"
|
||||
]
|
||||
},
|
||||
"dashscope/qwen-image-3.0": {
|
||||
"litellm_provider": "dashscope",
|
||||
"mode": "image_generation",
|
||||
"source": "https://www.alibabacloud.com/help/en/model-studio/models",
|
||||
"supported_endpoints": [
|
||||
"/v1/images/generations"
|
||||
]
|
||||
},
|
||||
"dashscope/qwen-image-3.0-pro": {
|
||||
"litellm_provider": "dashscope",
|
||||
"mode": "image_generation",
|
||||
"source": "https://www.alibabacloud.com/help/en/model-studio/models",
|
||||
"supported_endpoints": [
|
||||
"/v1/images/generations"
|
||||
]
|
||||
},
|
||||
"databricks/databricks-bge-large-en": {
|
||||
"cache_creation_input_token_cost": 1.0003e-07,
|
||||
"cache_read_input_token_cost": 1.0003e-07,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""
|
||||
Unit tests for DashScope image generation support (qwen-image-2.0, qwen-image-2.0-pro).
|
||||
Unit tests for DashScope image generation support (qwen-image-2.0, qwen-image-2.0-pro,
|
||||
qwen-image-3.0, qwen-image-3.0-pro).
|
||||
|
||||
Run in docker: pytest tests/test_litellm/test_dashscope_image_generation.py -v
|
||||
"""
|
||||
|
|
@ -30,6 +31,8 @@ from litellm.llms.base_llm.chat.transformation import BaseLLMException
|
|||
[
|
||||
"dashscope/qwen-image-2.0",
|
||||
"dashscope/qwen-image-2.0-pro",
|
||||
"dashscope/qwen-image-3.0",
|
||||
"dashscope/qwen-image-3.0-pro",
|
||||
],
|
||||
)
|
||||
def test_get_llm_provider_returns_dashscope(model_string: str):
|
||||
|
|
@ -48,6 +51,8 @@ def test_get_llm_provider_returns_dashscope(model_string: str):
|
|||
[
|
||||
("dashscope/qwen-image-2.0", "dashscope"),
|
||||
("dashscope/qwen-image-2.0-pro", "dashscope"),
|
||||
("dashscope/qwen-image-3.0", "dashscope"),
|
||||
("dashscope/qwen-image-3.0-pro", "dashscope"),
|
||||
],
|
||||
)
|
||||
def test_get_model_info_mode_is_image_generation(
|
||||
|
|
@ -93,6 +98,19 @@ class TestDashScopeImageGenerationConfig:
|
|||
url = self.cfg.get_complete_url(custom, None, "qwen-image-2.0", {}, {})
|
||||
assert url == custom
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"chat_api_base",
|
||||
[
|
||||
"https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1/",
|
||||
],
|
||||
)
|
||||
def test_get_complete_url_ignores_chat_compatible_mode_base(
|
||||
self, chat_api_base: str
|
||||
):
|
||||
url = self.cfg.get_complete_url(chat_api_base, None, "qwen-image-3.0", {}, {})
|
||||
assert url == DEFAULT_API_BASE
|
||||
|
||||
def test_validate_environment_sets_auth_header(self):
|
||||
headers = self.cfg.validate_environment(
|
||||
headers={},
|
||||
|
|
@ -135,6 +153,27 @@ class TestDashScopeImageGenerationConfig:
|
|||
assert messages[0]["content"][0]["text"] == "a puppy on green grass"
|
||||
assert req["parameters"]["size"] == "1024*1024"
|
||||
|
||||
@pytest.mark.parametrize("model", ["qwen-image-3.0", "qwen-image-3.0-pro"])
|
||||
def test_transform_request_qwen_image_3(self, model: str):
|
||||
req = self.cfg.transform_image_generation_request(
|
||||
model=model,
|
||||
prompt="a poster with small multilingual text",
|
||||
optional_params=self.cfg.map_openai_params(
|
||||
non_default_params={"size": "2048x2048", "n": 6},
|
||||
optional_params={},
|
||||
model=model,
|
||||
drop_params=False,
|
||||
),
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
assert req["model"] == model
|
||||
assert req["input"]["messages"][0]["content"][0]["text"] == (
|
||||
"a poster with small multilingual text"
|
||||
)
|
||||
assert req["parameters"]["size"] == "2048*2048"
|
||||
assert req["parameters"]["n"] == 6
|
||||
|
||||
def test_transform_request_empty_params(self):
|
||||
req = self.cfg.transform_image_generation_request(
|
||||
model="qwen-image-2.0-pro",
|
||||
|
|
@ -238,6 +277,48 @@ class TestDashScopeImageGenerationConfig:
|
|||
assert result.data[0].url == "https://example.com/img1.png"
|
||||
assert result.data[1].url == "https://example.com/img2.png"
|
||||
|
||||
def test_transform_response_multiple_images_in_one_choice(self):
|
||||
body = {
|
||||
"output": {
|
||||
"choices": [
|
||||
{
|
||||
"finish_reason": "stop",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"image": "https://example.com/img1.png", "type": "image"},
|
||||
{"image": "https://example.com/img2.png", "type": "image"},
|
||||
],
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
"usage": {
|
||||
"output_width": 1024,
|
||||
"output_height": 1024,
|
||||
"output_image_count": 2,
|
||||
},
|
||||
}
|
||||
mock_resp = MagicMock(spec=httpx.Response)
|
||||
mock_resp.status_code = 200
|
||||
mock_resp.headers = {}
|
||||
mock_resp.json.return_value = body
|
||||
|
||||
result = self.cfg.transform_image_generation_response(
|
||||
model="qwen-image-3.0",
|
||||
raw_response=mock_resp,
|
||||
model_response=ImageResponse(),
|
||||
logging_obj=MagicMock(),
|
||||
request_data={},
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
encoding=None,
|
||||
)
|
||||
assert [image.url for image in result.data] == [
|
||||
"https://example.com/img1.png",
|
||||
"https://example.com/img2.png",
|
||||
]
|
||||
|
||||
def test_transform_response_raises_on_non_200_status(self):
|
||||
mock_resp = MagicMock(spec=httpx.Response)
|
||||
mock_resp.status_code = 400
|
||||
|
|
@ -294,14 +375,14 @@ class TestDashScopeImageGenerationConfig:
|
|||
)
|
||||
assert mapped["size"] == "1024*1024"
|
||||
|
||||
def test_map_openai_params_n_to_image_count(self):
|
||||
def test_map_openai_params_n_passthrough(self):
|
||||
mapped = self.cfg.map_openai_params(
|
||||
non_default_params={"n": 2},
|
||||
optional_params={},
|
||||
model="qwen-image-2.0",
|
||||
drop_params=False,
|
||||
)
|
||||
assert mapped["image_count"] == 2
|
||||
assert mapped == {"n": 2}
|
||||
|
||||
def test_map_openai_params_unknown_size_uses_asterisk(self):
|
||||
mapped = self.cfg.map_openai_params(
|
||||
|
|
@ -338,7 +419,15 @@ class TestDashScopeImageGenerationConfig:
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_litellm_image_generation_dashscope_end_to_end():
|
||||
@pytest.mark.parametrize(
|
||||
"model",
|
||||
[
|
||||
"dashscope/qwen-image-2.0",
|
||||
"dashscope/qwen-image-3.0",
|
||||
"dashscope/qwen-image-3.0-pro",
|
||||
],
|
||||
)
|
||||
def test_litellm_image_generation_dashscope_end_to_end(model: str):
|
||||
mock_response_body = {
|
||||
"output": {
|
||||
"choices": [
|
||||
|
|
@ -374,7 +463,7 @@ def test_litellm_image_generation_dashscope_end_to_end():
|
|||
mock_post.return_value = mock_http_response
|
||||
|
||||
response = litellm.image_generation(
|
||||
model="dashscope/qwen-image-2.0",
|
||||
model=model,
|
||||
prompt="a puppy playing on green grass",
|
||||
api_key="sk-test-key",
|
||||
size="1024x1024",
|
||||
|
|
@ -392,7 +481,7 @@ def test_litellm_image_generation_dashscope_end_to_end():
|
|||
called_url = (
|
||||
call_args[0][0] if call_args[0] else call_args.kwargs.get("url", "")
|
||||
)
|
||||
assert "dashscope" in called_url or "aliyuncs" in called_url
|
||||
assert called_url == DEFAULT_API_BASE
|
||||
|
||||
# Verify request body contains DashScope format
|
||||
call_kwargs = call_args[1] if call_args[1] else {}
|
||||
|
|
@ -400,3 +489,4 @@ def test_litellm_image_generation_dashscope_end_to_end():
|
|||
body = call_kwargs["json"]
|
||||
assert "input" in body
|
||||
assert "messages" in body["input"]
|
||||
assert body["parameters"]["size"] == "1024*1024"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue