mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-02 02:11:58 +00:00
fix(deepseek): pass reasoning_effort value through to DeepSeek V4 API
Fixes #27439 DeepSeek V4 Pro/Flash support graded reasoning effort via reasoning_effort ("high" or "max"). The old code collapsed all non-none values to thinking: {"type": "enabled"}, discarding the effort level entirely. Changes in map_openai_params(): - reasoning_effort low/medium/high -> normalized to "high", sends both thinking: {"type": "enabled"} and reasoning_effort: "high" for V4 models - reasoning_effort max/xhigh -> normalized to "max", sends both thinking: {"type": "enabled"} and reasoning_effort: "max" for V4 models - reasoning_effort is NOT forwarded to deepseek-reasoner/R1 - those models have always-on thinking and reject the field (guarded via supports_reasoning()) - reasoning_effort="none" sends thinking: {"type": "disabled"} for V4 opt-in models only; no-op for deepseek-reasoner (always-on, can't disable)
This commit is contained in:
parent
7270f723de
commit
903b09c7ef
2 changed files with 239 additions and 32 deletions
|
|
@ -35,7 +35,8 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
|
|||
Map OpenAI params to DeepSeek params.
|
||||
|
||||
Handles `thinking` and `reasoning_effort` parameters for DeepSeek reasoner models.
|
||||
DeepSeek only supports `{"type": "enabled"}` - no budget_tokens like Anthropic.
|
||||
DeepSeek supports `{"type": "enabled"}` and `{"type": "disabled"}` for thinking,
|
||||
and `reasoning_effort` values of `"high"` or `"max"` for V4 models.
|
||||
|
||||
Reference: https://api-docs.deepseek.com/guides/thinking_mode
|
||||
"""
|
||||
|
|
@ -49,18 +50,51 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
|
|||
thinking_value = optional_params.pop("thinking", None)
|
||||
reasoning_effort = optional_params.pop("reasoning_effort", None)
|
||||
|
||||
# Handle thinking parameter - only accept {"type": "enabled"}
|
||||
if thinking_value is not None:
|
||||
if (
|
||||
isinstance(thinking_value, dict)
|
||||
and thinking_value.get("type") == "enabled"
|
||||
):
|
||||
# DeepSeek only accepts {"type": "enabled"}, ignore budget_tokens
|
||||
optional_params["thinking"] = {"type": "enabled"}
|
||||
is_always_on_reasoner = supports_reasoning(
|
||||
model=model, custom_llm_provider="deepseek"
|
||||
)
|
||||
|
||||
# Handle reasoning_effort - map to thinking enabled
|
||||
elif reasoning_effort is not None and reasoning_effort != "none":
|
||||
optional_params["thinking"] = {"type": "enabled"}
|
||||
# Handle thinking parameter - accepts {"type": "enabled"} or {"type": "disabled"}.
|
||||
# Guard: deepseek-reasoner has always-on thinking and rejects {"type": "disabled"}.
|
||||
if thinking_value is not None:
|
||||
if isinstance(thinking_value, dict) and thinking_value.get("type") in (
|
||||
"enabled",
|
||||
"disabled",
|
||||
):
|
||||
thinking_type = thinking_value.get("type")
|
||||
if thinking_type == "disabled" and is_always_on_reasoner:
|
||||
pass # no-op: deepseek-reasoner rejects {"type": "disabled"}
|
||||
else:
|
||||
optional_params["thinking"] = {"type": thinking_type}
|
||||
|
||||
# Handle reasoning_effort when thinking was not explicitly provided.
|
||||
if reasoning_effort is not None and "thinking" not in optional_params:
|
||||
if reasoning_effort == "none":
|
||||
# Only send thinking: disabled on V4 opt-in models.
|
||||
# deepseek-reasoner/R1 have always-on thinking and reject {"type": "disabled"}.
|
||||
if not is_always_on_reasoner:
|
||||
optional_params["thinking"] = {"type": "disabled"}
|
||||
else:
|
||||
# Normalize to DeepSeek's two supported values
|
||||
normalized = "max" if reasoning_effort in ("max", "xhigh") else "high"
|
||||
optional_params["thinking"] = {"type": "enabled"}
|
||||
# Only forward reasoning_effort on V4 opt-in models.
|
||||
# deepseek-reasoner/R1 have supports_reasoning=True but don't accept reasoning_effort field.
|
||||
if not is_always_on_reasoner:
|
||||
optional_params["reasoning_effort"] = normalized
|
||||
|
||||
# When both thinking=enabled and reasoning_effort are provided for V4 models,
|
||||
# also forward the effort level (not dropped by the thinking branch above).
|
||||
if (
|
||||
reasoning_effort is not None
|
||||
and reasoning_effort != "none"
|
||||
and optional_params.get("thinking", {}).get("type") == "enabled"
|
||||
and "reasoning_effort" not in optional_params
|
||||
and not is_always_on_reasoner
|
||||
):
|
||||
optional_params["reasoning_effort"] = (
|
||||
"max" if reasoning_effort in ("max", "xhigh") else "high"
|
||||
)
|
||||
|
||||
return optional_params
|
||||
|
||||
|
|
@ -137,14 +171,13 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
|
|||
|
||||
def _thinking_mode_active(self, model: str, optional_params: dict) -> bool:
|
||||
"""
|
||||
Returns True only when thinking mode is actually active for this request:
|
||||
- model supports reasoning (capability check)
|
||||
- user explicitly passed thinking={"type": "enabled"} (opt-in check)
|
||||
Returns True when thinking mode is active for this request:
|
||||
- deepseek-reasoner/R1: always-on thinking, no explicit param needed
|
||||
- V4 opt-in models: only when user explicitly passed thinking={"type": "enabled"}
|
||||
"""
|
||||
return (
|
||||
supports_reasoning(model=model, custom_llm_provider="deepseek")
|
||||
and (optional_params.get("thinking") or {}).get("type") == "enabled"
|
||||
)
|
||||
if supports_reasoning(model=model, custom_llm_provider="deepseek"):
|
||||
return True # deepseek-reasoner always has thinking on
|
||||
return (optional_params.get("thinking") or {}).get("type") == "enabled"
|
||||
|
||||
def transform_request(
|
||||
self,
|
||||
|
|
@ -158,10 +191,8 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
|
|||
Ensures `reasoning_content` is forwarded on assistant messages for
|
||||
multi-turn thinking-mode conversations (issue #28045).
|
||||
|
||||
Only runs when thinking mode is actually active - guarded by both
|
||||
supports_reasoning() (model capability) and optional_params["thinking"]
|
||||
(user explicitly enabled it), preventing spurious injection on models
|
||||
like deepseek-v3.2 that support thinking as opt-in but not always-on.
|
||||
Runs when thinking mode is active: always for deepseek-reasoner (always-on),
|
||||
and for V4 opt-in models only when the user explicitly enabled thinking.
|
||||
"""
|
||||
if self._thinking_mode_active(model=model, optional_params=optional_params):
|
||||
messages = self._fill_reasoning_content(messages)
|
||||
|
|
|
|||
|
|
@ -52,60 +52,126 @@ class TestDeepSeekThinkingParams:
|
|||
assert "budget_tokens" not in result.get("thinking", {})
|
||||
|
||||
def test_map_reasoning_effort_medium(self):
|
||||
"""Test that reasoning_effort='medium' maps to thinking enabled."""
|
||||
"""Test that reasoning_effort='medium' normalizes to high for V4 models."""
|
||||
non_default_params = {"reasoning_effort": "medium"}
|
||||
optional_params = {}
|
||||
|
||||
result = self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model=self.model,
|
||||
model="deepseek-v4-pro",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result["thinking"] == {"type": "enabled"}
|
||||
assert result["reasoning_effort"] == "high"
|
||||
|
||||
def test_map_reasoning_effort_low(self):
|
||||
"""Test that reasoning_effort='low' maps to thinking enabled."""
|
||||
"""Test that reasoning_effort='low' normalizes to high for V4 models."""
|
||||
non_default_params = {"reasoning_effort": "low"}
|
||||
optional_params = {}
|
||||
|
||||
result = self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model=self.model,
|
||||
model="deepseek-v4-pro",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result["thinking"] == {"type": "enabled"}
|
||||
assert result["reasoning_effort"] == "high"
|
||||
|
||||
def test_map_reasoning_effort_high(self):
|
||||
"""Test that reasoning_effort='high' maps to thinking enabled."""
|
||||
"""Test that reasoning_effort='high' passes through as high for V4 models."""
|
||||
non_default_params = {"reasoning_effort": "high"}
|
||||
optional_params = {}
|
||||
|
||||
result = self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model=self.model,
|
||||
model="deepseek-v4-pro",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result["thinking"] == {"type": "enabled"}
|
||||
assert result["reasoning_effort"] == "high"
|
||||
|
||||
def test_map_reasoning_effort_none_does_not_enable_thinking(self):
|
||||
"""Test that reasoning_effort='none' does not enable thinking."""
|
||||
def test_map_reasoning_effort_max(self):
|
||||
"""Test that reasoning_effort='max' passes through as max for V4 models."""
|
||||
non_default_params = {"reasoning_effort": "max"}
|
||||
optional_params = {}
|
||||
|
||||
result = self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model="deepseek-v4-pro",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result["thinking"] == {"type": "enabled"}
|
||||
assert result["reasoning_effort"] == "max"
|
||||
|
||||
def test_map_reasoning_effort_xhigh_normalizes_to_max(self):
|
||||
"""Test that reasoning_effort='xhigh' normalizes to max for V4 models."""
|
||||
non_default_params = {"reasoning_effort": "xhigh"}
|
||||
optional_params = {}
|
||||
|
||||
result = self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model="deepseek-v4-pro",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result["thinking"] == {"type": "enabled"}
|
||||
assert result["reasoning_effort"] == "max"
|
||||
|
||||
def test_map_reasoning_effort_not_forwarded_for_reasoner(self):
|
||||
"""Test that reasoning_effort is not forwarded to deepseek-reasoner (R1 doesn't accept it)."""
|
||||
non_default_params = {"reasoning_effort": "max"}
|
||||
optional_params = {}
|
||||
|
||||
result = self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model=self.model, # deepseek-reasoner
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
# thinking should still be enabled but reasoning_effort must NOT be forwarded
|
||||
assert result["thinking"] == {"type": "enabled"}
|
||||
assert "reasoning_effort" not in result
|
||||
|
||||
def test_map_reasoning_effort_none_is_noop_for_reasoner(self):
|
||||
"""Test that reasoning_effort='none' is a no-op for deepseek-reasoner (always-on thinking)."""
|
||||
non_default_params = {"reasoning_effort": "none"}
|
||||
optional_params = {}
|
||||
|
||||
result = self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model=self.model,
|
||||
model=self.model, # deepseek-reasoner
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
# deepseek-reasoner has always-on thinking, API rejects {"type": "disabled"}
|
||||
assert "thinking" not in result
|
||||
assert "reasoning_effort" not in result
|
||||
|
||||
def test_map_reasoning_effort_none_disables_thinking_for_v4(self):
|
||||
"""Test that reasoning_effort='none' sends thinking disabled for V4 opt-in models."""
|
||||
non_default_params = {"reasoning_effort": "none"}
|
||||
optional_params = {}
|
||||
|
||||
result = self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model="deepseek-v4-pro",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result["thinking"] == {"type": "disabled"}
|
||||
assert "reasoning_effort" not in result
|
||||
|
||||
def test_map_reasoning_effort_null_does_not_enable_thinking(self):
|
||||
"""Test that reasoning_effort=None does not enable thinking."""
|
||||
|
|
@ -166,3 +232,113 @@ class TestDeepSeekThinkingParams:
|
|||
)
|
||||
|
||||
assert "thinking" not in result
|
||||
|
||||
def test_map_thinking_disabled_is_noop_for_reasoner(self):
|
||||
"""Test that thinking={"type": "disabled"} is a no-op for deepseek-reasoner (always-on thinking)."""
|
||||
non_default_params = {"thinking": {"type": "disabled"}}
|
||||
optional_params = {}
|
||||
|
||||
result = self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model=self.model, # deepseek-reasoner
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
# deepseek-reasoner rejects {"type": "disabled"} - should be silently dropped
|
||||
assert "thinking" not in result
|
||||
|
||||
def test_map_thinking_disabled_passes_through_for_v4(self):
|
||||
"""Test that thinking={"type": "disabled"} is passed through for V4 opt-in models."""
|
||||
non_default_params = {"thinking": {"type": "disabled"}}
|
||||
optional_params = {}
|
||||
|
||||
result = self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model="deepseek-v4-pro",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result["thinking"] == {"type": "disabled"}
|
||||
|
||||
def test_map_thinking_disabled_with_budget_tokens_strips_budget(self):
|
||||
"""Test that budget_tokens is stripped even when thinking is disabled."""
|
||||
non_default_params = {"thinking": {"type": "disabled", "budget_tokens": 0}}
|
||||
optional_params = {}
|
||||
|
||||
result = self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model="deepseek-v4-pro",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result["thinking"] == {"type": "disabled"}
|
||||
assert "budget_tokens" not in result.get("thinking", {})
|
||||
|
||||
def test_map_thinking_and_reasoning_effort_both_forwarded_for_v4(self):
|
||||
"""Test that when both thinking and reasoning_effort are provided for V4, effort is not dropped."""
|
||||
non_default_params = {
|
||||
"thinking": {"type": "enabled"},
|
||||
"reasoning_effort": "max",
|
||||
}
|
||||
optional_params = {}
|
||||
|
||||
result = self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model="deepseek-v4-pro",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
assert result["thinking"] == {"type": "enabled"}
|
||||
assert result["reasoning_effort"] == "max"
|
||||
|
||||
|
||||
class TestFillReasoningContent:
|
||||
"""Test _fill_reasoning_content helper for multi-turn thinking-mode conversations."""
|
||||
|
||||
def setup_method(self):
|
||||
self.config = DeepSeekChatConfig()
|
||||
|
||||
def test_injects_placeholder_when_reasoning_content_missing(self):
|
||||
"""Assistant message missing reasoning_content gets a space placeholder injected."""
|
||||
messages = [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": "hi"},
|
||||
]
|
||||
result = self.config._fill_reasoning_content(messages)
|
||||
assert result[1]["reasoning_content"] == " "
|
||||
|
||||
def test_promotes_reasoning_content_from_provider_specific_fields(self):
|
||||
"""reasoning_content stored in provider_specific_fields is promoted to top level."""
|
||||
messages = [
|
||||
{"role": "user", "content": "hello"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "hi",
|
||||
"provider_specific_fields": {"reasoning_content": "my reasoning"},
|
||||
},
|
||||
]
|
||||
result = self.config._fill_reasoning_content(messages)
|
||||
assert result[1]["reasoning_content"] == "my reasoning"
|
||||
assert "reasoning_content" not in result[1].get("provider_specific_fields", {})
|
||||
|
||||
def test_does_not_overwrite_existing_reasoning_content(self):
|
||||
"""Assistant message that already has reasoning_content is left unchanged."""
|
||||
messages = [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": "hi", "reasoning_content": "already here"},
|
||||
]
|
||||
result = self.config._fill_reasoning_content(messages)
|
||||
assert result[1]["reasoning_content"] == "already here"
|
||||
|
||||
def test_non_assistant_messages_are_unchanged(self):
|
||||
"""User and system messages are passed through untouched."""
|
||||
messages = [
|
||||
{"role": "system", "content": "you are helpful"},
|
||||
{"role": "user", "content": "hello"},
|
||||
]
|
||||
result = self.config._fill_reasoning_content(messages)
|
||||
assert result == messages
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue