fix(deepseek): treat thinking mode as default-on for DeepSeek V4 models

DeepSeek V4 (deepseek-v4-flash / deepseek-v4-pro) runs in thinking mode by
default and rejects multi-turn tool-call histories where an assistant message
is missing reasoning_content and carries a tool_call id DeepSeek does not
recognize (standard behavior for frontends/agent frameworks that regenerate
ids), returning HTTP 400 "The reasoning_content in the thinking mode must be
passed back to the API."

The injection mechanism for this already exists (_fill_reasoning_content,
#28080) but never fired for V4 because:
1. deepseek-v4-* models were missing from the model registry, so
   supports_reasoning() returned False
2. _thinking_mode_active() required an explicit thinking={"type": "enabled"}
   param, but V4 enables thinking by default

Changes:
- Add deepseek-v4-flash / deepseek-v4-pro registry entries (bare and
  deepseek/-prefixed) with official pricing
- _thinking_mode_active(): treat the V4 family as thinking-mode by default
  unless thinking={"type": "disabled"} is passed; opt-in models (e.g.
  deepseek-v3.2) keep requiring explicit enablement
- map_openai_params(): forward thinking={"type": "disabled"} (previously
  silently dropped) so users can opt out of V4 default-on thinking

Fixes #26395

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Milan 2026-06-11 22:51:39 +03:00
parent a992ed18df
commit 2ef49eed60
No known key found for this signature in database
4 changed files with 348 additions and 13 deletions

View file

@ -49,14 +49,15 @@ 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"}
# Handle thinking parameter - accept {"type": "enabled"} and
# {"type": "disabled"} (the latter opts out of V4's default-on thinking)
if thinking_value is not None:
if (
isinstance(thinking_value, dict)
and thinking_value.get("type") == "enabled"
if isinstance(thinking_value, dict) and thinking_value.get("type") in (
"enabled",
"disabled",
):
# DeepSeek only accepts {"type": "enabled"}, ignore budget_tokens
optional_params["thinking"] = {"type": "enabled"}
# DeepSeek only accepts the `type` key, ignore budget_tokens
optional_params["thinking"] = {"type": thinking_value["type"]}
# Handle reasoning_effort - map to thinking enabled
elif reasoning_effort is not None and reasoning_effort != "none":
@ -135,16 +136,33 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
messages=messages, model=model, is_async=False
)
# Model families where DeepSeek enables thinking mode BY DEFAULT (no
# `thinking` param required). Reference:
# https://api-docs.deepseek.com/guides/thinking_mode
DEFAULT_THINKING_MODEL_PREFIXES = ("deepseek-v4",)
def _is_default_thinking_model(self, model: str) -> bool:
return any(
prefix in model for prefix in self.DEFAULT_THINKING_MODEL_PREFIXES
)
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:
- user explicitly passed thinking={"type": "enabled"} on a model that
supports reasoning, OR
- the model runs in thinking mode by default (DeepSeek V4 family) and
the user did not explicitly disable it.
Models like deepseek-v3.2 (supports_reasoning but opt-in thinking)
remain untouched unless thinking is explicitly enabled.
"""
return (
supports_reasoning(model=model, custom_llm_provider="deepseek")
and (optional_params.get("thinking") or {}).get("type") == "enabled"
)
thinking_type = (optional_params.get("thinking") or {}).get("type")
if thinking_type == "disabled":
return False
if thinking_type == "enabled":
return supports_reasoning(model=model, custom_llm_provider="deepseek")
return self._is_default_thinking_model(model)
def transform_request(
self,

View file

@ -10854,6 +10854,52 @@
"supports_system_messages": true,
"supports_tool_choice": false
},
"deepseek-v4-flash": {
"cache_read_input_token_cost": 2.8e-09,
"input_cost_per_token": 1.4e-07,
"litellm_provider": "deepseek",
"max_input_tokens": 1048576,
"max_output_tokens": 393216,
"max_tokens": 393216,
"mode": "chat",
"output_cost_per_token": 2.8e-07,
"source": "https://api-docs.deepseek.com/quick_start/pricing",
"supported_endpoints": [
"/v1/chat/completions"
],
"supports_assistant_prefill": true,
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_parallel_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true
},
"deepseek-v4-pro": {
"cache_read_input_token_cost": 3.625e-09,
"input_cost_per_token": 4.35e-07,
"litellm_provider": "deepseek",
"max_input_tokens": 1048576,
"max_output_tokens": 393216,
"max_tokens": 393216,
"mode": "chat",
"output_cost_per_token": 8.7e-07,
"source": "https://api-docs.deepseek.com/quick_start/pricing",
"supported_endpoints": [
"/v1/chat/completions"
],
"supports_assistant_prefill": true,
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_parallel_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true
},
"dashscope/qwen-coder": {
"input_cost_per_token": 3e-07,
"litellm_provider": "dashscope",
@ -13446,6 +13492,52 @@
"supports_reasoning": true,
"supports_tool_choice": true
},
"deepseek/deepseek-v4-flash": {
"cache_read_input_token_cost": 2.8e-09,
"input_cost_per_token": 1.4e-07,
"litellm_provider": "deepseek",
"max_input_tokens": 1048576,
"max_output_tokens": 393216,
"max_tokens": 393216,
"mode": "chat",
"output_cost_per_token": 2.8e-07,
"source": "https://api-docs.deepseek.com/quick_start/pricing",
"supported_endpoints": [
"/v1/chat/completions"
],
"supports_assistant_prefill": true,
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_parallel_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true
},
"deepseek/deepseek-v4-pro": {
"cache_read_input_token_cost": 3.625e-09,
"input_cost_per_token": 4.35e-07,
"litellm_provider": "deepseek",
"max_input_tokens": 1048576,
"max_output_tokens": 393216,
"max_tokens": 393216,
"mode": "chat",
"output_cost_per_token": 8.7e-07,
"source": "https://api-docs.deepseek.com/quick_start/pricing",
"supported_endpoints": [
"/v1/chat/completions"
],
"supports_assistant_prefill": true,
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_parallel_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true
},
"deepseek.v3-v1:0": {
"input_cost_per_token": 5.8e-07,
"litellm_provider": "bedrock_converse",

View file

@ -10854,6 +10854,52 @@
"supports_system_messages": true,
"supports_tool_choice": false
},
"deepseek-v4-flash": {
"cache_read_input_token_cost": 2.8e-09,
"input_cost_per_token": 1.4e-07,
"litellm_provider": "deepseek",
"max_input_tokens": 1048576,
"max_output_tokens": 393216,
"max_tokens": 393216,
"mode": "chat",
"output_cost_per_token": 2.8e-07,
"source": "https://api-docs.deepseek.com/quick_start/pricing",
"supported_endpoints": [
"/v1/chat/completions"
],
"supports_assistant_prefill": true,
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_parallel_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true
},
"deepseek-v4-pro": {
"cache_read_input_token_cost": 3.625e-09,
"input_cost_per_token": 4.35e-07,
"litellm_provider": "deepseek",
"max_input_tokens": 1048576,
"max_output_tokens": 393216,
"max_tokens": 393216,
"mode": "chat",
"output_cost_per_token": 8.7e-07,
"source": "https://api-docs.deepseek.com/quick_start/pricing",
"supported_endpoints": [
"/v1/chat/completions"
],
"supports_assistant_prefill": true,
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_parallel_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true
},
"dashscope/qwen-coder": {
"input_cost_per_token": 3e-07,
"litellm_provider": "dashscope",
@ -13446,6 +13492,52 @@
"supports_reasoning": true,
"supports_tool_choice": true
},
"deepseek/deepseek-v4-flash": {
"cache_read_input_token_cost": 2.8e-09,
"input_cost_per_token": 1.4e-07,
"litellm_provider": "deepseek",
"max_input_tokens": 1048576,
"max_output_tokens": 393216,
"max_tokens": 393216,
"mode": "chat",
"output_cost_per_token": 2.8e-07,
"source": "https://api-docs.deepseek.com/quick_start/pricing",
"supported_endpoints": [
"/v1/chat/completions"
],
"supports_assistant_prefill": true,
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_parallel_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true
},
"deepseek/deepseek-v4-pro": {
"cache_read_input_token_cost": 3.625e-09,
"input_cost_per_token": 4.35e-07,
"litellm_provider": "deepseek",
"max_input_tokens": 1048576,
"max_output_tokens": 393216,
"max_tokens": 393216,
"mode": "chat",
"output_cost_per_token": 8.7e-07,
"source": "https://api-docs.deepseek.com/quick_start/pricing",
"supported_endpoints": [
"/v1/chat/completions"
],
"supports_assistant_prefill": true,
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_parallel_function_calling": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true
},
"deepseek.v3-v1:0": {
"input_cost_per_token": 5.8e-07,
"litellm_provider": "bedrock_converse",

View file

@ -166,3 +166,136 @@ class TestDeepSeekThinkingParams:
)
assert "thinking" not in result
def test_map_thinking_disabled_passed_through(self):
"""thinking={"type": "disabled"} must be forwarded so users can opt out
of DeepSeek V4's default-on thinking mode."""
result = self.config.map_openai_params(
non_default_params={"thinking": {"type": "disabled"}},
optional_params={},
model="deepseek-v4-flash",
drop_params=False,
)
assert result["thinking"] == {"type": "disabled"}
class TestDeepSeekV4DefaultThinkingMode:
"""
DeepSeek V4 models run in thinking mode BY DEFAULT and require
`reasoning_content` to be passed back on assistant messages
(https://github.com/BerriAI/litellm/issues/26395).
"""
def setup_method(self):
self.config = DeepSeekChatConfig()
# --- registry ---
@pytest.mark.parametrize(
"model",
["deepseek-v4-flash", "deepseek-v4-pro"],
)
def test_v4_models_registered_with_reasoning(self, model):
from litellm.utils import supports_reasoning
assert supports_reasoning(model=model, custom_llm_provider="deepseek")
assert supports_reasoning(model=f"deepseek/{model}")
# --- _thinking_mode_active guard ---
@pytest.mark.parametrize("model", ["deepseek-v4-flash", "deepseek-v4-pro"])
def test_thinking_active_by_default_for_v4(self, model):
assert self.config._thinking_mode_active(model=model, optional_params={})
@pytest.mark.parametrize("model", ["deepseek-v4-flash", "deepseek-v4-pro"])
def test_thinking_inactive_when_explicitly_disabled(self, model):
assert not self.config._thinking_mode_active(
model=model, optional_params={"thinking": {"type": "disabled"}}
)
@pytest.mark.parametrize("model", ["deepseek-v4-flash", "deepseek-v4-pro"])
def test_thinking_active_when_explicitly_enabled(self, model):
assert self.config._thinking_mode_active(
model=model, optional_params={"thinking": {"type": "enabled"}}
)
def test_opt_in_models_unaffected_by_default(self):
"""deepseek-v3.2 supports reasoning but thinking is opt-in: no thinking
param -> guard must stay off (no spurious injection)."""
assert not self.config._thinking_mode_active(
model="deepseek-v3.2", optional_params={}
)
assert self.config._thinking_mode_active(
model="deepseek-v3.2", optional_params={"thinking": {"type": "enabled"}}
)
def test_non_reasoning_model_unaffected(self):
assert not self.config._thinking_mode_active(
model="deepseek-chat", optional_params={}
)
# --- end-to-end transform_request ---
def _tool_call_history(self):
return [
{"role": "user", "content": "What's the weather in Tokyo?"},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_client_generated_id",
"type": "function",
"function": {
"name": "get_weather",
"arguments": '{"city": "Tokyo"}',
},
}
],
# reasoning_content stripped, tool_call id rewritten:
# standard behavior of frontends/agent frameworks, and the
# exact request shape DeepSeek rejects with
# "The `reasoning_content` in the thinking mode must be passed back to the API."
},
{
"role": "tool",
"tool_call_id": "call_client_generated_id",
"content": '{"weather": "Sunny", "temp_c": 28}',
},
]
def test_transform_request_injects_reasoning_content_for_v4_by_default(self):
body = self.config.transform_request(
model="deepseek-v4-flash",
messages=self._tool_call_history(),
optional_params={},
litellm_params={},
headers={},
)
assistant_msg = body["messages"][1]
assert assistant_msg["reasoning_content"] == " "
def test_transform_request_no_injection_when_thinking_disabled(self):
body = self.config.transform_request(
model="deepseek-v4-flash",
messages=self._tool_call_history(),
optional_params={"thinking": {"type": "disabled"}},
litellm_params={},
headers={},
)
assistant_msg = body["messages"][1]
assert "reasoning_content" not in assistant_msg
def test_transform_request_preserves_existing_reasoning_content(self):
messages = self._tool_call_history()
messages[1]["reasoning_content"] = "I should check the weather tool."
body = self.config.transform_request(
model="deepseek-v4-pro",
messages=messages,
optional_params={},
litellm_params={},
headers={},
)
assistant_msg = body["messages"][1]
assert assistant_msg["reasoning_content"] == "I should check the weather tool."