mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
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:
parent
66c8f5b1a9
commit
d260909f04
15 changed files with 542 additions and 31 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
7
litellm/llms/dashscope/messages/__init__.py
Normal file
7
litellm/llms/dashscope/messages/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
DashScope Anthropic-compatible Messages Module
|
||||
"""
|
||||
|
||||
from .transformation import DashScopeAnthropicMessagesConfig
|
||||
|
||||
__all__ = ["DashScopeAnthropicMessagesConfig"]
|
||||
68
litellm/llms/dashscope/messages/transformation.py
Normal file
68
litellm/llms/dashscope/messages/transformation.py
Normal 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")
|
||||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
136
tests/unit/llms/dashscope/test_dashscope_common_utils.py
Normal file
136
tests/unit/llms/dashscope/test_dashscope_common_utils.py
Normal 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"
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue