From be9f9afecb26cf964b2da3f1d3f3ce2be7bc5b0f Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:34:39 -0700 Subject: [PATCH] fix(dashscope): keep configured image and rerank bases, normalize blank keys, derive brand chat bases - image calls fall back to the provider's own chat base env (DASHSCOPE_API_BASE, QWENCLOUD_API_BASE, QWEN_AI_PLATFORM_API_BASE) before the image default - an explicit /compatible-api/v1/reranks URL wins over DASHSCOPE_API_BASE_RERANK again - a blank DashScope key counts as unset on /v1/messages, so it never falls back to ANTHROPIC_API_KEY - QwenCloud and Qianwen AI Platform chat and embeddings derive their compatible-mode base from a domain root - tests annotate new variables with Final and drop the config-class assertion --- litellm/llms/dashscope/common_utils.py | 6 +++ .../image_generation/transformation.py | 10 +++-- .../llms/dashscope/messages/transformation.py | 8 +++- litellm/llms/dashscope/qwen_ai_platform.py | 9 +++-- litellm/llms/dashscope/qwencloud.py | 9 +++-- .../test_dashscope_chat_transformation.py | 11 +++--- .../dashscope/test_dashscope_common_utils.py | 10 +++-- ...test_dashscope_embedding_transformation.py | 9 +++-- .../test_dashscope_messages_transformation.py | 31 ++++++--------- .../test_dashscope_rerank_transformation.py | 11 +++++- .../test_dashscope_video_transformation.py | 4 +- .../llms/dashscope/test_qwen_brand_aliases.py | 26 +++++++++++++ tests/unit/test_dashscope_image_generation.py | 39 ++++++++++++++----- 13 files changed, 127 insertions(+), 56 deletions(-) diff --git a/litellm/llms/dashscope/common_utils.py b/litellm/llms/dashscope/common_utils.py index 3e3dd5e3a9a..cfcd54bbb80 100644 --- a/litellm/llms/dashscope/common_utils.py +++ b/litellm/llms/dashscope/common_utils.py @@ -146,7 +146,13 @@ def _rerank_origin(api_base: str | None) -> str | None: return endpoints.origin if endpoints is not None else _chat_shaped_aliyun_origin(api_base) +def _is_rerank_url(api_base: str) -> bool: + return urlparse(api_base).path.rstrip("/") == DASHSCOPE_RERANK_PATH + + def resolve_dashscope_family_rerank_api_base(api_base: str | None, env_var: str, default_rerank_base: str) -> str: + if api_base is not None and _is_rerank_url(api_base): + return api_base origin: Final = _rerank_origin(api_base) if origin is not None: return get_secret_str(env_var) or f"{origin}{DASHSCOPE_RERANK_PATH}" diff --git a/litellm/llms/dashscope/image_generation/transformation.py b/litellm/llms/dashscope/image_generation/transformation.py index 47ba12b6592..8aff266367a 100644 --- a/litellm/llms/dashscope/image_generation/transformation.py +++ b/litellm/llms/dashscope/image_generation/transformation.py @@ -109,6 +109,9 @@ class DashScopeImageGenerationConfig(BaseImageGenerationConfig): def _resolve_image_api_base(self, image_api_base: str | None) -> str: return image_api_base or get_secret_str("DASHSCOPE_API_BASE_IMAGE") or DEFAULT_API_BASE + def _configured_chat_api_base(self) -> str | None: + return get_secret_str("DASHSCOPE_API_BASE") + def get_complete_url( self, api_base: str | None, @@ -118,12 +121,13 @@ class DashScopeImageGenerationConfig(BaseImageGenerationConfig): litellm_params: dict, stream: bool | None = None, ) -> str: - if api_base and api_base.rstrip("/").endswith(DASHSCOPE_CHAT_COMPATIBLE_PATH): + 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 self._resolve_image_api_base(None) - endpoints: Final = resolve_dashscope_endpoints(api_base) + endpoints: Final = resolve_dashscope_endpoints(configured_api_base) if endpoints is not None: return f"{endpoints.origin}{DASHSCOPE_IMAGE_PATH}" - return self._resolve_image_api_base(api_base) + return self._resolve_image_api_base(configured_api_base) def validate_environment( self, diff --git a/litellm/llms/dashscope/messages/transformation.py b/litellm/llms/dashscope/messages/transformation.py index 879d2e546e7..9d0d1db91c8 100644 --- a/litellm/llms/dashscope/messages/transformation.py +++ b/litellm/llms/dashscope/messages/transformation.py @@ -10,7 +10,7 @@ from litellm.litellm_core_utils.url_utils import ensure_api_route from litellm.llms.anthropic.messages.native.transformation import ( AnthropicMessagesConfig, ) -from litellm.secret_managers.main import get_secret_str +from litellm.secret_managers.main import get_secret_str, normalize_nonempty_secret_str from ..common_utils import ( dashscope_base_selects_anthropic_messages, @@ -42,7 +42,11 @@ class DashScopeAnthropicMessagesConfig(AnthropicMessagesConfig): @staticmethod def get_api_key(api_key: str | None = None) -> str | None: - return api_key or get_secret_str("DASHSCOPE_API_KEY") or litellm.api_key + return ( + normalize_nonempty_secret_str(api_key) + or normalize_nonempty_secret_str(get_secret_str("DASHSCOPE_API_KEY")) + or normalize_nonempty_secret_str(litellm.api_key) + ) @staticmethod def get_api_base(api_base: str | None = None) -> str: diff --git a/litellm/llms/dashscope/qwen_ai_platform.py b/litellm/llms/dashscope/qwen_ai_platform.py index f7cc50cc82a..256e01941c0 100644 --- a/litellm/llms/dashscope/qwen_ai_platform.py +++ b/litellm/llms/dashscope/qwen_ai_platform.py @@ -3,7 +3,7 @@ from typing import Final from litellm.secret_managers.main import get_secret_str from .chat.transformation import DashScopeChatConfig -from .common_utils import resolve_dashscope_family_rerank_api_base +from .common_utils import resolve_dashscope_family_rerank_api_base, resolve_dashscope_openai_api_base from .embed.transformation import DashScopeEmbeddingConfig from .image_generation.transformation import DashScopeImageGenerationConfig from .rerank.transformation import DashScopeRerankConfig @@ -38,7 +38,7 @@ class QwenAIPlatformChatConfig(DashScopeChatConfig): return self._resolve_chat_api_base(api_base), _resolve_qwen_ai_platform_api_key(api_key) def _resolve_chat_api_base(self, api_base: str | None) -> str: - return api_base or get_secret_str("QWEN_AI_PLATFORM_API_BASE") or QWEN_AI_PLATFORM_API_BASE + return resolve_dashscope_openai_api_base(api_base, "QWEN_AI_PLATFORM_API_BASE", QWEN_AI_PLATFORM_API_BASE) class QwenAIPlatformEmbeddingConfig(DashScopeEmbeddingConfig): @@ -46,7 +46,7 @@ class QwenAIPlatformEmbeddingConfig(DashScopeEmbeddingConfig): return _require_qwen_ai_platform_api_key(api_key) def _resolve_embedding_api_base(self, api_base: str | None) -> str: - return api_base or get_secret_str("QWEN_AI_PLATFORM_API_BASE") or QWEN_AI_PLATFORM_API_BASE + return resolve_dashscope_openai_api_base(api_base, "QWEN_AI_PLATFORM_API_BASE", QWEN_AI_PLATFORM_API_BASE) class QwenAIPlatformRerankConfig(DashScopeRerankConfig): @@ -66,6 +66,9 @@ class QwenAIPlatformImageGenerationConfig(DashScopeImageGenerationConfig): def _resolve_image_api_base(self, image_api_base: str | None) -> str: return image_api_base or get_secret_str("QWEN_AI_PLATFORM_API_BASE_IMAGE") or QWEN_AI_PLATFORM_IMAGE_API_BASE + def _configured_chat_api_base(self) -> str | None: + return get_secret_str("QWEN_AI_PLATFORM_API_BASE") + class QwenAIPlatformVideoConfig(DashScopeVideoConfig): def _resolve_api_key(self, api_key: str | None) -> str: diff --git a/litellm/llms/dashscope/qwencloud.py b/litellm/llms/dashscope/qwencloud.py index a12e21b0983..b21a2af057a 100644 --- a/litellm/llms/dashscope/qwencloud.py +++ b/litellm/llms/dashscope/qwencloud.py @@ -3,7 +3,7 @@ from typing import Final from litellm.secret_managers.main import get_secret_str from .chat.transformation import DashScopeChatConfig -from .common_utils import resolve_dashscope_family_rerank_api_base +from .common_utils import resolve_dashscope_family_rerank_api_base, resolve_dashscope_openai_api_base from .embed.transformation import DashScopeEmbeddingConfig from .image_generation.transformation import DashScopeImageGenerationConfig from .rerank.transformation import DashScopeRerankConfig @@ -38,7 +38,7 @@ class QwenCloudChatConfig(DashScopeChatConfig): return self._resolve_chat_api_base(api_base), _resolve_qwencloud_api_key(api_key) def _resolve_chat_api_base(self, api_base: str | None) -> str: - return api_base or get_secret_str("QWENCLOUD_API_BASE") or QWENCLOUD_API_BASE + return resolve_dashscope_openai_api_base(api_base, "QWENCLOUD_API_BASE", QWENCLOUD_API_BASE) class QwenCloudEmbeddingConfig(DashScopeEmbeddingConfig): @@ -46,7 +46,7 @@ class QwenCloudEmbeddingConfig(DashScopeEmbeddingConfig): return _require_qwencloud_api_key(api_key) def _resolve_embedding_api_base(self, api_base: str | None) -> str: - return api_base or get_secret_str("QWENCLOUD_API_BASE") or QWENCLOUD_API_BASE + return resolve_dashscope_openai_api_base(api_base, "QWENCLOUD_API_BASE", QWENCLOUD_API_BASE) class QwenCloudRerankConfig(DashScopeRerankConfig): @@ -66,6 +66,9 @@ class QwenCloudImageGenerationConfig(DashScopeImageGenerationConfig): def _resolve_image_api_base(self, image_api_base: str | None) -> str: return image_api_base or get_secret_str("QWENCLOUD_API_BASE_IMAGE") or QWENCLOUD_IMAGE_API_BASE + def _configured_chat_api_base(self) -> str | None: + return get_secret_str("QWENCLOUD_API_BASE") + class QwenCloudVideoConfig(DashScopeVideoConfig): def _resolve_api_key(self, api_key: str | None) -> str: diff --git a/tests/unit/llms/dashscope/test_dashscope_chat_transformation.py b/tests/unit/llms/dashscope/test_dashscope_chat_transformation.py index 746f15ceb56..99a8004b0a1 100644 --- a/tests/unit/llms/dashscope/test_dashscope_chat_transformation.py +++ b/tests/unit/llms/dashscope/test_dashscope_chat_transformation.py @@ -5,20 +5,21 @@ These tests validate the DashScopeConfig class which extends OpenAIGPTConfig. DashScope is an OpenAI-compatible provider with minor customizations. """ +from typing import Final +import pytest from litellm.types.llms.openai import AllMessageValues -import pytest import litellm from litellm import completion from litellm.llms.dashscope.chat.transformation import DashScopeChatConfig -WORKSPACE = "https://llm-abc123.cn-beijing.maas.aliyuncs.com" +WORKSPACE: Final = "https://llm-abc123.cn-beijing.maas.aliyuncs.com" def test_get_complete_url_derives_the_compatible_base_from_any_standard_base(): - config = DashScopeChatConfig() + config: Final = DashScopeChatConfig() for api_base in ( WORKSPACE, @@ -26,7 +27,7 @@ def test_get_complete_url_derives_the_compatible_base_from_any_standard_base(): f"{WORKSPACE}/apps/anthropic", f"{WORKSPACE}/api/v1", ): - url = config.get_complete_url( + url: Final = config.get_complete_url( api_base=api_base, api_key="sk-test", model="qwen3.7-plus", @@ -38,7 +39,7 @@ def test_get_complete_url_derives_the_compatible_base_from_any_standard_base(): def test_get_complete_url_keeps_unrecognized_hosts(): - url = DashScopeChatConfig().get_complete_url( + url: Final = DashScopeChatConfig().get_complete_url( api_base="https://gateway.example.com/v1", api_key="sk-test", model="qwen3.7-plus", diff --git a/tests/unit/llms/dashscope/test_dashscope_common_utils.py b/tests/unit/llms/dashscope/test_dashscope_common_utils.py index aba20ce5c5c..ff46c970cc4 100644 --- a/tests/unit/llms/dashscope/test_dashscope_common_utils.py +++ b/tests/unit/llms/dashscope/test_dashscope_common_utils.py @@ -2,6 +2,8 @@ Unit tests for DashScope's protocol-specific base URL derivation. """ +from typing import Final + import pytest from litellm.llms.dashscope.common_utils import ( @@ -12,7 +14,7 @@ from litellm.llms.dashscope.common_utils import ( resolve_dashscope_openai_api_base, ) -WORKSPACE = "https://llm-abc123.cn-beijing.maas.aliyuncs.com" +WORKSPACE: Final = "https://llm-abc123.cn-beijing.maas.aliyuncs.com" @pytest.mark.parametrize( @@ -29,7 +31,7 @@ WORKSPACE = "https://llm-abc123.cn-beijing.maas.aliyuncs.com" ], ) def test_workspace_base_derives_every_protocol(api_base: str): - endpoints = resolve_dashscope_endpoints(api_base) + endpoints: Final = resolve_dashscope_endpoints(api_base) assert endpoints is not None assert endpoints.origin == WORKSPACE @@ -48,7 +50,7 @@ def test_workspace_base_derives_every_protocol(api_base: str): ) @pytest.mark.parametrize("path", ["", "/", "/compatible-mode/v1"]) def test_standard_hosts_derive_every_protocol(origin: str, path: str): - endpoints = resolve_dashscope_endpoints(f"{origin}{path}") + endpoints: Final = resolve_dashscope_endpoints(f"{origin}{path}") assert endpoints is not None assert endpoints.openai == f"{origin}/compatible-mode/v1" @@ -119,7 +121,7 @@ def test_rerank_base_derives_from_the_domain(monkeypatch: pytest.MonkeyPatch): def test_rerank_base_keeps_unrecognized_hosts(): - gateway = "https://gateway.example.com/v1" + gateway: Final = "https://gateway.example.com/v1" assert ( resolve_dashscope_family_rerank_api_base(gateway, "DASHSCOPE_API_BASE_RERANK", "https://default/reranks") diff --git a/tests/unit/llms/dashscope/test_dashscope_embedding_transformation.py b/tests/unit/llms/dashscope/test_dashscope_embedding_transformation.py index 5a161f2e088..27afbee3173 100644 --- a/tests/unit/llms/dashscope/test_dashscope_embedding_transformation.py +++ b/tests/unit/llms/dashscope/test_dashscope_embedding_transformation.py @@ -3,6 +3,7 @@ Unit tests for DashScope embedding transformation. """ import json +from typing import Final from unittest.mock import MagicMock import httpx @@ -16,11 +17,11 @@ from litellm.llms.dashscope.embed.transformation import ( ) from litellm.types.utils import EmbeddingResponse -WORKSPACE = "https://llm-abc123.cn-beijing.maas.aliyuncs.com" +WORKSPACE: Final = "https://llm-abc123.cn-beijing.maas.aliyuncs.com" def test_get_complete_url_derives_the_compatible_base_from_any_standard_base(): - config = DashScopeEmbeddingConfig() + config: Final = DashScopeEmbeddingConfig() for api_base in ( WORKSPACE, @@ -28,7 +29,7 @@ def test_get_complete_url_derives_the_compatible_base_from_any_standard_base(): f"{WORKSPACE}/apps/anthropic", f"{WORKSPACE}/api/v1", ): - url = config.get_complete_url( + url: Final = config.get_complete_url( api_base=api_base, api_key="sk-test", model="text-embedding-v4", @@ -40,7 +41,7 @@ def test_get_complete_url_derives_the_compatible_base_from_any_standard_base(): def test_get_complete_url_keeps_unrecognized_hosts(): - url = DashScopeEmbeddingConfig().get_complete_url( + url: Final = DashScopeEmbeddingConfig().get_complete_url( api_base="https://gateway.example.com/v1", api_key="sk-test", model="text-embedding-v4", diff --git a/tests/unit/llms/dashscope/test_dashscope_messages_transformation.py b/tests/unit/llms/dashscope/test_dashscope_messages_transformation.py index fa11742a7ea..decc12afd87 100644 --- a/tests/unit/llms/dashscope/test_dashscope_messages_transformation.py +++ b/tests/unit/llms/dashscope/test_dashscope_messages_transformation.py @@ -12,10 +12,8 @@ from litellm.exceptions import AuthenticationError from litellm.llms.dashscope.messages.transformation import ( DashScopeAnthropicMessagesConfig, ) -from litellm.types.utils import LlmProviders -from litellm.utils import ProviderConfigManager -WORKSPACE = "https://llm-abc123.cn-beijing.maas.aliyuncs.com" +WORKSPACE: Final = "https://llm-abc123.cn-beijing.maas.aliyuncs.com" INTL: Final = "https://dashscope-intl.aliyuncs.com" CODING_PLAN: Final = "https://coding-intl.dashscope.aliyuncs.com" CHAT_COMPLETION: Final = { @@ -76,9 +74,9 @@ MESSAGE: Final = { ], ) def test_get_complete_url(api_base: str | None, expected: str): - config = DashScopeAnthropicMessagesConfig() + config: Final = DashScopeAnthropicMessagesConfig() - url = config.get_complete_url( + url: Final = config.get_complete_url( api_base=api_base, api_key="sk-test", model="qwen3.7-plus", @@ -89,8 +87,13 @@ def test_get_complete_url(api_base: str | None, expected: str): assert url == expected -def test_missing_dashscope_key_does_not_fall_back_to_anthropic(monkeypatch: pytest.MonkeyPatch): +@pytest.mark.parametrize("dashscope_api_key", [None, " "]) +def test_missing_dashscope_key_does_not_fall_back_to_anthropic( + monkeypatch: pytest.MonkeyPatch, dashscope_api_key: str | None +): monkeypatch.delenv("DASHSCOPE_API_KEY", raising=False) + if dashscope_api_key is not None: + monkeypatch.setenv("DASHSCOPE_API_KEY", dashscope_api_key) monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False) monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-secret") monkeypatch.setattr(litellm, "api_key", None) @@ -115,7 +118,7 @@ def test_missing_dashscope_key_does_not_fall_back_to_anthropic(monkeypatch: pyte def test_caller_auth_header_is_honored_without_a_dashscope_key(monkeypatch: pytest.MonkeyPatch): monkeypatch.delenv("DASHSCOPE_API_KEY", raising=False) monkeypatch.setattr(litellm, "api_key", None) - headers = {"x-api-key": "caller-key"} + headers: Final = {"x-api-key": "caller-key"} resolved_headers, _ = DashScopeAnthropicMessagesConfig().validate_anthropic_messages_environment( headers=headers, @@ -133,7 +136,7 @@ def test_caller_auth_header_is_honored_without_a_dashscope_key(monkeypatch: pyte def test_caller_authorization_header_is_honored_without_a_dashscope_key(monkeypatch: pytest.MonkeyPatch): monkeypatch.delenv("DASHSCOPE_API_KEY", raising=False) monkeypatch.setattr(litellm, "api_key", None) - headers = {"Authorization": "Bearer caller-token"} + headers: Final = {"Authorization": "Bearer caller-token"} resolved_headers, _ = DashScopeAnthropicMessagesConfig().validate_anthropic_messages_environment( headers=headers, @@ -162,21 +165,11 @@ def test_dashscope_key_wins_over_an_anthropic_environment_key(monkeypatch: pytes api_base=WORKSPACE, ) - header_values = " ".join(resolved_headers.values()) + header_values: Final = " ".join(resolved_headers.values()) assert "sk-dashscope" in header_values assert "sk-ant-secret" not in header_values -def test_dashscope_resolves_to_the_native_messages_config(): - config = ProviderConfigManager.get_provider_anthropic_messages_config( - model="qwen3.7-plus", - provider=LlmProviders.DASHSCOPE, - ) - - assert isinstance(config, DashScopeAnthropicMessagesConfig) - assert config.custom_llm_provider == "dashscope" - - def test_dashscope_prefix_reaches_the_provider(): model, provider, _, _ = litellm.get_llm_provider( "dashscope/qwen3.7-plus", diff --git a/tests/unit/llms/dashscope/test_dashscope_rerank_transformation.py b/tests/unit/llms/dashscope/test_dashscope_rerank_transformation.py index 8bac3f69ebf..bdec779558f 100644 --- a/tests/unit/llms/dashscope/test_dashscope_rerank_transformation.py +++ b/tests/unit/llms/dashscope/test_dashscope_rerank_transformation.py @@ -3,6 +3,7 @@ Unit tests for DashScope rerank transformation. """ import json +from typing import Final from unittest.mock import MagicMock import httpx @@ -27,12 +28,20 @@ class TestDashScopeRerankURL: assert url == DEFAULT_RERANK_URL def test_domain_root_remaps_to_rerank_route(self): - url = self.config.get_complete_url( + url: Final = self.config.get_complete_url( api_base="https://llm-abc123.cn-beijing.maas.aliyuncs.com", model="qwen3-rerank", ) assert url == "https://llm-abc123.cn-beijing.maas.aliyuncs.com/compatible-api/v1/reranks" + def test_explicit_rerank_url_wins_over_the_rerank_env(self, monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("DASHSCOPE_API_BASE_RERANK", DEFAULT_RERANK_URL) + explicit_url: Final = "https://dashscope-intl.aliyuncs.com/compatible-api/v1/reranks" + + url: Final = self.config.get_complete_url(api_base=explicit_url, model="qwen3-rerank") + + assert url == explicit_url + def test_chat_shaped_base_remaps_to_rerank_route(self): url = self.config.get_complete_url( api_base="https://dashscope.aliyuncs.com/compatible-mode/v1", diff --git a/tests/unit/llms/dashscope/test_dashscope_video_transformation.py b/tests/unit/llms/dashscope/test_dashscope_video_transformation.py index 04ed634b379..c1b4157c8ab 100644 --- a/tests/unit/llms/dashscope/test_dashscope_video_transformation.py +++ b/tests/unit/llms/dashscope/test_dashscope_video_transformation.py @@ -1120,14 +1120,14 @@ class TestDashScopeVideoEnvironment: assert url == "https://ws-123.cn-beijing.maas.aliyuncs.com" def test_every_protocol_base_normalizes_to_the_host(self): - workspace = "https://ws-123.cn-beijing.maas.aliyuncs.com" + workspace: Final = "https://ws-123.cn-beijing.maas.aliyuncs.com" for api_base in ( f"{workspace}/compatible-mode/v1", f"{workspace}/apps/anthropic", f"{workspace}/api/v1", ): - url = DashScopeVideoConfig().get_complete_url( + url: Final = DashScopeVideoConfig().get_complete_url( model="wan3.0-video", api_base=api_base, litellm_params={} ) diff --git a/tests/unit/llms/dashscope/test_qwen_brand_aliases.py b/tests/unit/llms/dashscope/test_qwen_brand_aliases.py index 69ae66d3818..a5167ec5870 100644 --- a/tests/unit/llms/dashscope/test_qwen_brand_aliases.py +++ b/tests/unit/llms/dashscope/test_qwen_brand_aliases.py @@ -2,6 +2,7 @@ import json import math import re from pathlib import Path +from typing import Final import pytest @@ -211,6 +212,18 @@ class TestQwenBrandDefaultUrls: ) assert url == f"{brand['default_base']}/embeddings" + @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): + url: Final = brand[config_key]().get_complete_url( + api_base="https://dashscope-intl.aliyuncs.com", + api_key="sk-test", + model="qwen-max", + optional_params={}, + litellm_params={}, + ) + assert url == f"https://dashscope-intl.aliyuncs.com/compatible-mode/v1/{route}" + @pytest.mark.parametrize("brand", BRAND_CASES) def test_rerank_complete_url(self, brand): url = brand["rerank_config"]().get_complete_url(api_base=None, model="gte-rerank-v2") @@ -249,6 +262,19 @@ 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): + 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( + api_base=None, + api_key="sk-test", + model="qwen-image", + optional_params={}, + litellm_params={}, + ) + assert url == "https://gateway.example.com/qwen/images" + @pytest.mark.parametrize("brand", BRAND_CASES) def test_validate_environment_requires_key(self, brand): with pytest.raises(ValueError, match="DASHSCOPE_API_KEY"): diff --git a/tests/unit/test_dashscope_image_generation.py b/tests/unit/test_dashscope_image_generation.py index 2cbf28aca9e..5f99a11519f 100644 --- a/tests/unit/test_dashscope_image_generation.py +++ b/tests/unit/test_dashscope_image_generation.py @@ -5,6 +5,7 @@ qwen-image-3.0, qwen-image-3.0-pro). Run in docker: pytest tests/unit/test_dashscope_image_generation.py -v """ +from typing import Final from unittest.mock import MagicMock, patch import httpx @@ -101,7 +102,7 @@ class TestDashScopeImageGenerationConfig: def test_get_complete_url_derives_from_the_domain( self, api_base: str, expected: str ): - url = self.cfg.get_complete_url(api_base, None, "qwen-image-3.0", {}, {}) + url: Final = self.cfg.get_complete_url(api_base, None, "qwen-image-3.0", {}, {}) assert url == expected def test_validate_environment_sets_auth_header(self): @@ -487,30 +488,48 @@ def test_litellm_image_generation_dashscope_end_to_end(model: str): @pytest.mark.parametrize( - ("api_base", "image_base_env", "expected_url"), + ("api_base", "chat_base_env", "image_base_env", "expected_url"), [ - (None, "", DEFAULT_API_BASE), - ("https://dashscope.aliyuncs.com", "", f"{DASHSCOPE_DEFAULT_ORIGIN}{DASHSCOPE_IMAGE_PATH}"), - (None, "https://images.example.com/generate", "https://images.example.com/generate"), + (None, "", "", DEFAULT_API_BASE), + ("https://dashscope.aliyuncs.com", "", "", f"{DASHSCOPE_DEFAULT_ORIGIN}{DASHSCOPE_IMAGE_PATH}"), + (None, "", "https://images.example.com/generate", "https://images.example.com/generate"), ( "https://dashscope.aliyuncs.com/compatible-mode/v1", + "", "https://images.example.com/generate", "https://images.example.com/generate", ), + (None, "https://gateway.example.com/dashscope/images", "", "https://gateway.example.com/dashscope/images"), + ( + None, + "https://dashscope-us.aliyuncs.com", + "", + f"https://dashscope-us.aliyuncs.com{DASHSCOPE_IMAGE_PATH}", + ), + ( + "https://dashscope-intl.aliyuncs.com", + "https://gateway.example.com/dashscope/images", + "", + f"https://dashscope-intl.aliyuncs.com{DASHSCOPE_IMAGE_PATH}", + ), ], ) -def test_image_generation_resolves_from_the_callers_base_not_the_chat_default( - monkeypatch: pytest.MonkeyPatch, api_base: str | None, image_base_env: str, expected_url: str +def test_image_generation_resolves_from_the_configured_base_not_the_chat_default( + monkeypatch: pytest.MonkeyPatch, + api_base: str | None, + chat_base_env: str, + image_base_env: str, + expected_url: str, ): - monkeypatch.delenv("DASHSCOPE_API_BASE", raising=False) + monkeypatch.setenv("DASHSCOPE_API_BASE", chat_base_env) monkeypatch.setenv("DASHSCOPE_API_BASE_IMAGE", image_base_env) - image_url = "https://dashscope-result.oss.aliyuncs.com/t.png" + image_url: Final = "https://dashscope-result.oss.aliyuncs.com/t.png" with respx.mock(assert_all_called=True) as upstream: upstream.post(expected_url).respond( 200, json={"output": {"choices": [{"message": {"content": [{"image": image_url}]}}]}} ) - response = litellm.image_generation( + response: Final = litellm.image_generation( model="dashscope/qwen-image-3.0", prompt="a puppy", api_key="sk-test-key", api_base=api_base )