diff --git a/litellm/llms/dashscope/image_generation/transformation.py b/litellm/llms/dashscope/image_generation/transformation.py index cfd6b78916e..d1483254fe0 100644 --- a/litellm/llms/dashscope/image_generation/transformation.py +++ b/litellm/llms/dashscope/image_generation/transformation.py @@ -122,9 +122,7 @@ class DashScopeImageGenerationConfig(BaseImageGenerationConfig): return f"{endpoints.origin}{DASHSCOPE_IMAGE_PATH}" image_api_base: Final = ( - None - if api_base and api_base.rstrip("/").endswith(DASHSCOPE_CHAT_COMPATIBLE_PATH) - else api_base + None if api_base and api_base.rstrip("/").endswith(DASHSCOPE_CHAT_COMPATIBLE_PATH) else api_base ) return self._resolve_image_api_base(image_api_base) diff --git a/litellm/llms/dashscope/messages/transformation.py b/litellm/llms/dashscope/messages/transformation.py index d46fe06f1f7..10fdd539305 100644 --- a/litellm/llms/dashscope/messages/transformation.py +++ b/litellm/llms/dashscope/messages/transformation.py @@ -2,9 +2,10 @@ DashScope Anthropic-compatible Messages transformation config. """ -from typing import Any # noqa: TID251 # override below must mirror the legacy base signature +from typing import ClassVar, Final import litellm +from litellm.exceptions import AuthenticationError from litellm.litellm_core_utils.url_utils import ensure_api_route from litellm.llms.anthropic.pass_through.messages.transformation import ( AnthropicMessagesConfig, @@ -21,6 +22,8 @@ class DashScopeAnthropicMessagesConfig(AnthropicMessagesConfig): ``dashscope.aliyuncs.com`` and ``{workspace}.{region}.maas.aliyuncs.com``. """ + _workload_identity_eligible: ClassVar[bool] = False + @property def custom_llm_provider(self) -> str | None: return "dashscope" @@ -36,23 +39,35 @@ class DashScopeAnthropicMessagesConfig(AnthropicMessagesConfig): def get_api_base(api_base: str | None = None) -> str: return resolve_dashscope_anthropic_api_base(api_base) - def validate_anthropic_messages_environment( + def validate_anthropic_messages_environment( # pyright: ignore[reportUnknownParameterType] # return type mirrors the base signature self, - headers: dict, # mutable-ok: mirrors the legacy base override signature + headers: dict[str, str], # mutable-ok: mirrors the legacy base override signature model: str, - messages: list[Any], # mutable-ok: mirrors the legacy base override signature - optional_params: dict, # mutable-ok: mirrors the legacy base override signature - litellm_params: dict, # mutable-ok: mirrors the legacy base override signature + messages: list[object], # mutable-ok: mirrors the legacy base override signature + optional_params: dict[str, object], # mutable-ok: mirrors the legacy base override signature + litellm_params: dict[str, object], # mutable-ok: mirrors the legacy base override signature api_key: str | None = None, api_base: str | None = None, - ) -> tuple[dict, str | None]: # mutable-ok: mirrors the legacy base override signature - return super().validate_anthropic_messages_environment( + ) -> tuple[dict, str | None]: # mutable-ok: mirrors the base signature # pyright: ignore[reportMissingTypeArgument] # mirrors the base signature + resolved_api_key: Final = self.get_api_key(api_key=api_key) + if resolved_api_key is None: + present_headers: Final = {name.lower() for name in headers} + if "x-api-key" not in present_headers and "authorization" not in present_headers: + raise AuthenticationError( + message=( + "Missing DashScope API key. Set DASHSCOPE_API_KEY or pass api_key explicitly. " + "Anthropic credentials are never sent to a DashScope endpoint." + ), + llm_provider="dashscope", + model=model, + ) + return super().validate_anthropic_messages_environment( # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] # base signature uses an unparameterized dict headers=headers, model=model, messages=messages, optional_params=optional_params, litellm_params=litellm_params, - api_key=self.get_api_key(api_key=api_key), + api_key=resolved_api_key, api_base=api_base, ) @@ -61,8 +76,8 @@ class DashScopeAnthropicMessagesConfig(AnthropicMessagesConfig): api_base: str | None, api_key: str | None, model: str, - optional_params: dict, # mutable-ok: mirrors the base get_complete_url signature - litellm_params: dict, # mutable-ok: mirrors the base get_complete_url signature + optional_params: dict[str, object], # mutable-ok: mirrors the base get_complete_url signature + litellm_params: dict[str, object], # mutable-ok: mirrors the base get_complete_url signature stream: bool | None = None, ) -> str: return ensure_api_route(self.get_api_base(api_base=api_base), "v1/messages") diff --git a/tests/unit/llms/dashscope/test_dashscope_messages_transformation.py b/tests/unit/llms/dashscope/test_dashscope_messages_transformation.py index 3151d9f1521..5c3e6ec72dc 100644 --- a/tests/unit/llms/dashscope/test_dashscope_messages_transformation.py +++ b/tests/unit/llms/dashscope/test_dashscope_messages_transformation.py @@ -5,6 +5,7 @@ Unit tests for DashScope's Anthropic-compatible Messages endpoint. import pytest import litellm +from litellm.exceptions import AuthenticationError from litellm.llms.dashscope.messages.transformation import ( DashScopeAnthropicMessagesConfig, ) @@ -65,6 +66,66 @@ 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): + monkeypatch.delenv("DASHSCOPE_API_KEY", raising=False) + monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False) + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-secret") + monkeypatch.setattr(litellm, "api_key", None) + headers: dict[str, str] = {} + + with pytest.raises(AuthenticationError) as error: + DashScopeAnthropicMessagesConfig().validate_anthropic_messages_environment( + headers=headers, + model="qwen3.7-plus", + messages=[], + optional_params={}, + litellm_params={}, + api_key=None, + api_base=WORKSPACE, + ) + + assert "DashScope" in str(error.value) + assert "sk-ant-secret" not in str(error.value) + assert {name.lower() for name in headers}.isdisjoint({"x-api-key", "authorization"}) + + +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"} + + resolved_headers, _ = DashScopeAnthropicMessagesConfig().validate_anthropic_messages_environment( + headers=headers, + model="qwen3.7-plus", + messages=[], + optional_params={}, + litellm_params={}, + api_key=None, + api_base=WORKSPACE, + ) + + assert resolved_headers["x-api-key"] == "caller-key" + + +def test_dashscope_key_wins_over_an_anthropic_environment_key(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setenv("DASHSCOPE_API_KEY", "sk-dashscope") + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-secret") + + resolved_headers, _ = DashScopeAnthropicMessagesConfig().validate_anthropic_messages_environment( + headers={}, + model="qwen3.7-plus", + messages=[], + optional_params={}, + litellm_params={}, + api_key=None, + api_base=WORKSPACE, + ) + + header_values = " ".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",