diff --git a/litellm/llms/deepseek/chat/transformation.py b/litellm/llms/deepseek/chat/transformation.py index 7ed3e484535..d8e11c99323 100644 --- a/litellm/llms/deepseek/chat/transformation.py +++ b/litellm/llms/deepseek/chat/transformation.py @@ -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, diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index aab0e4264d0..a0df50e481c 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -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", diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index f0b2432ddc8..15c68c18254 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -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", diff --git a/tests/litellm/llms/deepseek/chat/test_deepseek_chat_transformation.py b/tests/litellm/llms/deepseek/chat/test_deepseek_chat_transformation.py index a2f45e7188b..01a2c522d8d 100644 --- a/tests/litellm/llms/deepseek/chat/test_deepseek_chat_transformation.py +++ b/tests/litellm/llms/deepseek/chat/test_deepseek_chat_transformation.py @@ -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."