fix(deepseek): fall back to thinking_always_on model flag for tool-loop 400s

DeepSeekChatConfig._thinking_mode_active only returned True when a caller explicitly passed thinking={"type": "enabled"}, which is correct for opt-in reasoning models but wrong for deepseek-v4-pro and deepseek-v4-flash, which run thinking mode on by default server-side with no explicit param required. Requests through a router or proxy deployment that never set a reasoning param hit the guard as False, _fill_reasoning_content never ran, and the DeepSeek API rejected every second call of a multi-turn tool loop with a 400 asking for reasoning_content to be passed back.

The model cost map already has an established thinking_always_on flag, currently wired up only for Anthropic Claude Fable 5 / Mythos 5 models via AnthropicModelInfo._supports_model_capability, itself built on the same generic _supports_factory used by supports_reasoning. Added is_thinking_always_on to litellm/utils.py as a one-line wrapper mirroring supports_reasoning, and extended _thinking_mode_active to fall back to it when the request carries no explicit thinking param, while an explicit thinking={"type": "disabled"} still short-circuits to False.

Added thinking_always_on: true to the deepseek-v4-pro and deepseek-v4-flash entries in the model cost map (both the bare and deepseek/-prefixed keys, which are kept as duplicates) and to the CI-enforced byte-identical backup copy.

Added tests covering the always-on fallback, the explicit-disable override, and confirming opt-in models like deepseek-reasoner are unaffected. Verified locally: reverting only the code change while keeping the new cost-map flag reproduces the exact failure this fixes.

Closes #37636
This commit is contained in:
adityaanikam 2026-08-22 17:37:06 +05:30
parent b9bff0998c
commit 899df60a66
5 changed files with 53 additions and 16 deletions

View file

@ -11,7 +11,7 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
)
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import AllMessageValues
from litellm.utils import supports_reasoning
from litellm.utils import is_thinking_always_on, supports_reasoning
from ...openai.chat.gpt_transformation import OpenAIGPTConfig
@ -127,16 +127,20 @@ 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:
Returns True when thinking mode is active for this request:
- model supports reasoning (capability check)
- user explicitly passed thinking={"type": "enabled"} (opt-in check)
- AND either the user explicitly opted in with thinking={"type": "enabled"},
or thinking is on by default for this model and was not explicitly disabled.
"""
if not supports_reasoning(model=model, custom_llm_provider="deepseek"):
return False
thinking: Final = optional_params.get("thinking")
return (
supports_reasoning(model=model, custom_llm_provider="deepseek")
and isinstance(thinking, dict)
and thinking.get("type") == "enabled"
)
thinking_type = thinking.get("type") if isinstance(thinking, dict) else None
if thinking_type == "enabled":
return True
if thinking_type == "disabled":
return False
return is_thinking_always_on(model=model, custom_llm_provider="deepseek")
@staticmethod
def _drop_unsupported_tools(optional_params: dict) -> dict:

View file

@ -49632,7 +49632,8 @@
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
"supports_vision": false,
"thinking_always_on": true
},
"deepseek-v4-pro": {
"cache_creation_input_token_cost": 0.0,
@ -49658,7 +49659,8 @@
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
"supports_vision": false,
"thinking_always_on": true
},
"deepseek/deepseek-v4-flash": {
"cache_creation_input_token_cost": 0.0,
@ -49684,7 +49686,8 @@
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
"supports_vision": false,
"thinking_always_on": true
},
"deepseek/deepseek-v4-pro": {
"cache_creation_input_token_cost": 0.0,
@ -49710,7 +49713,8 @@
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
"supports_vision": false,
"thinking_always_on": true
},
"tencent/deepseek-v4-pro": {
"cache_creation_input_token_cost": 0.0,

View file

@ -2614,6 +2614,13 @@ def supports_reasoning(model: str, custom_llm_provider: str | None = None) -> bo
return _supports_factory(model=model, custom_llm_provider=custom_llm_provider, key="supports_reasoning")
def is_thinking_always_on(model: str, custom_llm_provider: str | None = None) -> bool:
"""
Check if the given model always runs in thinking mode by default and return a boolean value.
"""
return _supports_factory(model=model, custom_llm_provider=custom_llm_provider, key="thinking_always_on")
def supports_native_structured_output(model: str, custom_llm_provider: str | None = None) -> bool:
"""
Check if the given model supports native structured outputs and return a boolean value.

View file

@ -49632,7 +49632,8 @@
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
"supports_vision": false,
"thinking_always_on": true
},
"deepseek-v4-pro": {
"cache_creation_input_token_cost": 0.0,
@ -49658,7 +49659,8 @@
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
"supports_vision": false,
"thinking_always_on": true
},
"deepseek/deepseek-v4-flash": {
"cache_creation_input_token_cost": 0.0,
@ -49684,7 +49686,8 @@
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
"supports_vision": false,
"thinking_always_on": true
},
"deepseek/deepseek-v4-pro": {
"cache_creation_input_token_cost": 0.0,
@ -49710,7 +49713,8 @@
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": false
"supports_vision": false,
"thinking_always_on": true
},
"tencent/deepseek-v4-pro": {
"cache_creation_input_token_cost": 0.0,

View file

@ -108,6 +108,24 @@ def test_thinking_mode_active_bool_thinking_returns_false_without_crashing():
assert config._thinking_mode_active(model="deepseek-reasoner", optional_params={"thinking": True}) is False
def test_thinking_mode_active_true_by_default_for_always_on_model():
config = DeepSeekChatConfig()
assert config._thinking_mode_active(model="deepseek-v4-pro", optional_params={}) is True
def test_thinking_mode_active_false_when_explicitly_disabled_for_always_on_model():
config = DeepSeekChatConfig()
assert (
config._thinking_mode_active(model="deepseek-v4-pro", optional_params={"thinking": {"type": "disabled"}})
is False
)
def test_thinking_mode_active_false_by_default_for_opt_in_model():
config = DeepSeekChatConfig()
assert config._thinking_mode_active(model="deepseek-reasoner", optional_params={}) is False
class TestDeepSeekThinkingParams:
"""Test thinking and reasoning_effort parameter handling for DeepSeek."""