diff --git a/litellm/llms/mistral/chat/transformation.py b/litellm/llms/mistral/chat/transformation.py index 0d9577669a4..92d7b7dc884 100644 --- a/litellm/llms/mistral/chat/transformation.py +++ b/litellm/llms/mistral/chat/transformation.py @@ -24,7 +24,7 @@ from litellm.secret_managers.main import get_secret_str from litellm.types.llms.mistral import MistralThinkingBlock, MistralToolCallMessage from litellm.types.llms.openai import AllMessageValues from litellm.types.utils import ModelResponse, ModelResponseStream -from litellm.utils import convert_to_model_response_object +from litellm.utils import convert_to_model_response_object, supports_reasoning class MistralConfig(OpenAIGPTConfig): @@ -98,12 +98,26 @@ class MistralConfig(OpenAIGPTConfig): "parallel_tool_calls", ] - # Add reasoning support for magistral models - if "magistral" in model.lower(): + if self._is_magistral_model(model): supported_params.extend(["thinking", "reasoning_effort"]) + elif self._supports_native_reasoning_effort(model): + supported_params.append("reasoning_effort") return supported_params + @staticmethod + def _is_magistral_model(model: str) -> bool: + return "magistral" in model.lower() + + @staticmethod + def _supports_native_reasoning_effort(model: str) -> bool: + """ + Mistral's hybrid reasoning models (mistral-medium-3-5 and newer) take ``reasoning_effort`` + directly on /chat/completions, so it is forwarded as-is instead of being emulated with the + magistral system prompt: https://docs.mistral.ai/capabilities/reasoning + """ + return supports_reasoning(model=model, custom_llm_provider="mistral") + def _map_tool_choice(self, tool_choice: str) -> str: if tool_choice == "auto" or tool_choice == "none": return tool_choice @@ -168,10 +182,12 @@ class MistralConfig(OpenAIGPTConfig): optional_params["extra_body"] = {"random_seed": value} if param == "response_format": optional_params["response_format"] = value - if param == "reasoning_effort" and "magistral" in model.lower(): - # Flag that we need to add reasoning system prompt - optional_params["_add_reasoning_prompt"] = True - if param == "thinking" and "magistral" in model.lower(): + if param == "reasoning_effort" and value is not None: + if self._is_magistral_model(model): + optional_params["_add_reasoning_prompt"] = True + elif self._supports_native_reasoning_effort(model): + optional_params["reasoning_effort"] = value + if param == "thinking" and self._is_magistral_model(model): # Flag that we need to add reasoning system prompt optional_params["_add_reasoning_prompt"] = True if param == "parallel_tool_calls": diff --git a/tests/test_litellm/llms/mistral/test_mistral_chat_transformation.py b/tests/test_litellm/llms/mistral/test_mistral_chat_transformation.py index 7a3f372582f..46ff3d15ff2 100644 --- a/tests/test_litellm/llms/mistral/test_mistral_chat_transformation.py +++ b/tests/test_litellm/llms/mistral/test_mistral_chat_transformation.py @@ -311,6 +311,48 @@ class TestMistralReasoningSupport: assert len(result["messages"]) == 1 assert result["messages"][0]["role"] == "user" + @pytest.mark.parametrize( + "model", + ["mistral/mistral-medium-3-5", "mistral-medium-3-5", "mistral/mistral-medium-latest"], + ) + def test_native_reasoning_model_supports_reasoning_effort(self, model): + """Hybrid reasoning models take reasoning_effort natively, see https://github.com/BerriAI/litellm/issues/36407""" + supported_params = MistralConfig().get_supported_openai_params(model) + + assert "reasoning_effort" in supported_params + assert "thinking" not in supported_params + + def test_native_reasoning_effort_forwarded_to_request_body(self): + mistral_config = MistralConfig() + + optional_params = mistral_config.map_openai_params( + non_default_params={"reasoning_effort": "high"}, + optional_params={}, + model="mistral/mistral-medium-3-5", + drop_params=False, + ) + assert optional_params == {"reasoning_effort": "high"} + + request = mistral_config.transform_request( + model="mistral/mistral-medium-3-5", + messages=[{"role": "user", "content": "What is 15 * 7?"}], + optional_params=optional_params, + litellm_params={}, + headers={}, + ) + assert request["reasoning_effort"] == "high" + assert len(request["messages"]) == 1 + + def test_magistral_reasoning_effort_stays_prompt_based(self): + optional_params = MistralConfig().map_openai_params( + non_default_params={"reasoning_effort": "high"}, + optional_params={}, + model="mistral/magistral-medium-2506", + drop_params=False, + ) + + assert optional_params == {"_add_reasoning_prompt": True} + def test_case_insensitive_magistral_detection(self): """Test that magistral model detection is case-insensitive.""" mistral_config = MistralConfig()