diff --git a/docs/my-website/docs/providers/deepseek.md b/docs/my-website/docs/providers/deepseek.md index 31efb36c21f..1214431386d 100644 --- a/docs/my-website/docs/providers/deepseek.md +++ b/docs/my-website/docs/providers/deepseek.md @@ -58,9 +58,56 @@ We support ALL Deepseek models, just set `deepseek/` as a prefix when sending co ## Reasoning Models | Model Name | Function Call | |--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| deepseek-reasoner | `completion(model="deepseek/deepseek-reasoner", messages)` | +| deepseek-reasoner | `completion(model="deepseek/deepseek-reasoner", messages)` | +### Thinking / Reasoning Mode +Enable thinking mode for DeepSeek reasoner models using `thinking` or `reasoning_effort` parameters: + + + + +```python +from litellm import completion +import os + +os.environ['DEEPSEEK_API_KEY'] = "" + +resp = completion( + model="deepseek/deepseek-reasoner", + messages=[{"role": "user", "content": "What is 2+2?"}], + thinking={"type": "enabled"}, +) +print(resp.choices[0].message.reasoning_content) # Model's reasoning +print(resp.choices[0].message.content) # Final answer +``` + + + + +```python +from litellm import completion +import os + +os.environ['DEEPSEEK_API_KEY'] = "" + +resp = completion( + model="deepseek/deepseek-reasoner", + messages=[{"role": "user", "content": "What is 2+2?"}], + reasoning_effort="medium", # low, medium, high all map to thinking enabled +) +print(resp.choices[0].message.reasoning_content) # Model's reasoning +print(resp.choices[0].message.content) # Final answer +``` + + + + +:::note +DeepSeek only supports `{"type": "enabled"}` - unlike Anthropic, it doesn't support `budget_tokens`. Any `reasoning_effort` value other than `"none"` enables thinking mode. +::: + +### Basic Usage diff --git a/litellm/llms/deepseek/chat/transformation.py b/litellm/llms/deepseek/chat/transformation.py index a7defa886b5..d38ec4d67dd 100644 --- a/litellm/llms/deepseek/chat/transformation.py +++ b/litellm/llms/deepseek/chat/transformation.py @@ -14,6 +14,54 @@ from ...openai.chat.gpt_transformation import OpenAIGPTConfig class DeepSeekChatConfig(OpenAIGPTConfig): + def get_supported_openai_params(self, model: str) -> list: + """ + DeepSeek reasoner models support thinking parameter. + """ + params = super().get_supported_openai_params(model) + params.extend(["thinking", "reasoning_effort"]) + return params + + def map_openai_params( + self, + non_default_params: dict, + optional_params: dict, + model: str, + drop_params: bool, + ) -> dict: + """ + 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. + + Reference: https://api-docs.deepseek.com/guides/thinking_mode + """ + # Let parent handle standard params first + optional_params = super().map_openai_params( + non_default_params, optional_params, model, drop_params + ) + + # Pop thinking/reasoning_effort from optional_params first (parent may have added them) + # Then re-add only if valid for DeepSeek + 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"} + + # Handle reasoning_effort - map to thinking enabled + elif reasoning_effort is not None and reasoning_effort != "none": + optional_params["thinking"] = {"type": "enabled"} + + return optional_params + @overload def _transform_messages( self, messages: List[AllMessageValues], model: str, is_async: Literal[True] diff --git a/tests/litellm/llms/deepseek/__init__.py b/tests/litellm/llms/deepseek/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/litellm/llms/deepseek/chat/__init__.py b/tests/litellm/llms/deepseek/chat/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/litellm/llms/deepseek/chat/test_deepseek_chat_transformation.py b/tests/litellm/llms/deepseek/chat/test_deepseek_chat_transformation.py new file mode 100644 index 00000000000..a2f45e7188b --- /dev/null +++ b/tests/litellm/llms/deepseek/chat/test_deepseek_chat_transformation.py @@ -0,0 +1,168 @@ +""" +Unit tests for DeepSeek chat transformation. + +Tests the thinking and reasoning_effort parameter handling for DeepSeek models. +""" + +import pytest +from litellm.llms.deepseek.chat.transformation import DeepSeekChatConfig + + +class TestDeepSeekThinkingParams: + """Test thinking and reasoning_effort parameter handling for DeepSeek.""" + + def setup_method(self): + self.config = DeepSeekChatConfig() + self.model = "deepseek-reasoner" + + def test_get_supported_openai_params_includes_thinking(self): + """Test that thinking and reasoning_effort are in supported params.""" + params = self.config.get_supported_openai_params(self.model) + assert "thinking" in params + assert "reasoning_effort" in params + + def test_map_thinking_enabled(self): + """Test that thinking={"type": "enabled"} is passed through correctly.""" + non_default_params = {"thinking": {"type": "enabled"}} + optional_params = {} + + result = self.config.map_openai_params( + non_default_params=non_default_params, + optional_params=optional_params, + model=self.model, + drop_params=False, + ) + + assert result["thinking"] == {"type": "enabled"} + + def test_map_thinking_with_budget_tokens_strips_budget(self): + """Test that budget_tokens is stripped from thinking param (DeepSeek doesn't support it).""" + non_default_params = {"thinking": {"type": "enabled", "budget_tokens": 2048}} + optional_params = {} + + result = self.config.map_openai_params( + non_default_params=non_default_params, + optional_params=optional_params, + model=self.model, + drop_params=False, + ) + + # Should strip budget_tokens, only pass type + assert result["thinking"] == {"type": "enabled"} + assert "budget_tokens" not in result.get("thinking", {}) + + def test_map_reasoning_effort_medium(self): + """Test that reasoning_effort='medium' maps to thinking enabled.""" + 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, + drop_params=False, + ) + + assert result["thinking"] == {"type": "enabled"} + + def test_map_reasoning_effort_low(self): + """Test that reasoning_effort='low' maps to thinking enabled.""" + 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, + drop_params=False, + ) + + assert result["thinking"] == {"type": "enabled"} + + def test_map_reasoning_effort_high(self): + """Test that reasoning_effort='high' maps to thinking enabled.""" + 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, + drop_params=False, + ) + + assert result["thinking"] == {"type": "enabled"} + + def test_map_reasoning_effort_none_does_not_enable_thinking(self): + """Test that reasoning_effort='none' does not enable 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, + drop_params=False, + ) + + assert "thinking" not in result + + def test_map_reasoning_effort_null_does_not_enable_thinking(self): + """Test that reasoning_effort=None does not enable 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, + drop_params=False, + ) + + assert "thinking" not in result + + def test_thinking_takes_precedence_over_reasoning_effort(self): + """Test that thinking param takes precedence when both are provided.""" + non_default_params = { + "thinking": {"type": "enabled"}, + "reasoning_effort": "high", + } + optional_params = {} + + result = self.config.map_openai_params( + non_default_params=non_default_params, + optional_params=optional_params, + model=self.model, + drop_params=False, + ) + + # thinking should be set, reasoning_effort should not override + assert result["thinking"] == {"type": "enabled"} + + def test_invalid_thinking_type_ignored(self): + """Test that invalid thinking type values are ignored.""" + non_default_params = {"thinking": {"type": "invalid"}} + optional_params = {} + + result = self.config.map_openai_params( + non_default_params=non_default_params, + optional_params=optional_params, + model=self.model, + drop_params=False, + ) + + assert "thinking" not in result + + def test_thinking_none_value_ignored(self): + """Test that thinking=None is ignored.""" + non_default_params = {"thinking": None} + optional_params = {} + + result = self.config.map_openai_params( + non_default_params=non_default_params, + optional_params=optional_params, + model=self.model, + drop_params=False, + ) + + assert "thinking" not in result