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:
Divyansh8321 2026-05-18 00:39:53 +05:30
parent 7270f723de
commit 903b09c7ef
2 changed files with 239 additions and 32 deletions

View file

@ -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)

View file

@ -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