feat(dashscope): derive protocol bases from one domain

Accept the DashScope domain root, the OpenAI-compatible base, the Anthropic
Messages base, or a native /api/v1 base and derive every surface from it
Adds a native Anthropic Messages config for /apps/anthropic so a single
deployment serves chat, messages, embeddings, rerank, image generation, and
video against the same credential

Co-Authored-By: AaronHowell <111272993+AaronHowell@users.noreply.github.com>
This commit is contained in:
Rick 2026-10-21 16:36:45 +08:00
parent 66c8f5b1a9
commit d260909f04
15 changed files with 542 additions and 31 deletions

View file

@ -10,6 +10,7 @@ from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import AllMessageValues, ChatCompletionToolParam
from ...openai.chat.gpt_transformation import OpenAIGPTConfig
from ..common_utils import resolve_dashscope_openai_api_base
class DashScopeChatConfig(OpenAIGPTConfig):
@ -55,11 +56,9 @@ class DashScopeChatConfig(OpenAIGPTConfig):
def _get_openai_compatible_provider_info(
self, api_base: str | None, api_key: str | None
) -> tuple[str | None, str | None]:
api_base = (
api_base or get_secret_str("DASHSCOPE_API_BASE") or "https://dashscope.aliyuncs.com/compatible-mode/v1"
)
resolved_api_base: Final = resolve_dashscope_openai_api_base(api_base)
dynamic_api_key: Final = api_key or get_secret_str("DASHSCOPE_API_KEY")
return api_base, dynamic_api_key
return resolved_api_base, dynamic_api_key
def get_openai_compatible_provider_info(
self,
@ -69,7 +68,7 @@ class DashScopeChatConfig(OpenAIGPTConfig):
return self._get_openai_compatible_provider_info(api_base, api_key)
def _resolve_chat_api_base(self, api_base: str | None) -> str:
return api_base or "https://dashscope.aliyuncs.com/compatible-mode/v1"
return resolve_dashscope_openai_api_base(api_base)
def get_complete_url(
self,

View file

@ -2,6 +2,7 @@
Common utilities for the DashScope LLM provider.
"""
from dataclasses import dataclass
from typing import TYPE_CHECKING, Final
from urllib.parse import urlparse
@ -19,25 +20,113 @@ if TYPE_CHECKING:
from litellm.llms.base_llm.videos.transformation import BaseVideoConfig
DASHSCOPE_CHAT_COMPATIBLE_PATH: Final = "/compatible-mode/v1"
DASHSCOPE_RERANK_PATH: Final = "/compatible-api/v1/reranks"
DASHSCOPE_ANTHROPIC_PATH: Final = "/apps/anthropic"
DASHSCOPE_NATIVE_V1_PATH: Final = "/api/v1"
DASHSCOPE_COMPATIBLE_API_PATH: Final = "/compatible-api/v1"
DASHSCOPE_RERANK_PATH: Final = f"{DASHSCOPE_COMPATIBLE_API_PATH}/reranks"
DASHSCOPE_IMAGE_PATH: Final = "/api/v1/services/aigc/multimodal-generation/generation"
DASHSCOPE_DEFAULT_ORIGIN: Final = "https://dashscope.aliyuncs.com"
_STANDARD_DASHSCOPE_HOSTS: Final = frozenset(
{
"dashscope.aliyuncs.com",
"dashscope-intl.aliyuncs.com",
"dashscope-us.aliyuncs.com",
}
)
_WORKSPACE_HOST_SUFFIX: Final = ".maas.aliyuncs.com"
_EXCLUDED_WORKSPACE_PREFIXES: Final = ("trial.", "token-plan.")
_PROTOCOL_PATH_PREFIXES: Final = (
DASHSCOPE_CHAT_COMPATIBLE_PATH,
DASHSCOPE_ANTHROPIC_PATH,
DASHSCOPE_NATIVE_V1_PATH,
DASHSCOPE_COMPATIBLE_API_PATH,
)
def _rerank_base_for_chat_shaped_base(api_base: str | None) -> str | None:
if api_base is None:
@dataclass(frozen=True, slots=True)
class DashScopeEndpoints:
origin: str
openai: str
anthropic: str
native: str
def _is_dashscope_host(host: str) -> bool:
if host in _STANDARD_DASHSCOPE_HOSTS:
return True
return host.endswith(_WORKSPACE_HOST_SUFFIX) and not host.startswith(_EXCLUDED_WORKSPACE_PREFIXES)
def _dashscope_origin(api_base: str | None) -> str | None:
if not api_base:
return None
parsed: Final = urlparse(api_base)
host: Final = parsed.hostname or ""
on_aliyun_host: Final = host == "aliyuncs.com" or host.endswith(".aliyuncs.com")
if not on_aliyun_host or parsed.path.rstrip("/") != DASHSCOPE_CHAT_COMPATIBLE_PATH:
if parsed.scheme not in ("http", "https") or not parsed.netloc:
return None
return f"{parsed.scheme}://{parsed.netloc}{DASHSCOPE_RERANK_PATH}"
if parsed.query or parsed.fragment:
return None
host: Final = (parsed.hostname or "").lower()
if not _is_dashscope_host(host):
return None
path: Final = parsed.path.rstrip("/")
if path and not any(path == prefix or path.startswith(f"{prefix}/") for prefix in _PROTOCOL_PATH_PREFIXES):
return None
return f"{parsed.scheme}://{parsed.netloc}"
def resolve_dashscope_endpoints(api_base: str | None) -> DashScopeEndpoints | None:
"""
Derive DashScope's protocol-specific bases from any standard DashScope base.
Accepts the domain root, an OpenAI-compatible base, an Anthropic Messages
base, or a native `/api/v1` base for `dashscope.aliyuncs.com`,
`dashscope-intl.aliyuncs.com`, `dashscope-us.aliyuncs.com`, and
`{workspace}.{region}.maas.aliyuncs.com`. Returns None for any other host
or path so callers keep the caller-supplied base untouched.
"""
origin: Final = _dashscope_origin(api_base)
if origin is None:
return None
return DashScopeEndpoints(
origin=origin,
openai=f"{origin}{DASHSCOPE_CHAT_COMPATIBLE_PATH}",
anthropic=f"{origin}{DASHSCOPE_ANTHROPIC_PATH}",
native=f"{origin}{DASHSCOPE_NATIVE_V1_PATH}",
)
def resolve_dashscope_openai_api_base(
api_base: str | None,
env_var: str = "DASHSCOPE_API_BASE",
default: str = f"{DASHSCOPE_DEFAULT_ORIGIN}{DASHSCOPE_CHAT_COMPATIBLE_PATH}",
) -> str:
candidate: Final = api_base or get_secret_str(env_var)
endpoints: Final = resolve_dashscope_endpoints(candidate)
if endpoints is not None:
return endpoints.openai
return candidate or default
def resolve_dashscope_anthropic_api_base(
api_base: str | None,
env_var: str = "DASHSCOPE_API_BASE",
default: str = f"{DASHSCOPE_DEFAULT_ORIGIN}{DASHSCOPE_ANTHROPIC_PATH}",
) -> str:
candidate: Final = api_base or get_secret_str(env_var)
endpoints: Final = resolve_dashscope_endpoints(candidate)
if endpoints is not None:
return endpoints.anthropic
return candidate or default
def resolve_dashscope_family_rerank_api_base(api_base: str | None, env_var: str, default_rerank_base: str) -> str:
remapped: Final = _rerank_base_for_chat_shaped_base(api_base)
if api_base is not None and remapped is None:
endpoints: Final = resolve_dashscope_endpoints(api_base)
if endpoints is not None:
return get_secret_str(env_var) or f"{endpoints.origin}{DASHSCOPE_RERANK_PATH}"
if api_base is not None:
return api_base
return get_secret_str(env_var) or remapped or default_rerank_base
return get_secret_str(env_var) or default_rerank_base
def get_dashscope_family_embedding_config(custom_llm_provider: str) -> "BaseEmbeddingConfig":

View file

@ -23,7 +23,7 @@ from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import AllEmbeddingInputValues, AllMessageValues
from litellm.types.utils import EmbeddingResponse, Usage
from ..common_utils import DashScopeError
from ..common_utils import DashScopeError, resolve_dashscope_openai_api_base
DEFAULT_API_BASE: Final = "https://dashscope.aliyuncs.com/compatible-mode/v1"
@ -72,7 +72,7 @@ class DashScopeEmbeddingConfig(BaseEmbeddingConfig):
return resolved_api_key
def _resolve_embedding_api_base(self, api_base: str | None) -> str:
return api_base or get_secret_str("DASHSCOPE_API_BASE") or DEFAULT_API_BASE
return resolve_dashscope_openai_api_base(api_base, default=DEFAULT_API_BASE)
def validate_environment(
self,

View file

@ -39,6 +39,12 @@ from litellm.types.llms.openai import (
)
from litellm.types.utils import ImageObject, ImageResponse
from ..common_utils import (
DASHSCOPE_CHAT_COMPATIBLE_PATH,
DASHSCOPE_IMAGE_PATH,
resolve_dashscope_endpoints,
)
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
from litellm.litellm_core_utils.tokenizer import Encoding as Tokenizer
@ -52,8 +58,6 @@ _JSON_OBJECTS: Final = TypeAdapter(Iterable[Mapping[str, object]], config=Config
DEFAULT_API_BASE: Final = "https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
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",
@ -113,8 +117,14 @@ class DashScopeImageGenerationConfig(BaseImageGenerationConfig):
litellm_params: dict,
stream: bool | None = None,
) -> str:
endpoints: Final = resolve_dashscope_endpoints(api_base)
if endpoints is not None:
return f"{endpoints.origin}{DASHSCOPE_IMAGE_PATH}"
image_api_base: Final = (
api_base if api_base and not api_base.rstrip("/").endswith(CHAT_COMPATIBLE_MODE_PATH) else None
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)

View file

@ -0,0 +1,7 @@
"""
DashScope Anthropic-compatible Messages Module
"""
from .transformation import DashScopeAnthropicMessagesConfig
__all__ = ["DashScopeAnthropicMessagesConfig"]

View file

@ -0,0 +1,68 @@
"""
DashScope Anthropic-compatible Messages transformation config.
"""
from typing import Any # noqa: TID251 # override below must mirror the legacy base signature
import litellm
from litellm.litellm_core_utils.url_utils import ensure_api_route
from litellm.llms.anthropic.pass_through.messages.transformation import (
AnthropicMessagesConfig,
)
from litellm.secret_managers.main import get_secret_str
from ..common_utils import resolve_dashscope_anthropic_api_base
class DashScopeAnthropicMessagesConfig(AnthropicMessagesConfig):
"""
DashScope exposes an Anthropic-compatible Messages API at
``{origin}/apps/anthropic`` for every DashScope domain, including
``dashscope.aliyuncs.com`` and ``{workspace}.{region}.maas.aliyuncs.com``.
"""
@property
def custom_llm_provider(self) -> str | None:
return "dashscope"
def should_strip_billing_metadata(self) -> bool:
return True
@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
@staticmethod
def get_api_base(api_base: str | None = None) -> str:
return resolve_dashscope_anthropic_api_base(api_base)
def validate_anthropic_messages_environment(
self,
headers: dict, # 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
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(
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_base=api_base,
)
def get_complete_url(
self,
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
stream: bool | None = None,
) -> str:
return ensure_api_route(self.get_api_base(api_base=api_base), "v1/messages")

View file

@ -38,6 +38,8 @@ from litellm.types.videos.utils import (
)
from litellm.utils import get_model_info
from ..common_utils import resolve_dashscope_endpoints
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler
@ -206,6 +208,9 @@ def _parse_task_response(raw_response: httpx.Response) -> _TaskResponse:
def _normalized_api_base(api_base: str) -> str:
endpoints: Final = resolve_dashscope_endpoints(api_base)
if endpoints is not None:
return endpoints.origin
trimmed: Final = api_base.rstrip("/")
if trimmed.endswith(DASHSCOPE_COMPATIBLE_MODE_PATH):
return trimmed[: -len(DASHSCOPE_COMPATIBLE_MODE_PATH)]

View file

@ -9720,6 +9720,12 @@ class ProviderConfigManager:
)
return TencentAnthropicMessagesConfig()
elif litellm.LlmProviders.DASHSCOPE == provider:
from litellm.llms.dashscope.messages.transformation import (
DashScopeAnthropicMessagesConfig,
)
return DashScopeAnthropicMessagesConfig()
elif litellm.LlmProviders.GITHUB_COPILOT == provider:
if "claude" in model_lower:
from litellm.llms.github_copilot.messages.transformation import (

View file

@ -14,6 +14,40 @@ import litellm
from litellm import completion
from litellm.llms.dashscope.chat.transformation import DashScopeChatConfig
WORKSPACE = "https://llm-abc123.cn-beijing.maas.aliyuncs.com"
def test_get_complete_url_derives_the_compatible_base_from_any_standard_base():
config = DashScopeChatConfig()
for api_base in (
WORKSPACE,
f"{WORKSPACE}/compatible-mode/v1",
f"{WORKSPACE}/apps/anthropic",
f"{WORKSPACE}/api/v1",
):
url = config.get_complete_url(
api_base=api_base,
api_key="sk-test",
model="qwen3.7-plus",
optional_params={},
litellm_params={},
)
assert url == f"{WORKSPACE}/compatible-mode/v1/chat/completions"
def test_get_complete_url_keeps_unrecognized_hosts():
url = DashScopeChatConfig().get_complete_url(
api_base="https://gateway.example.com/v1",
api_key="sk-test",
model="qwen3.7-plus",
optional_params={},
litellm_params={},
)
assert url == "https://gateway.example.com/v1/chat/completions"
class TestDashScopeConfig:
"""Test class for DashScope functionality"""

View file

@ -0,0 +1,136 @@
"""
Unit tests for DashScope's protocol-specific base URL derivation.
"""
import pytest
from litellm.llms.dashscope.common_utils import (
DASHSCOPE_DEFAULT_ORIGIN,
resolve_dashscope_anthropic_api_base,
resolve_dashscope_endpoints,
resolve_dashscope_family_rerank_api_base,
resolve_dashscope_openai_api_base,
)
WORKSPACE = "https://llm-abc123.cn-beijing.maas.aliyuncs.com"
@pytest.mark.parametrize(
"api_base",
[
WORKSPACE,
f"{WORKSPACE}/",
f"{WORKSPACE}/compatible-mode/v1",
f"{WORKSPACE}/compatible-mode/v1/chat/completions",
f"{WORKSPACE}/apps/anthropic",
f"{WORKSPACE}/apps/anthropic/v1/messages",
f"{WORKSPACE}/api/v1",
f"{WORKSPACE}/api/v1/services/aigc/video-generation/video-synthesis",
],
)
def test_workspace_base_derives_every_protocol(api_base: str):
endpoints = resolve_dashscope_endpoints(api_base)
assert endpoints is not None
assert endpoints.origin == WORKSPACE
assert endpoints.openai == f"{WORKSPACE}/compatible-mode/v1"
assert endpoints.anthropic == f"{WORKSPACE}/apps/anthropic"
assert endpoints.native == f"{WORKSPACE}/api/v1"
@pytest.mark.parametrize(
"origin",
[
"https://dashscope.aliyuncs.com",
"https://dashscope-intl.aliyuncs.com",
"https://dashscope-us.aliyuncs.com",
],
)
@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}")
assert endpoints is not None
assert endpoints.openai == f"{origin}/compatible-mode/v1"
assert endpoints.anthropic == f"{origin}/apps/anthropic"
assert endpoints.native == f"{origin}/api/v1"
@pytest.mark.parametrize(
"api_base",
[
None,
"",
"not-a-url",
"https://gateway.example.com",
"https://gateway.example.com/compatible-mode/v1",
f"{WORKSPACE}/custom-path",
f"{WORKSPACE}/compatible-mode/v1?api-version=1",
"https://trial.cn-beijing.maas.aliyuncs.com",
"https://token-plan.cn-beijing.maas.aliyuncs.com",
],
)
def test_unrecognized_bases_are_not_rewritten(api_base: str | None):
assert resolve_dashscope_endpoints(api_base) is None
def test_openai_base_derives_from_the_domain(monkeypatch: pytest.MonkeyPatch):
monkeypatch.delenv("DASHSCOPE_API_BASE", raising=False)
assert resolve_dashscope_openai_api_base(WORKSPACE) == f"{WORKSPACE}/compatible-mode/v1"
assert resolve_dashscope_openai_api_base(f"{WORKSPACE}/apps/anthropic") == f"{WORKSPACE}/compatible-mode/v1"
def test_openai_base_keeps_unrecognized_hosts(monkeypatch: pytest.MonkeyPatch):
monkeypatch.delenv("DASHSCOPE_API_BASE", raising=False)
assert resolve_dashscope_openai_api_base("https://gateway.example.com/v1") == "https://gateway.example.com/v1"
def test_openai_base_defaults_to_dashscope(monkeypatch: pytest.MonkeyPatch):
monkeypatch.delenv("DASHSCOPE_API_BASE", raising=False)
assert resolve_dashscope_openai_api_base(None) == f"{DASHSCOPE_DEFAULT_ORIGIN}/compatible-mode/v1"
def test_anthropic_base_derives_from_the_domain(monkeypatch: pytest.MonkeyPatch):
monkeypatch.delenv("DASHSCOPE_API_BASE", raising=False)
assert resolve_dashscope_anthropic_api_base(WORKSPACE) == f"{WORKSPACE}/apps/anthropic"
assert resolve_dashscope_anthropic_api_base(f"{WORKSPACE}/compatible-mode/v1") == f"{WORKSPACE}/apps/anthropic"
def test_anthropic_base_keeps_unrecognized_hosts(monkeypatch: pytest.MonkeyPatch):
monkeypatch.delenv("DASHSCOPE_API_BASE", raising=False)
assert (
resolve_dashscope_anthropic_api_base("https://gateway.example.com/anthropic")
== "https://gateway.example.com/anthropic"
)
def test_rerank_base_derives_from_the_domain(monkeypatch: pytest.MonkeyPatch):
monkeypatch.delenv("DASHSCOPE_API_BASE_RERANK", raising=False)
assert (
resolve_dashscope_family_rerank_api_base(WORKSPACE, "DASHSCOPE_API_BASE_RERANK", "https://default/reranks")
== f"{WORKSPACE}/compatible-api/v1/reranks"
)
def test_rerank_base_keeps_unrecognized_hosts():
gateway = "https://gateway.example.com/v1"
assert (
resolve_dashscope_family_rerank_api_base(gateway, "DASHSCOPE_API_BASE_RERANK", "https://default/reranks")
== gateway
)
def test_rerank_base_uses_the_configured_default(monkeypatch: pytest.MonkeyPatch):
monkeypatch.delenv("DASHSCOPE_API_BASE_RERANK", raising=False)
assert (
resolve_dashscope_family_rerank_api_base(None, "DASHSCOPE_API_BASE_RERANK", "https://default/reranks")
== "https://default/reranks"
)

View file

@ -16,6 +16,40 @@ from litellm.llms.dashscope.embed.transformation import (
)
from litellm.types.utils import EmbeddingResponse
WORKSPACE = "https://llm-abc123.cn-beijing.maas.aliyuncs.com"
def test_get_complete_url_derives_the_compatible_base_from_any_standard_base():
config = DashScopeEmbeddingConfig()
for api_base in (
WORKSPACE,
f"{WORKSPACE}/compatible-mode/v1",
f"{WORKSPACE}/apps/anthropic",
f"{WORKSPACE}/api/v1",
):
url = config.get_complete_url(
api_base=api_base,
api_key="sk-test",
model="text-embedding-v4",
optional_params={},
litellm_params={},
)
assert url == f"{WORKSPACE}/compatible-mode/v1/embeddings"
def test_get_complete_url_keeps_unrecognized_hosts():
url = DashScopeEmbeddingConfig().get_complete_url(
api_base="https://gateway.example.com/v1",
api_key="sk-test",
model="text-embedding-v4",
optional_params={},
litellm_params={},
)
assert url == "https://gateway.example.com/v1/embeddings"
def test_validate_environment_and_url():
config = DashScopeEmbeddingConfig()

View file

@ -0,0 +1,85 @@
"""
Unit tests for DashScope's Anthropic-compatible Messages endpoint.
"""
import pytest
import litellm
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"
@pytest.mark.parametrize(
("api_base", "expected"),
[
(
None,
"https://dashscope.aliyuncs.com/apps/anthropic/v1/messages",
),
(
"https://dashscope.aliyuncs.com",
"https://dashscope.aliyuncs.com/apps/anthropic/v1/messages",
),
(
"https://dashscope.aliyuncs.com/compatible-mode/v1",
"https://dashscope.aliyuncs.com/apps/anthropic/v1/messages",
),
(
"https://dashscope.aliyuncs.com/apps/anthropic",
"https://dashscope.aliyuncs.com/apps/anthropic/v1/messages",
),
(
"https://dashscope.aliyuncs.com/apps/anthropic/v1/messages",
"https://dashscope.aliyuncs.com/apps/anthropic/v1/messages",
),
(
WORKSPACE,
f"{WORKSPACE}/apps/anthropic/v1/messages",
),
(
f"{WORKSPACE}/compatible-mode/v1",
f"{WORKSPACE}/apps/anthropic/v1/messages",
),
(
"https://gateway.example.com/anthropic",
"https://gateway.example.com/anthropic/v1/messages",
),
],
)
def test_get_complete_url(api_base: str | None, expected: str):
config = DashScopeAnthropicMessagesConfig()
url = config.get_complete_url(
api_base=api_base,
api_key="sk-test",
model="qwen3.7-plus",
optional_params={},
litellm_params={},
)
assert url == expected
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",
api_base=WORKSPACE,
)
assert provider == "dashscope"
assert model == "qwen3.7-plus"

View file

@ -26,6 +26,13 @@ class TestDashScopeRerankURL:
url = self.config.get_complete_url(api_base=None, model="qwen3-rerank")
assert url == DEFAULT_RERANK_URL
def test_domain_root_remaps_to_rerank_route(self):
url = 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_chat_shaped_base_remaps_to_rerank_route(self):
url = self.config.get_complete_url(
api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",

View file

@ -1119,6 +1119,20 @@ 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"
for api_base in (
f"{workspace}/compatible-mode/v1",
f"{workspace}/apps/anthropic",
f"{workspace}/api/v1",
):
url = DashScopeVideoConfig().get_complete_url(
model="wan3.0-video", api_base=api_base, litellm_params={}
)
assert url == workspace
def test_default_host_is_beijing(self, monkeypatch):
monkeypatch.delenv("DASHSCOPE_API_BASE_VIDEO", raising=False)
monkeypatch.delenv("DASHSCOPE_API_BASE", raising=False)

View file

@ -12,14 +12,17 @@ import pytest
from pydantic import ValidationError
import litellm
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.dashscope.common_utils import (
DASHSCOPE_DEFAULT_ORIGIN,
DASHSCOPE_IMAGE_PATH,
)
from litellm.llms.dashscope.image_generation.transformation import (
DashScopeImageGenerationConfig,
DEFAULT_API_BASE,
DashScopeImageGenerationConfig,
)
from litellm.types.utils import ImageResponse
from litellm.utils import get_llm_provider
from litellm.llms.base_llm.chat.transformation import BaseLLMException
# ---------------------------------------------------------------------------
# 1. Provider detection
@ -65,17 +68,31 @@ class TestDashScopeImageGenerationConfig:
assert url == custom
@pytest.mark.parametrize(
"chat_api_base",
("api_base", "expected"),
[
"https://dashscope.aliyuncs.com/compatible-mode/v1",
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1/",
(
"https://dashscope.aliyuncs.com/compatible-mode/v1",
"https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
),
(
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1/",
DEFAULT_API_BASE,
),
(
"https://llm-abc123.cn-beijing.maas.aliyuncs.com",
"https://llm-abc123.cn-beijing.maas.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
),
(
"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",
),
],
)
def test_get_complete_url_ignores_chat_compatible_mode_base(
self, chat_api_base: str
def test_get_complete_url_derives_from_the_domain(
self, api_base: str, expected: str
):
url = self.cfg.get_complete_url(chat_api_base, None, "qwen-image-3.0", {}, {})
assert url == DEFAULT_API_BASE
url = self.cfg.get_complete_url(api_base, None, "qwen-image-3.0", {}, {})
assert url == expected
def test_validate_environment_sets_auth_header(self):
headers = self.cfg.validate_environment(
@ -447,7 +464,7 @@ def test_litellm_image_generation_dashscope_end_to_end(model: str):
called_url = (
call_args[0][0] if call_args[0] else call_args.kwargs.get("url", "")
)
assert called_url == DEFAULT_API_BASE
assert called_url == f"{DASHSCOPE_DEFAULT_ORIGIN}{DASHSCOPE_IMAGE_PATH}"
# Verify request body contains DashScope format
call_kwargs = call_args[1] if call_args[1] else {}