From cc84e2c76b3432be13047f9191abfa3a4487064b Mon Sep 17 00:00:00 2001 From: Jay Date: Fri, 8 May 2026 12:13:07 -0400 Subject: [PATCH] fix: gate reasoning_effort to V4 models, handle combined params MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Greptile review feedback: - Add _is_v4_model() to only forward reasoning_effort to V4 Pro/Flash (older models like deepseek-reasoner don't support the param) - Forward reasoning_effort even when thinking dict is also provided (fixes silent drop in the elif branch) - Move normalization (low→high, xhigh→max) before branching - Expand tests: V4 vs non-V4 coverage, combined params, model detection Signed-off-by: Jay --- litellm/llms/deepseek/chat/transformation.py | 32 ++-- .../chat/test_deepseek_chat_transformation.py | 153 ++++++++++++++---- 2 files changed, 143 insertions(+), 42 deletions(-) diff --git a/litellm/llms/deepseek/chat/transformation.py b/litellm/llms/deepseek/chat/transformation.py index 7df1c89d989..2e1d71f8ac3 100644 --- a/litellm/llms/deepseek/chat/transformation.py +++ b/litellm/llms/deepseek/chat/transformation.py @@ -47,29 +47,41 @@ class DeepSeekChatConfig(OpenAIGPTConfig): thinking_value = optional_params.pop("thinking", None) reasoning_effort = optional_params.pop("reasoning_effort", None) + # Normalize reasoning_effort values per DeepSeek V4 compatibility + # mappings: low/medium→high, xhigh→max + if reasoning_effort is not None and reasoning_effort != "none": + if reasoning_effort in ("low", "medium"): + reasoning_effort = "high" + elif reasoning_effort == "xhigh": + reasoning_effort = "max" + # 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"} + # Forward reasoning_effort alongside thinking for V4 models + if reasoning_effort is not None and reasoning_effort != "none": + if self._is_v4_model(model): + optional_params["reasoning_effort"] = reasoning_effort - # Handle reasoning_effort - enable thinking and pass effort level - # DeepSeek V4 Pro/Flash support reasoning_effort as a native param - # with values "high" and "max". Older models ignore it gracefully. + # Handle reasoning_effort alone (without explicit thinking dict) elif reasoning_effort is not None and reasoning_effort != "none": optional_params["thinking"] = {"type": "enabled"} - # Normalize per DeepSeek V4 compatibility mappings - if reasoning_effort in ("low", "medium"): - reasoning_effort = "high" - elif reasoning_effort == "xhigh": - reasoning_effort = "max" - optional_params["reasoning_effort"] = reasoning_effort + # Only V4 models support reasoning_effort as a native parameter + if self._is_v4_model(model): + optional_params["reasoning_effort"] = reasoning_effort return optional_params + @staticmethod + def _is_v4_model(model: str) -> bool: + """Check if the model is a DeepSeek V4 variant that supports + reasoning_effort as a native parameter.""" + return "v4" in model.lower() + @overload def _transform_messages( self, messages: List[AllMessageValues], model: str, is_async: Literal[True] 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 d13ae62d93f..ae0d84bdc06 100644 --- a/tests/litellm/llms/deepseek/chat/test_deepseek_chat_transformation.py +++ b/tests/litellm/llms/deepseek/chat/test_deepseek_chat_transformation.py @@ -14,6 +14,7 @@ class TestDeepSeekThinkingParams: def setup_method(self): self.config = DeepSeekChatConfig() self.model = "deepseek-reasoner" + self.v4_model = "deepseek-v4-pro" def test_get_supported_openai_params_includes_thinking(self): """Test that thinking and reasoning_effort are in supported params.""" @@ -51,8 +52,10 @@ class TestDeepSeekThinkingParams: 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' enables thinking and normalizes to 'high'.""" + # --- reasoning_effort on non-V4 models (backward compat) --- + + def test_map_reasoning_effort_medium_non_v4(self): + """Non-V4: reasoning_effort='medium' enables thinking but does NOT forward effort.""" non_default_params = {"reasoning_effort": "medium"} optional_params = {} @@ -64,10 +67,10 @@ class TestDeepSeekThinkingParams: ) assert result["thinking"] == {"type": "enabled"} - assert result["reasoning_effort"] == "high" + assert "reasoning_effort" not in result - def test_map_reasoning_effort_low(self): - """Test that reasoning_effort='low' enables thinking and normalizes to 'high'.""" + def test_map_reasoning_effort_low_non_v4(self): + """Non-V4: reasoning_effort='low' enables thinking but does NOT forward effort.""" non_default_params = {"reasoning_effort": "low"} optional_params = {} @@ -79,10 +82,10 @@ class TestDeepSeekThinkingParams: ) assert result["thinking"] == {"type": "enabled"} - assert result["reasoning_effort"] == "high" + assert "reasoning_effort" not in result - def test_map_reasoning_effort_high(self): - """Test that reasoning_effort='high' is passed through to DeepSeek V4.""" + def test_map_reasoning_effort_high_non_v4(self): + """Non-V4: reasoning_effort='high' enables thinking but does NOT forward effort.""" non_default_params = {"reasoning_effort": "high"} optional_params = {} @@ -93,29 +96,116 @@ class TestDeepSeekThinkingParams: drop_params=False, ) + assert result["thinking"] == {"type": "enabled"} + assert "reasoning_effort" not in result + + # --- reasoning_effort on V4 models (native support) --- + + def test_map_reasoning_effort_high_v4(self): + """V4: reasoning_effort='high' enables thinking AND forwards effort.""" + 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.v4_model, + drop_params=False, + ) + assert result["thinking"] == {"type": "enabled"} assert result["reasoning_effort"] == "high" - def test_map_reasoning_effort_max(self): - """Test that reasoning_effort='max' is passed through to DeepSeek V4.""" + def test_map_reasoning_effort_max_v4(self): + """V4: reasoning_effort='max' is forwarded.""" 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, + model=self.v4_model, 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' per DeepSeek V4 compat.""" + def test_map_reasoning_effort_xhigh_normalizes_to_max_v4(self): + """V4: reasoning_effort='xhigh' normalizes to 'max'.""" 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=self.v4_model, + drop_params=False, + ) + + assert result["thinking"] == {"type": "enabled"} + assert result["reasoning_effort"] == "max" + + def test_map_reasoning_effort_low_normalizes_to_high_v4(self): + """V4: reasoning_effort='low' normalizes to 'high'.""" + 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.v4_model, + drop_params=False, + ) + + assert result["thinking"] == {"type": "enabled"} + assert result["reasoning_effort"] == "high" + + def test_map_reasoning_effort_medium_normalizes_to_high_v4(self): + """V4: reasoning_effort='medium' normalizes to 'high'.""" + 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.v4_model, + drop_params=False, + ) + + assert result["thinking"] == {"type": "enabled"} + assert result["reasoning_effort"] == "high" + + # --- Combined thinking + reasoning_effort --- + + def test_thinking_and_reasoning_effort_both_forwarded_v4(self): + """V4: When both thinking and reasoning_effort are provided, + both are forwarded — effort is NOT silently 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=self.v4_model, + drop_params=False, + ) + + assert result["thinking"] == {"type": "enabled"} + assert result["reasoning_effort"] == "max" + + def test_thinking_takes_precedence_non_v4(self): + """Non-V4: When both are provided, thinking is set but + reasoning_effort is NOT forwarded (model doesn't support it).""" + 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, @@ -124,7 +214,9 @@ class TestDeepSeekThinkingParams: ) assert result["thinking"] == {"type": "enabled"} - assert result["reasoning_effort"] == "max" + assert "reasoning_effort" not in result + + # --- Edge cases --- def test_map_reasoning_effort_none_does_not_enable_thinking(self): """Test that reasoning_effort='none' does not enable thinking.""" @@ -139,6 +231,7 @@ class TestDeepSeekThinkingParams: ) assert "thinking" not in result + 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.""" @@ -154,24 +247,6 @@ class TestDeepSeekThinkingParams: 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"}} @@ -199,3 +274,17 @@ class TestDeepSeekThinkingParams: ) assert "thinking" not in result + + # --- V4 model detection --- + + def test_is_v4_model_positive(self): + """Test V4 model detection for various V4 model names.""" + assert DeepSeekChatConfig._is_v4_model("deepseek-v4-pro") is True + assert DeepSeekChatConfig._is_v4_model("deepseek-v4-flash") is True + assert DeepSeekChatConfig._is_v4_model("deepseek/deepseek-v4-pro") is True + + def test_is_v4_model_negative(self): + """Test V4 model detection rejects non-V4 models.""" + assert DeepSeekChatConfig._is_v4_model("deepseek-reasoner") is False + assert DeepSeekChatConfig._is_v4_model("deepseek-chat") is False + assert DeepSeekChatConfig._is_v4_model("deepseek-coder") is False