This commit is contained in:
songkuan-zheng 2026-09-13 00:12:04 +08:00 committed by GitHub
commit 98292f7bf0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 190 additions and 2 deletions

View file

@ -2,14 +2,45 @@
Translates from OpenAI's `/v1/chat/completions` to DashScope's `/v1/chat/completions`
"""
from collections.abc import Coroutine
from collections.abc import Coroutine, Mapping
from types import MappingProxyType
from typing import Any, Final, Literal, overload
from typing_extensions import ReadOnly, TypedDict
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import AllMessageValues, ChatCompletionToolParam
from ...openai.chat.gpt_transformation import OpenAIGPTConfig
THINKING_OFF_REASONING_EFFORTS: Final = frozenset({"none", "disable"})
DASHSCOPE_THINKING_PARAMS: Final = frozenset({"thinking", "reasoning_effort"})
class DashScopeThinkingBody(TypedDict, total=False):
enable_thinking: ReadOnly[bool]
thinking_budget: ReadOnly[int]
reasoning_effort: ReadOnly[str]
def _dashscope_thinking_body(thinking: object, reasoning_effort: object) -> DashScopeThinkingBody:
if isinstance(thinking, dict):
enabled: Final = thinking.get("type") != "disabled"
budget: Final = thinking.get("budget_tokens")
if enabled and isinstance(budget, int) and not isinstance(budget, bool):
with_budget: Final[DashScopeThinkingBody] = {"enable_thinking": enabled, "thinking_budget": budget}
return with_budget
toggled: Final[DashScopeThinkingBody] = {"enable_thinking": enabled}
return toggled
if isinstance(reasoning_effort, str):
if reasoning_effort in THINKING_OFF_REASONING_EFFORTS:
off: Final[DashScopeThinkingBody] = {"enable_thinking": False}
return off
with_effort: Final[DashScopeThinkingBody] = {"enable_thinking": True, "reasoning_effort": reasoning_effort}
return with_effort
untouched: Final[DashScopeThinkingBody] = {}
return untouched
class DashScopeChatConfig(OpenAIGPTConfig):
def remove_cache_control_flag_from_messages_and_tools(
@ -73,3 +104,35 @@ class DashScopeChatConfig(OpenAIGPTConfig):
if resolved_api_base.endswith("/chat/completions"):
return resolved_api_base
return f"{resolved_api_base}/chat/completions"
def get_supported_openai_params(self, model: str) -> list:
base_params: Final = super().get_supported_openai_params(model)
return [*base_params, "thinking", "reasoning_effort"] # mutable-ok: inherited list contract
def _map_openai_params(
self,
non_default_params: dict[str, object],
optional_params: dict[str, object],
model: str,
drop_params: bool,
) -> dict[str, object]:
supported_openai_params: Final = frozenset(self.get_supported_openai_params(model))
passthrough_params: Final = MappingProxyType(
{
k: v
for k, v in non_default_params.items()
if k in supported_openai_params and k not in DASHSCOPE_THINKING_PARAMS
}
)
native: Final = _dashscope_thinking_body(
non_default_params.get("thinking"), non_default_params.get("reasoning_effort")
)
if not native:
return {**optional_params, **passthrough_params} # mutable-ok: dict return contract of OpenAIGPTConfig
existing: Final = optional_params.get("extra_body")
existing_body: Final = existing if isinstance(existing, Mapping) else MappingProxyType({})
return { # mutable-ok: dict return contract of OpenAIGPTConfig
**optional_params,
**passthrough_params,
"extra_body": {**existing_body, **native}, # mutable-ok: the OpenAI SDK json-encodes extra_body from a dict
}

View file

@ -7,12 +7,14 @@ DashScope is an OpenAI-compatible provider with minor customizations.
from litellm.types.llms.openai import AllMessageValues
import json
import pytest
import litellm
from litellm import completion
from litellm.llms.dashscope.chat.transformation import DashScopeChatConfig
from litellm.types.llms.openai import AllMessageValues
class TestDashScopeConfig:
@ -185,3 +187,126 @@ class TestDashScopeConfig:
)
assert transformed_tools[0].get("cache_control") == {"type": "ephemeral"}
class TestDashScopeThinkingParams:
"""thinking and reasoning_effort reach DashScope as enable_thinking, thinking_budget and reasoning_effort."""
@staticmethod
def _map(**non_default_params: object) -> dict[str, object]:
return DashScopeChatConfig().map_openai_params(
non_default_params=non_default_params,
optional_params={},
model="qwen3.8-max",
drop_params=False,
)
@pytest.mark.parametrize(
("thinking", "expected"),
[
({"type": "enabled", "budget_tokens": 4096}, {"enable_thinking": True, "thinking_budget": 4096}),
({"type": "enabled"}, {"enable_thinking": True}),
({"type": "disabled"}, {"enable_thinking": False}),
({"type": "disabled", "budget_tokens": 4096}, {"enable_thinking": False}),
],
)
def test_thinking_maps_to_enable_thinking_and_budget(self, thinking, expected):
assert self._map(thinking=thinking)["extra_body"] == expected
@pytest.mark.parametrize("effort", ["low", "medium", "high", "minimal"])
def test_reasoning_effort_enables_thinking_and_is_forwarded(self, effort):
params = self._map(reasoning_effort=effort)
assert params["extra_body"] == {"enable_thinking": True, "reasoning_effort": effort}
@pytest.mark.parametrize("effort", ["none", "disable"])
def test_reasoning_effort_off_disables_thinking(self, effort):
assert self._map(reasoning_effort=effort)["extra_body"] == {"enable_thinking": False}
def test_thinking_budget_wins_over_reasoning_effort(self):
params = self._map(thinking={"type": "enabled", "budget_tokens": 512}, reasoning_effort="high")
assert params["extra_body"] == {"enable_thinking": True, "thinking_budget": 512}
def test_no_thinking_params_leaves_extra_body_absent(self):
params = self._map(temperature=0.5, max_tokens=16)
assert "extra_body" not in params
assert params["temperature"] == 0.5
assert params["max_tokens"] == 16
def test_thinking_params_never_become_top_level_kwargs(self):
params = self._map(thinking={"type": "enabled"}, reasoning_effort="high")
assert "thinking" not in params
assert "reasoning_effort" not in params
def test_existing_extra_body_is_preserved_and_not_mutated(self):
caller_extra_body = {"enable_search": True}
params = DashScopeChatConfig().map_openai_params(
non_default_params={"thinking": {"type": "enabled"}},
optional_params={"extra_body": caller_extra_body},
model="qwen3.8-max",
drop_params=False,
)
assert params["extra_body"] == {"enable_search": True, "enable_thinking": True}
assert caller_extra_body == {"enable_search": True}
def test_caller_optional_params_are_not_mutated(self):
caller_optional_params = {"temperature": 0.2, "extra_body": {"enable_search": True}}
params = DashScopeChatConfig().map_openai_params(
non_default_params={"thinking": {"type": "disabled"}, "max_tokens": 8},
optional_params=caller_optional_params,
model="qwen3.8-max",
drop_params=False,
)
assert params == {
"temperature": 0.2,
"max_tokens": 8,
"extra_body": {"enable_search": True, "enable_thinking": False},
}
assert params is not caller_optional_params
assert caller_optional_params == {"temperature": 0.2, "extra_body": {"enable_search": True}}
def test_get_optional_params_accepts_thinking_without_drop_params(self):
params = litellm.get_optional_params(
model="qwen3.8-max",
custom_llm_provider="dashscope",
thinking={"type": "disabled"},
drop_params=False,
)
assert params["extra_body"]["enable_thinking"] is False
assert "thinking" not in params
@pytest.mark.respx()
def test_thinking_disabled_reaches_the_wire_as_enable_thinking(self, respx_mock):
litellm.disable_aiohttp_transport = True
api_base = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
route = respx_mock.post(f"{api_base}/chat/completions").respond(
json={
"id": "chatcmpl-456",
"object": "chat.completion",
"created": 1677652288,
"model": "qwen3.8-max",
"choices": [{"index": 0, "message": {"role": "assistant", "content": "4"}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 9, "completion_tokens": 1, "total_tokens": 10},
},
status_code=200,
)
completion(
model="dashscope/qwen3.8-max",
messages=[{"role": "user", "content": "2+2?"}],
api_key="fake-dashscope-key",
api_base=api_base,
thinking={"type": "disabled"},
drop_params=False,
)
sent = json.loads(route.calls.last.request.content)
assert sent["enable_thinking"] is False
assert "thinking" not in sent
assert "extra_body" not in sent