mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
feat(dashscope): add native Responses API support
DashScope (Alibaba Cloud) provides an OpenAI-compatible `/v1/responses` endpoint. This adds a `DashScopeResponsesAPIConfig` so that LiteLLM can route Responses API calls to DashScope natively, enabling `previous_response_id` sticky routing and server-side context caching. Changes: - New `litellm/llms/dashscope/responses/transformation.py` - Register in `ProviderConfigManager._get_python_responses_api_config()` - Add lazy import entry in `_lazy_imports_registry.py` - Unit tests covering registration, URL construction, auth, and params
This commit is contained in:
parent
e3d4c29d37
commit
d6ba114aff
6 changed files with 328 additions and 0 deletions
|
|
@ -299,6 +299,7 @@ LLM_CONFIG_NAMES = (
|
|||
"GigaChatConfig",
|
||||
"GigaChatEmbeddingConfig",
|
||||
"DashScopeChatConfig",
|
||||
"DashScopeResponsesAPIConfig",
|
||||
"MoonshotChatConfig",
|
||||
"DockerModelRunnerChatConfig",
|
||||
"V0ChatConfig",
|
||||
|
|
@ -1125,6 +1126,10 @@ _LLM_CONFIGS_IMPORT_MAP = {
|
|||
".llms.dashscope.chat.transformation",
|
||||
"DashScopeChatConfig",
|
||||
),
|
||||
"DashScopeResponsesAPIConfig": (
|
||||
".llms.dashscope.responses.transformation",
|
||||
"DashScopeResponsesAPIConfig",
|
||||
),
|
||||
"MoonshotChatConfig": (".llms.moonshot.chat.transformation", "MoonshotChatConfig"),
|
||||
"DockerModelRunnerChatConfig": (
|
||||
".llms.docker_model_runner.chat.transformation",
|
||||
|
|
|
|||
0
litellm/llms/dashscope/responses/__init__.py
Normal file
0
litellm/llms/dashscope/responses/__init__.py
Normal file
147
litellm/llms/dashscope/responses/transformation.py
Normal file
147
litellm/llms/dashscope/responses/transformation.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
"""
|
||||
Translates from OpenAI's `/v1/responses` to DashScope's `/compatible-mode/v1/responses`
|
||||
|
||||
DashScope (Alibaba Cloud) provides an OpenAI-compatible endpoint, so most of
|
||||
the heavy lifting is delegated to ``OpenAIResponsesAPIConfig``. This subclass
|
||||
only overrides provider identification, authentication, URL construction, and
|
||||
the supported-parameter whitelist.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
import httpx
|
||||
|
||||
import litellm
|
||||
from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfig
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
from litellm.types.utils import LlmProviders
|
||||
|
||||
_DEFAULT_API_BASE = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
|
||||
_SUPPORTED_OPTIONAL_PARAMS: List[str] = [
|
||||
"instructions",
|
||||
"max_output_tokens",
|
||||
"metadata",
|
||||
"previous_response_id",
|
||||
"reasoning",
|
||||
"store",
|
||||
"stream",
|
||||
"temperature",
|
||||
"text",
|
||||
"tools",
|
||||
"tool_choice",
|
||||
"top_p",
|
||||
# LiteLLM request plumbing helpers
|
||||
"extra_headers",
|
||||
"extra_query",
|
||||
"extra_body",
|
||||
"timeout",
|
||||
]
|
||||
|
||||
|
||||
class DashScopeResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
||||
"""Responses API configuration for DashScope (Alibaba Cloud)."""
|
||||
|
||||
@property
|
||||
def custom_llm_provider(self) -> LlmProviders:
|
||||
return LlmProviders.DASHSCOPE
|
||||
|
||||
def get_supported_openai_params(self, model: str) -> list:
|
||||
"""Return the parameter whitelist for DashScope Responses API."""
|
||||
supported = ["input", "model"] + list(_SUPPORTED_OPTIONAL_PARAMS)
|
||||
# metadata is LiteLLM-internal; advertise all others
|
||||
if "metadata" in supported:
|
||||
supported.remove("metadata")
|
||||
return supported
|
||||
|
||||
def map_openai_params(
|
||||
self,
|
||||
response_api_optional_params: ResponsesAPIOptionalRequestParams,
|
||||
model: str,
|
||||
drop_params: bool,
|
||||
) -> Dict:
|
||||
"""Filter parameters to the DashScope-supported set."""
|
||||
params = {
|
||||
key: value
|
||||
for key, value in dict(response_api_optional_params).items()
|
||||
if key in _SUPPORTED_OPTIONAL_PARAMS
|
||||
}
|
||||
# LiteLLM metadata is internal-only; don't send to provider
|
||||
params.pop("metadata", None)
|
||||
return params
|
||||
|
||||
def validate_environment(
|
||||
self,
|
||||
headers: dict,
|
||||
model: str,
|
||||
litellm_params: Optional[GenericLiteLLMParams],
|
||||
) -> dict:
|
||||
"""Build auth headers for DashScope Responses API."""
|
||||
if litellm_params is None:
|
||||
litellm_params = GenericLiteLLMParams()
|
||||
elif isinstance(litellm_params, dict):
|
||||
litellm_params = GenericLiteLLMParams(**litellm_params)
|
||||
|
||||
api_key = (
|
||||
litellm_params.api_key
|
||||
or litellm.api_key
|
||||
or get_secret_str("DASHSCOPE_API_KEY")
|
||||
)
|
||||
|
||||
if api_key is None:
|
||||
raise ValueError(
|
||||
"DashScope API key is required. "
|
||||
"Set DASHSCOPE_API_KEY or pass api_key."
|
||||
)
|
||||
|
||||
result_headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
if headers:
|
||||
result_headers.update(headers)
|
||||
return result_headers
|
||||
|
||||
def get_complete_url(
|
||||
self,
|
||||
api_base: Optional[str],
|
||||
litellm_params: dict,
|
||||
) -> str:
|
||||
"""Construct DashScope Responses API endpoint."""
|
||||
base_url = (
|
||||
api_base
|
||||
or litellm.api_base
|
||||
or get_secret_str("DASHSCOPE_API_BASE")
|
||||
or _DEFAULT_API_BASE
|
||||
)
|
||||
|
||||
base_url = base_url.rstrip("/")
|
||||
|
||||
if base_url.endswith("/responses"):
|
||||
return base_url
|
||||
if base_url.endswith("/v1"):
|
||||
return f"{base_url}/responses"
|
||||
if base_url.endswith("/compatible-mode/v1"):
|
||||
return f"{base_url}/responses"
|
||||
return f"{base_url}/compatible-mode/v1/responses"
|
||||
|
||||
def get_error_class(
|
||||
self,
|
||||
error_message: str,
|
||||
status_code: int,
|
||||
headers: Union[dict, httpx.Headers],
|
||||
) -> Exception:
|
||||
from litellm.llms.openai.common_utils import OpenAIError
|
||||
|
||||
typed_headers: httpx.Headers = (
|
||||
headers
|
||||
if isinstance(headers, httpx.Headers)
|
||||
else httpx.Headers(headers or {})
|
||||
)
|
||||
return OpenAIError(
|
||||
status_code=status_code,
|
||||
message=error_message,
|
||||
headers=typed_headers,
|
||||
)
|
||||
|
|
@ -8490,6 +8490,8 @@ class ProviderConfigManager:
|
|||
return litellm.ChatGPTResponsesAPIConfig()
|
||||
elif litellm.LlmProviders.LITELLM_PROXY == provider:
|
||||
return litellm.LiteLLMProxyResponsesAPIConfig()
|
||||
elif litellm.LlmProviders.DASHSCOPE == provider:
|
||||
return litellm.DashScopeResponsesAPIConfig()
|
||||
elif litellm.LlmProviders.VOLCENGINE == provider:
|
||||
return litellm.VolcEngineResponsesAPIConfig()
|
||||
elif litellm.LlmProviders.MANUS == provider:
|
||||
|
|
|
|||
0
tests/test_litellm/llms/dashscope/responses/__init__.py
Normal file
0
tests/test_litellm/llms/dashscope/responses/__init__.py
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
"""
|
||||
Tests for DashScope Responses API transformation.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../../../../.."))
|
||||
|
||||
import litellm
|
||||
from litellm.llms.dashscope.responses.transformation import (
|
||||
DashScopeResponsesAPIConfig,
|
||||
)
|
||||
from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
from litellm.types.utils import LlmProviders
|
||||
from litellm.utils import ProviderConfigManager
|
||||
|
||||
|
||||
class TestDashScopeResponsesAPITransformation:
|
||||
"""Test DashScope Responses API configuration and transformations."""
|
||||
|
||||
def test_provider_config_registration(self):
|
||||
"""Provider registry should return DashScopeResponsesAPIConfig."""
|
||||
config = ProviderConfigManager.get_provider_responses_api_config(
|
||||
model="dashscope/qwen-plus",
|
||||
provider=LlmProviders.DASHSCOPE,
|
||||
)
|
||||
|
||||
assert config is not None, "Config should not be None for DashScope provider"
|
||||
assert isinstance(
|
||||
config, DashScopeResponsesAPIConfig
|
||||
), f"Expected DashScopeResponsesAPIConfig, got {type(config)}"
|
||||
assert (
|
||||
config.custom_llm_provider == LlmProviders.DASHSCOPE
|
||||
), "custom_llm_provider should be DASHSCOPE"
|
||||
|
||||
def test_get_complete_url_default(self):
|
||||
"""Default URL should point to DashScope compatible-mode endpoint."""
|
||||
config = DashScopeResponsesAPIConfig()
|
||||
url = config.get_complete_url(api_base=None, litellm_params={})
|
||||
assert url == "https://dashscope.aliyuncs.com/compatible-mode/v1/responses"
|
||||
|
||||
def test_get_complete_url_custom_base(self):
|
||||
"""Custom api_base should be respected."""
|
||||
config = DashScopeResponsesAPIConfig()
|
||||
|
||||
# Base ending with /v1
|
||||
url = config.get_complete_url(
|
||||
api_base="https://custom.example.com/v1", litellm_params={}
|
||||
)
|
||||
assert url == "https://custom.example.com/v1/responses"
|
||||
|
||||
# Base already ending with /responses
|
||||
url = config.get_complete_url(
|
||||
api_base="https://custom.example.com/v1/responses", litellm_params={}
|
||||
)
|
||||
assert url == "https://custom.example.com/v1/responses"
|
||||
|
||||
# Bare domain
|
||||
url = config.get_complete_url(
|
||||
api_base="https://custom.example.com", litellm_params={}
|
||||
)
|
||||
assert url == "https://custom.example.com/compatible-mode/v1/responses"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"litellm_params, expected_key",
|
||||
[
|
||||
({"api_key": "dict-key"}, "dict-key"),
|
||||
(GenericLiteLLMParams(api_key="attr-key"), "attr-key"),
|
||||
],
|
||||
)
|
||||
def test_validate_environment_uses_api_key(
|
||||
self, monkeypatch, litellm_params, expected_key
|
||||
):
|
||||
"""validate_environment should pull api key from params/env and attach headers."""
|
||||
config = DashScopeResponsesAPIConfig()
|
||||
|
||||
monkeypatch.setattr(litellm, "api_key", None)
|
||||
monkeypatch.delenv("DASHSCOPE_API_KEY", raising=False)
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={}, model="dashscope/qwen-plus", litellm_params=litellm_params
|
||||
)
|
||||
|
||||
assert headers.get("Authorization") == f"Bearer {expected_key}"
|
||||
assert headers.get("Content-Type") == "application/json"
|
||||
|
||||
def test_validate_environment_from_env(self, monkeypatch):
|
||||
"""validate_environment should fall back to DASHSCOPE_API_KEY env var."""
|
||||
config = DashScopeResponsesAPIConfig()
|
||||
|
||||
monkeypatch.setattr(litellm, "api_key", None)
|
||||
monkeypatch.setenv("DASHSCOPE_API_KEY", "env-key")
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={}, model="dashscope/qwen-plus", litellm_params={}
|
||||
)
|
||||
|
||||
assert headers.get("Authorization") == "Bearer env-key"
|
||||
|
||||
def test_validate_environment_raises_without_key(self, monkeypatch):
|
||||
"""validate_environment should error when no key is available."""
|
||||
config = DashScopeResponsesAPIConfig()
|
||||
|
||||
monkeypatch.setattr(litellm, "api_key", None)
|
||||
monkeypatch.delenv("DASHSCOPE_API_KEY", raising=False)
|
||||
|
||||
with pytest.raises(ValueError, match="DashScope API key is required"):
|
||||
config.validate_environment(
|
||||
headers={}, model="dashscope/qwen-plus", litellm_params={}
|
||||
)
|
||||
|
||||
def test_supported_params(self):
|
||||
"""Supported params should match documented DashScope surface."""
|
||||
config = DashScopeResponsesAPIConfig()
|
||||
supported = set(config.get_supported_openai_params("dashscope/qwen-plus"))
|
||||
|
||||
expected = {
|
||||
"input",
|
||||
"model",
|
||||
"instructions",
|
||||
"max_output_tokens",
|
||||
"previous_response_id",
|
||||
"reasoning",
|
||||
"store",
|
||||
"stream",
|
||||
"temperature",
|
||||
"text",
|
||||
"tools",
|
||||
"tool_choice",
|
||||
"top_p",
|
||||
"extra_headers",
|
||||
"extra_query",
|
||||
"extra_body",
|
||||
"timeout",
|
||||
}
|
||||
|
||||
assert supported == expected
|
||||
|
||||
def test_map_openai_params_filters_unsupported(self):
|
||||
"""map_openai_params should drop unsupported and metadata params."""
|
||||
config = DashScopeResponsesAPIConfig()
|
||||
params = ResponsesAPIOptionalRequestParams(
|
||||
temperature=0.7,
|
||||
metadata={"k": "v"},
|
||||
)
|
||||
|
||||
mapped = config.map_openai_params(
|
||||
response_api_optional_params=params,
|
||||
model="dashscope/qwen-plus",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert mapped.get("temperature") == 0.7
|
||||
assert "metadata" not in mapped
|
||||
|
||||
def test_extra_headers_merged(self, monkeypatch):
|
||||
"""Extra headers passed to validate_environment should be merged."""
|
||||
config = DashScopeResponsesAPIConfig()
|
||||
|
||||
monkeypatch.setattr(litellm, "api_key", None)
|
||||
monkeypatch.setenv("DASHSCOPE_API_KEY", "test-key")
|
||||
|
||||
headers = config.validate_environment(
|
||||
headers={"X-Custom": "value"},
|
||||
model="dashscope/qwen-plus",
|
||||
litellm_params={},
|
||||
)
|
||||
|
||||
assert headers.get("X-Custom") == "value"
|
||||
assert headers.get("Authorization") == "Bearer test-key"
|
||||
Loading…
Add table
Reference in a new issue