mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-30 06:43:27 +00:00
feat(dashscope)!: derive protocol bases from one domain (#47812)
* 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> * fix(dashscope): keep Anthropic credentials off DashScope messages Require a DashScope key or a caller auth header before the Messages path runs, and opt out of workload identity, so the base class cannot fall back to ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN Also format the image generation base selection * style(dashscope): keep the messages config within the format gate * test(dashscope): cover caller Authorization on messages * fix(dashscope): keep the chat bridge for protocol-named bases and the image default for unconfigured calls * 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 * fix(dashscope): keep an explicit compatible-api rerank base as given * fix(dashscope): derive chat from compatible-api bases, keep the caller's rerank base, and let explicit bases beat per-endpoint env vars * fix(dashscope): keep a full DashScope image endpoint URL as given and type the brand tests * fix(dashscope): let the image env var beat any shared DashScope base * fix(dashscope): keep a full DASHSCOPE_API_BASE image URL ahead of the image env var * fix(dashscope): keep a native DashScope endpoint URL as given for chat and the api base header --------- Co-authored-by: AaronHowell <111272993+AaronHowell@users.noreply.github.com> Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
parent
06cdee91da
commit
c98641fd5a
21 changed files with 1015 additions and 37 deletions
|
|
@ -68,6 +68,11 @@ from litellm.utils import (
|
|||
)
|
||||
|
||||
_PROVIDER_ROUTING_HINTS: Final = TypeAdapter(ProviderRoutingHints)
|
||||
_PROVIDERS_RESOLVING_THEIR_OWN_IMAGE_BASE: Final = (
|
||||
LlmProviders.DASHSCOPE,
|
||||
LlmProviders.QWENCLOUD,
|
||||
LlmProviders.QWEN_AI_PLATFORM,
|
||||
)
|
||||
|
||||
# Cache for ImageEditRequestUtils to avoid repeated __getattr__ calls
|
||||
_ImageEditRequestUtils_cache: type["ImageEditRequestUtils"] | None = None
|
||||
|
|
@ -271,6 +276,7 @@ def image_generation(
|
|||
"""
|
||||
try:
|
||||
args: Final = locals()
|
||||
requested_api_base: Final = api_base
|
||||
aimg_generation: Final = kwargs.get("aimg_generation", False)
|
||||
litellm_call_id: Final = kwargs.get("litellm_call_id", None)
|
||||
logger_fn: Final = kwargs.get("logger_fn", None)
|
||||
|
|
@ -468,7 +474,11 @@ def image_generation(
|
|||
raise ValueError(f"image generation config is not supported for {custom_llm_provider}")
|
||||
|
||||
# Resolve api_base from litellm.api_base if not explicitly provided
|
||||
_api_base: Final = api_base or litellm.api_base
|
||||
_api_base: Final = (
|
||||
requested_api_base
|
||||
if custom_llm_provider in _PROVIDERS_RESOLVING_THEIR_OWN_IMAGE_BASE
|
||||
else api_base or litellm.api_base
|
||||
)
|
||||
litellm_params_dict["api_base"] = _api_base
|
||||
|
||||
return llm_http_handler.image_generation_handler(
|
||||
|
|
|
|||
|
|
@ -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,10 +20,123 @@ 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:
|
||||
@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)
|
||||
if parsed.scheme not in ("http", "https") or not parsed.netloc:
|
||||
return None
|
||||
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 is_dashscope_native_endpoint_url(api_base: str) -> bool:
|
||||
return urlparse(api_base).path.rstrip("/").startswith(f"{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)
|
||||
if candidate and is_dashscope_native_endpoint_url(candidate):
|
||||
return candidate
|
||||
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 dashscope_base_selects_anthropic_messages(api_base: str | None) -> bool:
|
||||
candidate: Final = api_base or get_secret_str("DASHSCOPE_API_BASE")
|
||||
if not candidate:
|
||||
return False
|
||||
path: Final = urlparse(candidate).path.rstrip("/")
|
||||
if path.endswith(DASHSCOPE_ANTHROPIC_PATH) or f"{DASHSCOPE_ANTHROPIC_PATH}/" in path:
|
||||
return True
|
||||
return not path and resolve_dashscope_endpoints(candidate) is not None
|
||||
|
||||
|
||||
def _chat_shaped_aliyun_origin(api_base: str | None) -> str | None:
|
||||
if api_base is None:
|
||||
return None
|
||||
parsed: Final = urlparse(api_base)
|
||||
|
|
@ -30,14 +144,24 @@ def _rerank_base_for_chat_shaped_base(api_base: str | None) -> str | None:
|
|||
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:
|
||||
return None
|
||||
return f"{parsed.scheme}://{parsed.netloc}{DASHSCOPE_RERANK_PATH}"
|
||||
return f"{parsed.scheme}://{parsed.netloc}"
|
||||
|
||||
|
||||
def _is_compatible_api_base(api_base: str) -> bool:
|
||||
path: Final = urlparse(api_base).path.rstrip("/")
|
||||
return path == DASHSCOPE_COMPATIBLE_API_PATH or path.startswith(f"{DASHSCOPE_COMPATIBLE_API_PATH}/")
|
||||
|
||||
|
||||
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:
|
||||
if api_base is None:
|
||||
return get_secret_str(env_var) or default_rerank_base
|
||||
if _is_compatible_api_base(api_base):
|
||||
return api_base
|
||||
return get_secret_str(env_var) or remapped or default_rerank_base
|
||||
chat_origin: Final = _chat_shaped_aliyun_origin(api_base)
|
||||
if chat_origin is not None:
|
||||
return get_secret_str(env_var) or f"{chat_origin}{DASHSCOPE_RERANK_PATH}"
|
||||
endpoints: Final = resolve_dashscope_endpoints(api_base)
|
||||
return f"{endpoints.origin}{DASHSCOPE_RERANK_PATH}" if endpoints is not None else api_base
|
||||
|
||||
|
||||
def get_dashscope_family_embedding_config(custom_llm_provider: str) -> "BaseEmbeddingConfig":
|
||||
|
|
|
|||
|
|
@ -25,7 +25,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"
|
||||
|
||||
|
|
@ -91,7 +91,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,13 @@ from litellm.types.llms.openai import (
|
|||
)
|
||||
from litellm.types.utils import ImageObject, ImageResponse
|
||||
|
||||
from ..common_utils import (
|
||||
DASHSCOPE_CHAT_COMPATIBLE_PATH,
|
||||
DASHSCOPE_IMAGE_PATH,
|
||||
is_dashscope_native_endpoint_url,
|
||||
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
|
||||
|
|
@ -53,8 +60,6 @@ _DECODED_JSON: Final = TypeAdapter(object)
|
|||
|
||||
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",
|
||||
|
|
@ -102,8 +107,14 @@ class DashScopeImageGenerationConfig(BaseImageGenerationConfig):
|
|||
raise ValueError("DASHSCOPE_API_KEY is not set")
|
||||
return resolved_api_key
|
||||
|
||||
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 _image_api_base_env(self) -> str | None:
|
||||
return get_secret_str("DASHSCOPE_API_BASE_IMAGE")
|
||||
|
||||
def _default_image_api_base(self) -> str:
|
||||
return DEFAULT_API_BASE
|
||||
|
||||
def _configured_chat_api_base(self) -> str | None:
|
||||
return get_secret_str("DASHSCOPE_API_BASE")
|
||||
|
||||
def get_complete_url(
|
||||
self,
|
||||
|
|
@ -114,10 +125,18 @@ class DashScopeImageGenerationConfig(BaseImageGenerationConfig):
|
|||
litellm_params: dict,
|
||||
stream: bool | None = None,
|
||||
) -> str:
|
||||
image_api_base: Final = (
|
||||
api_base if api_base and not api_base.rstrip("/").endswith(CHAT_COMPATIBLE_MODE_PATH) else None
|
||||
)
|
||||
return self._resolve_image_api_base(image_api_base)
|
||||
image_env_api_base: Final = self._image_api_base_env()
|
||||
configured_api_base: Final = api_base or self._configured_chat_api_base()
|
||||
if not configured_api_base or configured_api_base.rstrip("/").endswith(DASHSCOPE_CHAT_COMPATIBLE_PATH):
|
||||
return image_env_api_base or self._default_image_api_base()
|
||||
if is_dashscope_native_endpoint_url(configured_api_base):
|
||||
return configured_api_base
|
||||
endpoints: Final = resolve_dashscope_endpoints(configured_api_base)
|
||||
if endpoints is None:
|
||||
return configured_api_base
|
||||
if not api_base and image_env_api_base:
|
||||
return image_env_api_base
|
||||
return f"{endpoints.origin}{DASHSCOPE_IMAGE_PATH}"
|
||||
|
||||
def validate_environment(
|
||||
self,
|
||||
|
|
|
|||
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"]
|
||||
96
litellm/llms/dashscope/messages/transformation.py
Normal file
96
litellm/llms/dashscope/messages/transformation.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
"""
|
||||
DashScope Anthropic-compatible Messages transformation config.
|
||||
"""
|
||||
|
||||
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.messages.native.transformation import (
|
||||
AnthropicMessagesConfig,
|
||||
)
|
||||
from litellm.secret_managers.main import get_secret_str, normalize_nonempty_secret_str
|
||||
|
||||
from ..common_utils import (
|
||||
dashscope_base_selects_anthropic_messages,
|
||||
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``.
|
||||
"""
|
||||
|
||||
_workload_identity_eligible: ClassVar[bool] = False
|
||||
|
||||
@property
|
||||
def custom_llm_provider(self) -> str | None:
|
||||
return "dashscope"
|
||||
|
||||
def should_strip_billing_metadata(self) -> bool:
|
||||
return True
|
||||
|
||||
def handles_web_search_natively(self) -> bool:
|
||||
return False
|
||||
|
||||
def serves_api_base(self, model: str, api_base: str | None) -> bool:
|
||||
return dashscope_base_selects_anthropic_messages(api_base)
|
||||
|
||||
@staticmethod
|
||||
def get_api_key(api_key: str | None = None) -> str | None:
|
||||
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:
|
||||
return resolve_dashscope_anthropic_api_base(api_base)
|
||||
|
||||
def validate_anthropic_messages_environment(
|
||||
self,
|
||||
headers: dict[str, str], # mutable-ok: mirrors the legacy base override signature
|
||||
model: str,
|
||||
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, str], str | None]: # mutable-ok: 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, reportReturnType] # base signature uses an unparameterized dict
|
||||
headers=headers,
|
||||
model=model,
|
||||
messages=messages,
|
||||
optional_params=optional_params,
|
||||
litellm_params=litellm_params,
|
||||
api_key=resolved_api_key,
|
||||
api_base=api_base,
|
||||
)
|
||||
|
||||
def get_complete_url(
|
||||
self,
|
||||
api_base: str | None,
|
||||
api_key: str | None,
|
||||
model: str,
|
||||
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")
|
||||
|
|
@ -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):
|
||||
|
|
@ -63,8 +63,14 @@ class QwenAIPlatformImageGenerationConfig(DashScopeImageGenerationConfig):
|
|||
def _resolve_api_key(self, api_key: str | None) -> str:
|
||||
return _require_qwen_ai_platform_api_key(api_key)
|
||||
|
||||
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 _image_api_base_env(self) -> str | None:
|
||||
return get_secret_str("QWEN_AI_PLATFORM_API_BASE_IMAGE")
|
||||
|
||||
def _default_image_api_base(self) -> str:
|
||||
return 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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
@ -63,8 +63,14 @@ class QwenCloudImageGenerationConfig(DashScopeImageGenerationConfig):
|
|||
def _resolve_api_key(self, api_key: str | None) -> str:
|
||||
return _require_qwencloud_api_key(api_key)
|
||||
|
||||
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 _image_api_base_env(self) -> str | None:
|
||||
return get_secret_str("QWENCLOUD_API_BASE_IMAGE")
|
||||
|
||||
def _default_image_api_base(self) -> str:
|
||||
return QWENCLOUD_IMAGE_API_BASE
|
||||
|
||||
def _configured_chat_api_base(self) -> str | None:
|
||||
return get_secret_str("QWENCLOUD_API_BASE")
|
||||
|
||||
|
||||
class QwenCloudVideoConfig(DashScopeVideoConfig):
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ from litellm.types.rerank import RerankResponse
|
|||
from litellm.types.router import *
|
||||
from litellm.utils import ProviderConfigManager, client, exception_type
|
||||
|
||||
_PROVIDERS_RESOLVING_THEIR_OWN_RERANK_BASE: Final = (
|
||||
litellm.LlmProviders.DASHSCOPE,
|
||||
litellm.LlmProviders.QWENCLOUD,
|
||||
litellm.LlmProviders.QWEN_AI_PLATFORM,
|
||||
)
|
||||
|
||||
####### ENVIRONMENT VARIABLES ###################
|
||||
# Initialize any necessary instances or variables here
|
||||
together_rerank: Final = TogetherAIRerank()
|
||||
|
|
@ -543,7 +549,11 @@ def rerank(
|
|||
logging_obj=litellm_logging_obj,
|
||||
timeout=optional_params.timeout,
|
||||
api_key=dynamic_api_key or optional_params.api_key,
|
||||
api_base=dynamic_api_base or optional_params.api_base,
|
||||
api_base=(
|
||||
optional_params.api_base or dynamic_api_base
|
||||
if _custom_llm_provider in _PROVIDERS_RESOLVING_THEIR_OWN_RERANK_BASE
|
||||
else dynamic_api_base or optional_params.api_base
|
||||
),
|
||||
_is_async=_is_async,
|
||||
headers=headers or litellm.headers or {},
|
||||
client=client,
|
||||
|
|
|
|||
|
|
@ -10048,6 +10048,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 (
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ class TestTryShortCircuitSearch:
|
|||
("github_copilot", "github_copilot/claude-sonnet-5.5"),
|
||||
("databricks", "databricks/databricks-claude-sonnet-5-5"),
|
||||
("databricks", "databricks/system.ai.claude-sonnet-5-5"),
|
||||
("dashscope", "dashscope/qwen3.7-plus"),
|
||||
],
|
||||
)
|
||||
async def test_short_circuits_single_web_search_tool(self, provider, model):
|
||||
|
|
|
|||
|
|
@ -5,15 +5,50 @@ 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: Final = "https://llm-abc123.cn-beijing.maas.aliyuncs.com"
|
||||
|
||||
|
||||
def test_get_complete_url_derives_the_compatible_base_from_any_standard_base():
|
||||
config: Final = DashScopeChatConfig()
|
||||
|
||||
for api_base in (
|
||||
WORKSPACE,
|
||||
f"{WORKSPACE}/compatible-mode/v1",
|
||||
f"{WORKSPACE}/apps/anthropic",
|
||||
f"{WORKSPACE}/api/v1",
|
||||
):
|
||||
url: Final = 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: Final = 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"""
|
||||
|
|
|
|||
171
tests/unit/llms/dashscope/test_dashscope_common_utils.py
Normal file
171
tests/unit/llms/dashscope/test_dashscope_common_utils.py
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
"""
|
||||
Unit tests for DashScope's protocol-specific base URL derivation.
|
||||
"""
|
||||
|
||||
from typing import Final
|
||||
|
||||
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: Final = "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: Final = 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: Final = 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"
|
||||
assert (
|
||||
resolve_dashscope_openai_api_base(f"{WORKSPACE}/compatible-api/v1/reranks") == f"{WORKSPACE}/compatible-mode/v1"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"endpoint_url",
|
||||
[
|
||||
"https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
|
||||
f"{WORKSPACE}/api/v1/services/aigc/video-generation/video-synthesis",
|
||||
],
|
||||
)
|
||||
def test_openai_base_keeps_a_native_endpoint_url(monkeypatch: pytest.MonkeyPatch, endpoint_url: str):
|
||||
monkeypatch.setenv("DASHSCOPE_API_BASE", endpoint_url)
|
||||
|
||||
assert resolve_dashscope_openai_api_base(None) == endpoint_url
|
||||
assert resolve_dashscope_openai_api_base(endpoint_url) == endpoint_url
|
||||
assert resolve_dashscope_openai_api_base(f"{WORKSPACE}/api/v1") == 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: Final = "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"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"origin",
|
||||
["https://cn-hongkong.dashscope.aliyuncs.com", "https://token-plan.cn-beijing.maas.aliyuncs.com"],
|
||||
)
|
||||
def test_rerank_keeps_remapping_chat_bases_on_other_aliyun_hosts(monkeypatch: pytest.MonkeyPatch, origin: str):
|
||||
monkeypatch.delenv("DASHSCOPE_API_BASE_RERANK", raising=False)
|
||||
|
||||
assert (
|
||||
resolve_dashscope_family_rerank_api_base(
|
||||
f"{origin}/compatible-mode/v1", "DASHSCOPE_API_BASE_RERANK", "https://default/reranks"
|
||||
)
|
||||
== f"{origin}/compatible-api/v1/reranks"
|
||||
)
|
||||
|
|
@ -3,6 +3,7 @@ Unit tests for DashScope embedding transformation.
|
|||
"""
|
||||
|
||||
import json
|
||||
from typing import Final
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import httpx
|
||||
|
|
@ -16,6 +17,40 @@ from litellm.llms.dashscope.embed.transformation import (
|
|||
)
|
||||
from litellm.types.utils import EmbeddingResponse
|
||||
|
||||
WORKSPACE: Final = "https://llm-abc123.cn-beijing.maas.aliyuncs.com"
|
||||
|
||||
|
||||
def test_get_complete_url_derives_the_compatible_base_from_any_standard_base():
|
||||
config: Final = DashScopeEmbeddingConfig()
|
||||
|
||||
for api_base in (
|
||||
WORKSPACE,
|
||||
f"{WORKSPACE}/compatible-mode/v1",
|
||||
f"{WORKSPACE}/apps/anthropic",
|
||||
f"{WORKSPACE}/api/v1",
|
||||
):
|
||||
url: Final = 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: Final = 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,215 @@
|
|||
"""
|
||||
Unit tests for DashScope's Anthropic-compatible Messages endpoint.
|
||||
"""
|
||||
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
import litellm
|
||||
from litellm.exceptions import AuthenticationError
|
||||
from litellm.llms.dashscope.messages.transformation import (
|
||||
DashScopeAnthropicMessagesConfig,
|
||||
)
|
||||
|
||||
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 = {
|
||||
"id": "chatcmpl-1",
|
||||
"object": "chat.completion",
|
||||
"created": 1792227137,
|
||||
"model": "qwen3.7-plus",
|
||||
"choices": [{"index": 0, "message": {"role": "assistant", "content": "ok"}, "finish_reason": "stop"}],
|
||||
"usage": {"prompt_tokens": 9, "completion_tokens": 1, "total_tokens": 10},
|
||||
}
|
||||
MESSAGE: Final = {
|
||||
"id": "msg_1",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"model": "qwen3.7-plus",
|
||||
"content": [{"type": "text", "text": "ok"}],
|
||||
"stop_reason": "end_turn",
|
||||
"stop_sequence": None,
|
||||
"usage": {"input_tokens": 9, "output_tokens": 1},
|
||||
}
|
||||
|
||||
|
||||
@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: Final = DashScopeAnthropicMessagesConfig()
|
||||
|
||||
url: Final = config.get_complete_url(
|
||||
api_base=api_base,
|
||||
api_key="sk-test",
|
||||
model="qwen3.7-plus",
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
)
|
||||
|
||||
assert url == expected
|
||||
|
||||
|
||||
@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)
|
||||
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: Final = {"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_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: Final = {"Authorization": "Bearer caller-token"}
|
||||
|
||||
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["Authorization"] == "Bearer caller-token"
|
||||
|
||||
|
||||
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: Final = " ".join(resolved_headers.values())
|
||||
assert "sk-dashscope" in header_values
|
||||
assert "sk-ant-secret" not in header_values
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("api_base", "expected_url"),
|
||||
[
|
||||
(INTL, f"{INTL}/apps/anthropic/v1/messages"),
|
||||
(f"{INTL}/apps/anthropic", f"{INTL}/apps/anthropic/v1/messages"),
|
||||
(WORKSPACE, f"{WORKSPACE}/apps/anthropic/v1/messages"),
|
||||
(f"{CODING_PLAN}/apps/anthropic", f"{CODING_PLAN}/apps/anthropic/v1/messages"),
|
||||
(None, "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"),
|
||||
(f"{INTL}/compatible-mode/v1", f"{INTL}/compatible-mode/v1/chat/completions"),
|
||||
(f"{CODING_PLAN}/v1", f"{CODING_PLAN}/v1/chat/completions"),
|
||||
("https://gateway.example.com/anthropic", "https://gateway.example.com/anthropic/chat/completions"),
|
||||
],
|
||||
)
|
||||
async def test_only_a_domain_root_or_anthropic_base_leaves_the_chat_bridge(
|
||||
monkeypatch: pytest.MonkeyPatch, api_base: str | None, expected_url: str
|
||||
):
|
||||
monkeypatch.delenv("DASHSCOPE_API_BASE", raising=False)
|
||||
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||
|
||||
with respx.mock(assert_all_called=False) as upstream:
|
||||
upstream.route(path__regex=r".*/chat/completions$").respond(200, json=CHAT_COMPLETION)
|
||||
upstream.route(path__regex=r".*/v1/messages$").respond(200, json=MESSAGE)
|
||||
|
||||
await litellm.anthropic.messages.acreate(
|
||||
model="dashscope/qwen3.7-plus",
|
||||
messages=[{"role": "user", "content": "say ok"}],
|
||||
max_tokens=16,
|
||||
api_key="sk-dashscope",
|
||||
api_base=api_base,
|
||||
)
|
||||
|
||||
assert [str(call.request.url) for call in upstream.calls] == [expected_url]
|
||||
|
|
@ -3,12 +3,15 @@ Unit tests for DashScope rerank transformation.
|
|||
"""
|
||||
|
||||
import json
|
||||
from typing import Final
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
from pydantic import ValidationError
|
||||
|
||||
import litellm
|
||||
|
||||
from litellm.llms.dashscope.common_utils import DashScopeError
|
||||
from litellm.llms.dashscope.rerank.transformation import (
|
||||
|
|
@ -26,6 +29,21 @@ 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: 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",
|
||||
|
|
@ -434,3 +452,37 @@ def test_transform_rerank_response_shape_errors_do_not_echo_the_payload():
|
|||
_transform({"results": [["leaked document text"]]})
|
||||
|
||||
assert "leaked document text" not in str(exc_info.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("provider", "rerank_env"),
|
||||
[
|
||||
("dashscope", "DASHSCOPE_API_BASE_RERANK"),
|
||||
("qwencloud", "QWENCLOUD_API_BASE_RERANK"),
|
||||
("qwen_ai_platform", "QWEN_AI_PLATFORM_API_BASE_RERANK"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"api_base",
|
||||
[
|
||||
"https://dashscope-intl.aliyuncs.com/compatible-api/v1/reranks",
|
||||
"https://dashscope-intl.aliyuncs.com/compatible-api/v1",
|
||||
"https://dashscope-intl.aliyuncs.com",
|
||||
"https://dashscope-intl.aliyuncs.com/api/v1",
|
||||
],
|
||||
)
|
||||
def test_rerank_sends_an_explicit_base_over_the_rerank_env(
|
||||
monkeypatch: pytest.MonkeyPatch, provider: str, rerank_env: str, api_base: str
|
||||
):
|
||||
monkeypatch.setenv("DASHSCOPE_API_BASE", "https://dashscope-intl.aliyuncs.com")
|
||||
monkeypatch.setenv(rerank_env, DEFAULT_RERANK_URL)
|
||||
|
||||
with respx.mock(assert_all_called=True) as upstream:
|
||||
upstream.post("https://dashscope-intl.aliyuncs.com/compatible-api/v1/reranks").respond(
|
||||
200, json={"id": "r1", "results": [{"index": 0, "relevance_score": 0.9}], "usage": {"total_tokens": 5}}
|
||||
)
|
||||
response: Final = litellm.rerank(
|
||||
model=f"{provider}/qwen3-rerank", query="q", documents=["a"], api_base=api_base, api_key="sk-test"
|
||||
)
|
||||
|
||||
assert response.results == [{"index": 0, "relevance_score": 0.9}]
|
||||
|
|
|
|||
|
|
@ -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: 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: Final = 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)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ import json
|
|||
import math
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Final, Literal
|
||||
|
||||
import pytest
|
||||
from typing_extensions import ReadOnly, TypedDict
|
||||
|
||||
import litellm
|
||||
from litellm import completion, get_llm_provider
|
||||
|
|
@ -53,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(
|
||||
{
|
||||
|
|
@ -211,6 +230,20 @@ 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: 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",
|
||||
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 +282,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: 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(
|
||||
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"):
|
||||
|
|
|
|||
|
|
@ -5,21 +5,26 @@ 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
|
||||
import pytest
|
||||
import respx
|
||||
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
|
||||
|
|
@ -77,6 +82,47 @@ class TestDashScopeImageGenerationConfig:
|
|||
url = self.cfg.get_complete_url(chat_api_base, None, "qwen-image-3.0", {}, {})
|
||||
assert url == DEFAULT_API_BASE
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("api_base", "expected"),
|
||||
[
|
||||
(
|
||||
"https://dashscope.aliyuncs.com",
|
||||
"https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
|
||||
),
|
||||
(
|
||||
"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",
|
||||
),
|
||||
(
|
||||
"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(
|
||||
self, api_base: str, expected: str
|
||||
):
|
||||
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={},
|
||||
|
|
@ -458,6 +504,81 @@ def test_litellm_image_generation_dashscope_end_to_end(model: str):
|
|||
assert body["parameters"]["size"] == "1024*1024"
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("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"),
|
||||
(
|
||||
"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", "https://images.example.com/generate", "https://images.example.com/generate"),
|
||||
(
|
||||
None,
|
||||
f"https://dashscope-us.aliyuncs.com{DASHSCOPE_IMAGE_PATH}",
|
||||
"https://images.example.com/generate",
|
||||
f"https://dashscope-us.aliyuncs.com{DASHSCOPE_IMAGE_PATH}",
|
||||
),
|
||||
(
|
||||
None,
|
||||
"https://gateway.example.com/dashscope/images",
|
||||
"https://images.example.com/generate",
|
||||
"https://gateway.example.com/dashscope/images",
|
||||
),
|
||||
(
|
||||
None,
|
||||
"https://dashscope-us.aliyuncs.com/compatible-mode/v1",
|
||||
"https://images.example.com/generate",
|
||||
"https://images.example.com/generate",
|
||||
),
|
||||
(
|
||||
"https://dashscope-us.aliyuncs.com",
|
||||
"",
|
||||
"https://images.example.com/generate",
|
||||
f"https://dashscope-us.aliyuncs.com{DASHSCOPE_IMAGE_PATH}",
|
||||
),
|
||||
(
|
||||
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_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.setenv("DASHSCOPE_API_BASE", chat_base_env)
|
||||
monkeypatch.setenv("DASHSCOPE_API_BASE_IMAGE", image_base_env)
|
||||
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: Final = litellm.image_generation(
|
||||
model="dashscope/qwen-image-3.0", prompt="a puppy", api_key="sk-test-key", api_base=api_base
|
||||
)
|
||||
|
||||
assert response.data[0].url == image_url
|
||||
|
||||
|
||||
def _transform_response(payload: object) -> ImageResponse:
|
||||
return DashScopeImageGenerationConfig().transform_image_generation_response(
|
||||
model="qwen-image-2.0",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue