diff --git a/litellm/llms/dashscope/image_generation/transformation.py b/litellm/llms/dashscope/image_generation/transformation.py index f5d4da5996e..79fe13ce179 100644 --- a/litellm/llms/dashscope/image_generation/transformation.py +++ b/litellm/llms/dashscope/image_generation/transformation.py @@ -25,6 +25,7 @@ Response format: from collections.abc import Iterable, Mapping from typing import TYPE_CHECKING, Any, Final +from urllib.parse import urlparse import httpx from pydantic import ConfigDict, TypeAdapter @@ -42,6 +43,7 @@ from litellm.types.utils import ImageObject, ImageResponse from ..common_utils import ( DASHSCOPE_CHAT_COMPATIBLE_PATH, DASHSCOPE_IMAGE_PATH, + DASHSCOPE_NATIVE_V1_PATH, resolve_dashscope_endpoints, ) @@ -70,6 +72,10 @@ OPENAI_TO_DASHSCOPE_SIZE: Final[dict] = { } +def _is_native_endpoint_url(api_base: str) -> bool: + return urlparse(api_base).path.rstrip("/").startswith(f"{DASHSCOPE_NATIVE_V1_PATH}/") + + class DashScopeImageGenerationConfig(BaseImageGenerationConfig): """ Configuration for DashScope image generation (qwen-image-2.0, qwen-image-2.0-pro, @@ -128,6 +134,8 @@ class DashScopeImageGenerationConfig(BaseImageGenerationConfig): configured_api_base: Final = api_base or self._configured_chat_api_base() if configured_api_base and configured_api_base.rstrip("/").endswith(DASHSCOPE_CHAT_COMPATIBLE_PATH): return image_env_api_base or self._default_image_api_base() + if configured_api_base and _is_native_endpoint_url(configured_api_base): + return configured_api_base endpoints: Final = resolve_dashscope_endpoints(configured_api_base) if endpoints is not None: return (None if api_base else image_env_api_base) or f"{endpoints.origin}{DASHSCOPE_IMAGE_PATH}" diff --git a/tests/unit/llms/dashscope/test_qwen_brand_aliases.py b/tests/unit/llms/dashscope/test_qwen_brand_aliases.py index a5167ec5870..43ef8f90b39 100644 --- a/tests/unit/llms/dashscope/test_qwen_brand_aliases.py +++ b/tests/unit/llms/dashscope/test_qwen_brand_aliases.py @@ -2,9 +2,10 @@ import json import math import re from pathlib import Path -from typing import Final +from typing import Final, Literal import pytest +from typing_extensions import ReadOnly, TypedDict import litellm from litellm import completion, get_llm_provider @@ -54,6 +55,23 @@ DASHSCOPE_FAMILY_ENV_VARS = [ "QWEN_AI_PLATFORM_API_BASE_IMAGE", ] + + +class BrandCase(TypedDict): + provider: ReadOnly[str] + display_name: ReadOnly[str] + enum: ReadOnly[LlmProviders] + key_env: ReadOnly[str] + base_env: ReadOnly[str] + default_base: ReadOnly[str] + default_rerank_base: ReadOnly[str] + default_image_base: ReadOnly[str] + chat_config: ReadOnly[type[DashScopeChatConfig]] + embedding_config: ReadOnly[type[DashScopeEmbeddingConfig]] + rerank_config: ReadOnly[type[DashScopeRerankConfig]] + image_config: ReadOnly[type[DashScopeImageGenerationConfig]] + + BRAND_CASES = [ pytest.param( { @@ -214,7 +232,9 @@ class TestQwenBrandDefaultUrls: @pytest.mark.parametrize("brand", BRAND_CASES) @pytest.mark.parametrize(("config_key", "route"), [("chat_config", "chat/completions"), ("embedding_config", "embeddings")]) - def test_openai_routes_derive_from_a_domain_root(self, brand, config_key, route): + def test_openai_routes_derive_from_a_domain_root( + self, brand: BrandCase, config_key: Literal["chat_config", "embedding_config"], route: str + ) -> None: url: Final = brand[config_key]().get_complete_url( api_base="https://dashscope-intl.aliyuncs.com", api_key="sk-test", @@ -263,7 +283,7 @@ class TestQwenBrandDefaultUrls: assert url == brand["default_image_base"] @pytest.mark.parametrize("brand", BRAND_CASES) - def test_image_generation_uses_its_own_configured_base(self, monkeypatch, brand): + def test_image_generation_uses_its_own_configured_base(self, monkeypatch: pytest.MonkeyPatch, brand: BrandCase) -> None: monkeypatch.setenv("DASHSCOPE_API_BASE", "https://legacy.example.com/images") monkeypatch.setenv(brand["base_env"], "https://gateway.example.com/qwen/images") url: Final = brand["image_config"]().get_complete_url( diff --git a/tests/unit/test_dashscope_image_generation.py b/tests/unit/test_dashscope_image_generation.py index 6d3905c6574..abd80e9732e 100644 --- a/tests/unit/test_dashscope_image_generation.py +++ b/tests/unit/test_dashscope_image_generation.py @@ -97,6 +97,10 @@ class TestDashScopeImageGenerationConfig: "https://llm-abc123.cn-beijing.maas.aliyuncs.com/apps/anthropic", "https://llm-abc123.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation", ), + ( + "https://dashscope-us.aliyuncs.com/api/v1/", + "https://dashscope-us.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation", + ), ], ) def test_get_complete_url_derives_from_the_domain( @@ -105,6 +109,20 @@ class TestDashScopeImageGenerationConfig: url: Final = self.cfg.get_complete_url(api_base, None, "qwen-image-3.0", {}, {}) assert url == expected + @pytest.mark.parametrize( + "endpoint_url", + [ + "https://dashscope.aliyuncs.com/api/v1/services/aigc/image-generation/generation", + "https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis", + ], + ) + def test_get_complete_url_keeps_a_full_dashscope_endpoint_url( + self, monkeypatch: pytest.MonkeyPatch, endpoint_url: str + ): + monkeypatch.setenv("DASHSCOPE_API_BASE_IMAGE", "https://images.example.com/generate") + url: Final = self.cfg.get_complete_url(endpoint_url, None, "qwen-image-3.0", {}, {}) + assert url == endpoint_url + def test_validate_environment_sets_auth_header(self): headers = self.cfg.validate_environment( headers={},