fix(mistral): send Magistral reasoning as supported native levels (#51329)

* fix(mistral): send reasoning_effort and thinking to magistral models as a level Mistral accepts

magistral-small-latest and magistral-medium-latest now take reasoning_effort
(none or high) as their reasoning switch, but LiteLLM dropped reasoning_effort
and thinking for any magistral model and injected Magistral's deprecated
reasoning system prompt instead, so streamed calls returned no reasoning and
"none" still reasoned.

Forward reasoning_effort rounded to the cost map's declared levels, map
thinking onto it (disabled becomes none), drop the system prompt, and declare
none and high on both magistral rows.

* fix(mistral): send adaptive thinking to magistral as a reasoning level

* fix(mistral): reject reasoning_effort strings that are not a level, and leave "default" to Mistral

A string such as "HIGH" or "auto" went to Mistral as sent and Mistral answered 422, even with drop_params. It now takes the same path as a number: dropped with a warning under drop_params, a 400 without it. "default", which litellm.completion lists as a value, sends no reasoning_effort so Mistral applies its own default.

* fix(mistral): preserve thinking when unsupported effort is dropped

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* refactor(mistral): raise reasoning failures at the public boundary

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(mistral): share fixed capabilities with warning regressions

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Mateo Wang 2026-11-14 00:33:54 -05:00 • committed by GitHub
parent 8decbe4801
commit ccb7f753c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 294 additions and 422 deletions

View file

@ -13,8 +13,8 @@ from typing import TYPE_CHECKING, Final, Literal, cast, get_type_hints, overload
from urllib.parse import urlparse
import httpx
from pydantic import TypeAdapter
from typing_extensions import TypeIs # noqa: TID251 # TypeIs reaches typing only on 3.13
from pydantic import JsonValue, TypeAdapter
from typing_extensions import TypeIs, assert_never # noqa: TID251 # these are absent from typing on Python 3.10
import litellm
from litellm._logging import verbose_logger
@ -28,10 +28,11 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import (
strip_none_values_from_message,
)
from litellm.litellm_core_utils.reasoning_effort_capability import (
REASONING_EFFORT_ADVERTISEMENT_ORDER,
declared_reasoning_efforts_for_model,
nearest_declared_reasoning_effort,
)
from litellm.litellm_core_utils.reasoning_effort_utils import OPAQUE_VALUE, warn_dropped_reasoning
from litellm.litellm_core_utils.reasoning_effort_utils import reasoning_effort_from_thinking, warn_dropped_reasoning
from litellm.litellm_core_utils.url_utils import ensure_api_route
from litellm.llms.openai.chat.gpt_transformation import (
OpenAIChatCompletionStreamingHandler,
@ -78,22 +79,29 @@ def _accepted_reasoning_effort(model: str, requested: str, custom_llm_provider:
return accepted
def _thinking_as_reasoning_effort(thinking: object, *, model: str, drop_params: bool) -> str | None:
match thinking:
case {"type": "disabled"}:
return "none"
case {"type": "adaptive"}:
return "medium"
case _:
return reasoning_effort_from_thinking(thinking, model=model, drop_params=drop_params)
_NO_TOOL_CALL_TURN: Final[tuple[int, frozenset[str]]] = (-1, frozenset())
_MISTRAL_REQUEST_ROUTING: Final = TypeAdapter(MistralRequestRouting)
_MISTRAL_CACHE_KEY_PARAMS: Final = TypeAdapter(MistralCacheKeyParams)
_MISTRAL_REASONING_EFFORT_PARAMS: Final = TypeAdapter(MistralReasoningEffortParams)
_MISTRAL_FILE_PART: Final = TypeAdapter(MistralFilePart)
_DECODED_JSON: Final = TypeAdapter(object)
_REASONING_PROMPT_REASON: Final = "litellm sends Magistral a reasoning system prompt instead of the requested level."
_NON_LEVEL_EFFORT_REASON: Final = (
"the Mistral API takes it only as a level such as 'high', so Mistral applies its default effort."
)
_REASONING_LEVELS: Final = frozenset(REASONING_EFFORT_ADVERTISEMENT_ORDER)
_PROVIDER_DEFAULT_EFFORT: Final = "default"
_NON_LEVEL_EFFORT_REASON: Final = "the Mistral API takes it only as a level such as 'high'."
_UNUSABLE_TOOL_CHOICE_REASON: Final = (
"the Mistral API takes tool_choice only as 'auto', 'none', 'any', 'required', "
"or {'type': 'function', 'function': {'name': <tool name>}}."
)
_DOCUMENTED_MAGISTRAL_EFFORTS: Final = frozenset({"low", "medium", "high"})
_REASONING_ON_THINKING_TYPES: Final = frozenset({"enabled", "adaptive"})
def _reasoning_level_and_summary(reasoning_effort: object) -> tuple[object, object]:
@ -103,20 +111,20 @@ def _reasoning_level_and_summary(reasoning_effort: object) -> tuple[object, obje
return requested.get("effort"), requested.get("summary")
def _warn_unless_reasoning_prompt_honors(param: str, value: object, model: str) -> None:
if param == "reasoning_effort" and isinstance(value, str) and value in _DOCUMENTED_MAGISTRAL_EFFORTS:
return
thinking: Final = as_str_mapping(value)
thinking_type: Final = thinking.get("type") if thinking is not None else None
if param == "thinking" and isinstance(thinking_type, str) and thinking_type in _REASONING_ON_THINKING_TYPES:
return
warn_dropped_reasoning(param, value, model, _REASONING_PROMPT_REASON, drop_params_gated=False)
def _is_json_array(value: object) -> TypeIs[Sequence[object]]: # guard-ok: a decoded JSON array is a list
return isinstance(value, list)
def _public_reasoning_level(result: str | UnsupportedParamsError | None) -> str | None:
match result:
case UnsupportedParamsError():
raise result
case str() | None:
return result
case _:
assert_never(result)
class MistralConfig(OpenAIGPTConfig):
"""
Reference: https://docs.mistral.ai/api/
@ -238,33 +246,6 @@ class MistralConfig(OpenAIGPTConfig):
model=model,
)
@staticmethod
def _get_mistral_reasoning_system_prompt() -> str:
"""
Returns the system prompt for Mistral reasoning models.
Based on Mistral's documentation: https://huggingface.co/mistralai/Magistral-Small-2506
Mistral recommends the following system prompt for reasoning:
"""
return """
<s>[SYSTEM_PROMPT]system_prompt
A user will ask you to solve a task. You should first draft your thinking process (inner monologue) until you have derived the final answer. Afterwards, write a self-contained summary of your thoughts (i.e. your summary should be succinct but contain all the critical steps you needed to reach the conclusion). You should use Markdown to format your response. Write both your thoughts and summary in the same language as the task posed by the user. NEVER use \boxed{} in your response.
Your thinking process must follow the template below:
<think>
Your thoughts or/and draft, like working through an exercise on scratch paper. Be as casual and as long as you want until you are confident to generate a correct answer.
</think>
Here, provide a concise summary that reflects your reasoning and presents a clear final answer to the user. Don't mention that this is a summary.
Problem:
[/SYSTEM_PROMPT][INST]user_message[/INST]<think>
reasoning_traces
</think>
assistant_response</s>[INST]user_message[/INST]
"""
def map_openai_params(
self,
non_default_params: dict,
@ -272,6 +253,15 @@ class MistralConfig(OpenAIGPTConfig):
model: str,
drop_params: bool,
) -> dict:
requested: Final = _MISTRAL_REASONING_EFFORT_PARAMS.validate_python(non_default_params)
reasoning_level: Final = (
_public_reasoning_level(self._reasoning_level_to_send(model, requested, drop_params))
if "reasoning_effort" in requested
else None
)
leaves_level_to_mistral: Final = (
_reasoning_level_and_summary(requested.get("reasoning_effort"))[0] == _PROVIDER_DEFAULT_EFFORT
)
for param, value in non_default_params.items():
if param == "max_tokens":
optional_params["max_tokens"] = value
@ -296,42 +286,42 @@ class MistralConfig(OpenAIGPTConfig):
optional_params["tool_choice"] = tool_choice
if param == "seed":
optional_params["extra_body"] = {"random_seed": value}
if param == "reasoning_effort" and reasoning_level is not None:
optional_params["reasoning_effort"] = reasoning_level
if (
param == "reasoning_effort"
and "magistral" not in model.lower()
and (
reasoning_level := self._reasoning_level_to_send(
model, _MISTRAL_REASONING_EFFORT_PARAMS.validate_python(non_default_params), drop_params
)
)
param == "thinking"
and reasoning_level is None
and not leaves_level_to_mistral
and (thinking_effort := _thinking_as_reasoning_effort(value, model=model, drop_params=drop_params))
is not None
):
optional_params["reasoning_effort"] = reasoning_level
if param in ("reasoning_effort", "thinking") and "magistral" in model.lower():
# Flag that we need to add reasoning system prompt
optional_params["_add_reasoning_prompt"] = True
_warn_unless_reasoning_prompt_honors(param, OPAQUE_VALUE.validate_python(value), model)
optional_params["reasoning_effort"] = _accepted_reasoning_effort(
model, thinking_effort, self.custom_llm_provider
)
if param in ("temperature", "top_p", "stop", "response_format", "parallel_tool_calls", "prompt_cache_key"):
optional_params[param] = value
return optional_params
def _reasoning_level_to_send(
self, model: str, requested: MistralReasoningEffortParams, drop_params: bool
) -> str | None:
) -> str | UnsupportedParamsError | None:
requested_effort: Final = requested.get("reasoning_effort")
level, summary = _reasoning_level_and_summary(requested_effort)
leaves_level_to_mistral: Final = level == _PROVIDER_DEFAULT_EFFORT
sendable: Final = (
_accepted_reasoning_effort(model, level, self.custom_llm_provider) if isinstance(level, str) else None
_accepted_reasoning_effort(model, level, self.custom_llm_provider)
if isinstance(level, str) and level in _REASONING_LEVELS
else None
)
if sendable is not None and summary is None:
if summary is None and (sendable is not None or leaves_level_to_mistral):
return sendable
if drop_params or litellm.drop_params is True:
if sendable is None and level is not None:
if sendable is None and level is not None and not leaves_level_to_mistral:
warn_dropped_reasoning(
"reasoning_effort", requested_effort, model, _NON_LEVEL_EFFORT_REASON, drop_params_gated=True
)
return sendable
raise UnsupportedParamsError(
return UnsupportedParamsError(
message=(
f"{self.custom_llm_provider} does not support parameters: ['reasoning_effort'], for model={model}, "
"since the Mistral API takes it only as a level such as 'high', with no reasoning summary. "
@ -545,55 +535,8 @@ class MistralConfig(OpenAIGPTConfig):
"""
return [self._with_mistral_file_ids(m) for m in messages]
def _add_reasoning_system_prompt_if_needed(
self, messages: list[AllMessageValues], optional_params: dict
) -> list[AllMessageValues]:
"""
Add reasoning system prompt for Mistral magistral models when reasoning_effort is specified.
"""
if not optional_params.get("_add_reasoning_prompt", False):
return messages
# Check if there's already a system message
has_system_message: Final = any(msg.get("role") == "system" for msg in messages)
if has_system_message:
# Prepend reasoning instructions to existing system message
for i, msg in enumerate(messages):
if msg.get("role") == "system":
existing_content = msg.get("content", "")
reasoning_prompt = self._get_mistral_reasoning_system_prompt()
# Handle both string and list content, preserving original format
if isinstance(existing_content, str):
# String content - prepend reasoning prompt
new_content: str | list = f"{reasoning_prompt}\n\n{existing_content}"
elif isinstance(existing_content, list):
# List content - prepend reasoning prompt as text block
new_content = [{"type": "text", "text": reasoning_prompt + "\n\n"}] + existing_content
else:
# Fallback for any other type - convert to string
new_content = f"{reasoning_prompt}\n\n{existing_content}"
messages[i] = cast(AllMessageValues, {**msg, "content": new_content})
break
else:
# Add new system message with reasoning instructions
reasoning_message: Final[AllMessageValues] = cast(
AllMessageValues,
{
"role": "system",
"content": self._get_mistral_reasoning_system_prompt(),
},
)
messages = [reasoning_message] + messages
# Remove the internal flag
optional_params.pop("_add_reasoning_prompt", None)
return messages
@classmethod
def _clean_tool_schema_for_mistral(cls, tools: list) -> list:
def _clean_tool_schema_for_mistral(cls, tools: list[JsonValue]) -> list[JsonValue]:
"""
Clean tool schemas to remove fields that cause issues with Mistral API.
@ -953,15 +896,10 @@ class MistralConfig(OpenAIGPTConfig):
) -> dict:
"""
Transform the overall request to be sent to the API.
For magistral models, adds reasoning system prompt when reasoning_effort is specified.
Returns:
dict: The transformed request. Sent as the body of the API call.
"""
# Add reasoning system prompt if needed (for magistral models)
if "magistral" in model.lower() and optional_params.get("_add_reasoning_prompt", False):
messages = self._add_reasoning_system_prompt_if_needed(messages, optional_params)
cache_key_params: Final = _MISTRAL_CACHE_KEY_PARAMS.validate_python(optional_params)
withheld_params: Final = (
("client_metadata", "prompt_cache_key")

View file

@ -44279,6 +44279,10 @@
"max_output_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 7.5e-06,
"reasoning_effort_levels": [
"none",
"high"
],
"source": "https://docs.mistral.ai/models/model-cards/mistral-medium-3-5-26-04",
"supports_assistant_prefill": true,
"supports_function_calling": true,
@ -44295,6 +44299,10 @@
"max_output_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 6e-07,
"reasoning_effort_levels": [
"none",
"high"
],
"source": "https://docs.mistral.ai/models/model-cards/mistral-small-4-0-26-03",
"supports_assistant_prefill": true,
"supports_function_calling": true,

View file

@ -44279,6 +44279,10 @@
"max_output_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 7.5e-06,
"reasoning_effort_levels": [
"none",
"high"
],
"source": "https://docs.mistral.ai/models/model-cards/mistral-medium-3-5-26-04",
"supports_assistant_prefill": true,
"supports_function_calling": true,
@ -44295,6 +44299,10 @@
"max_output_tokens": 262144,
"mode": "chat",
"output_cost_per_token": 6e-07,
"reasoning_effort_levels": [
"none",
"high"
],
"source": "https://docs.mistral.ai/models/model-cards/mistral-small-4-0-26-03",
"supports_assistant_prefill": true,
"supports_function_calling": true,

View file

@ -46,6 +46,39 @@ async def test_mistral_chat_transformation():
)
@pytest.fixture
def fixed_reasoning_capabilities(local_model_cost_map: None, monkeypatch: pytest.MonkeyPatch) -> None:
import litellm
declarations: Final = (
("mistral-medium-latest", ("none", "high")),
("mistral-small-latest", ("none", "high")),
("mistral-vibe-cli-latest", ("none", "high")),
("magistral-small-latest", ("none", "high")),
("magistral-medium-latest", ("none", "high")),
("zai-glm-5", ("none", "low", "high", "max")),
("zai-glm-5-2", ("none", "low", "medium", "high", "xhigh", "max")),
)
for model, levels in declarations:
monkeypatch.setitem(
litellm.model_cost,
f"mistral/{model}",
{
"litellm_provider": "mistral",
"mode": "chat",
"supports_reasoning": True,
"reasoning_effort_levels": levels,
},
)
for model in ("codestral-latest", "mistral-large-latest"):
monkeypatch.setitem(
litellm.model_cost,
f"mistral/{model}",
{"litellm_provider": "mistral", "mode": "chat", "supports_reasoning": False},
)
@pytest.mark.usefixtures("fixed_reasoning_capabilities")
class TestMistralReasoningSupport:
"""Test suite for Mistral Magistral reasoning functionality."""
@ -66,32 +99,6 @@ class TestMistralReasoningSupport:
assert "reasoning_effort" not in supported_params_normal
assert "thinking" not in supported_params_normal
def test_map_openai_params_reasoning_effort(self, local_model_cost_map):
"""Test that reasoning_effort parameter is properly mapped for magistral models."""
mistral_config = MistralConfig()
# Test reasoning_effort mapping for magistral model
optional_params = {}
result = mistral_config.map_openai_params(
non_default_params={"reasoning_effort": "low"},
optional_params=optional_params,
model="mistral/magistral-medium-2506",
drop_params=False,
)
assert result.get("_add_reasoning_prompt") is True
optional_params_normal = {}
result_normal = mistral_config.map_openai_params(
non_default_params={"reasoning_effort": "low"},
optional_params=optional_params_normal,
model="mistral/mistral-medium-latest",
drop_params=False,
)
assert "_add_reasoning_prompt" not in result_normal
assert result_normal["reasoning_effort"] == "high"
@pytest.mark.parametrize(
("model", "requested", "sent"),
[
@ -102,18 +109,28 @@ class TestMistralReasoningSupport:
("mistral-medium-latest", "xhigh", "high"),
("mistral-small-latest", "medium", "high"),
("mistral-vibe-cli-latest", "medium", "high"),
("magistral-small-latest", "high", "high"),
("magistral-small-latest", "none", "none"),
("magistral-small-latest", "low", "high"),
("magistral-small-latest", "medium", "high"),
("magistral-medium-latest", "minimal", "high"),
("magistral-medium-latest", "none", "none"),
("zai-glm-5", "none", "none"),
("zai-glm-5", "minimal", "low"),
("zai-glm-5", "medium", "high"),
("zai-glm-5", "xhigh", "max"),
("zai-glm-5-2", "medium", "medium"),
("zai-glm-5-2", "xhigh", "xhigh"),
("zai-glm-5-2", "max", "max"),
("zai-glm-5-2", "minimal", "low"),
],
)
def test_reasoning_effort_is_sent_as_a_level_the_model_accepts(self, local_model_cost_map, model, requested, sent):
def test_reasoning_effort_is_sent_as_a_level_the_model_accepts(
self, local_model_cost_map: None, model: str, requested: str, sent: str
) -> None:
import litellm
optional_params = litellm.get_optional_params(
optional_params: Final = litellm.get_optional_params(
model=model,
custom_llm_provider="mistral",
reasoning_effort=requested,
@ -128,10 +145,12 @@ class TestMistralReasoningSupport:
({"summary": "auto"}, True, {}),
],
)
def test_reasoning_effort_object_is_sent_as_its_level(self, local_model_cost_map, requested, drop_params, sent):
def test_reasoning_effort_object_is_sent_as_its_level(
self, local_model_cost_map: None, requested: object, drop_params: bool, sent: Mapping[str, str]
) -> None:
import litellm
optional_params = litellm.get_optional_params(
optional_params: Final = litellm.get_optional_params(
model="mistral-small-latest",
custom_llm_provider="mistral",
reasoning_effort=requested,
@ -140,7 +159,9 @@ class TestMistralReasoningSupport:
assert {key: optional_params[key] for key in ("reasoning_effort",) if key in optional_params} == sent
@pytest.mark.parametrize("requested", [{"effort": "high", "summary": "auto"}, {"summary": "auto"}])
def test_reasoning_effort_object_mistral_cannot_take_is_rejected(self, local_model_cost_map, requested):
def test_reasoning_effort_object_mistral_cannot_take_is_rejected(
self, local_model_cost_map: None, requested: object
) -> None:
import litellm
with pytest.raises(litellm.UnsupportedParamsError, match=r"\['reasoning_effort'\]"):
@ -151,8 +172,8 @@ class TestMistralReasoningSupport:
)
def test_reasoning_effort_is_forwarded_verbatim_when_the_map_declares_no_levels(
self, local_model_cost_map, monkeypatch
):
self, local_model_cost_map: None, monkeypatch: pytest.MonkeyPatch
) -> None:
import litellm
monkeypatch.setitem(
@ -160,14 +181,14 @@ class TestMistralReasoningSupport:
"mistral/undeclared-reasoner",
{"litellm_provider": "mistral", "mode": "chat", "supports_reasoning": True},
)
optional_params = litellm.get_optional_params(
optional_params: Final = litellm.get_optional_params(
model="undeclared-reasoner",
custom_llm_provider="mistral",
reasoning_effort="medium",
)
assert optional_params["reasoning_effort"] == "medium"
def test_reasoning_effort_stays_unsupported_for_non_reasoning_models(self):
def test_reasoning_effort_stays_unsupported_for_non_reasoning_models(self) -> None:
import litellm
with pytest.raises(litellm.UnsupportedParamsError):
@ -177,7 +198,7 @@ class TestMistralReasoningSupport:
reasoning_effort="high",
)
dropped = litellm.get_optional_params(
dropped: Final = litellm.get_optional_params(
model="codestral-latest",
custom_llm_provider="mistral",
reasoning_effort="high",
@ -199,213 +220,111 @@ class TestMistralReasoningSupport:
assert "client_metadata" not in request
assert request["temperature"] == 0.2
def test_map_openai_params_thinking(self):
"""Test that thinking parameter is properly mapped for magistral models."""
mistral_config = MistralConfig()
@pytest.mark.parametrize(
("thinking", "sent"),
[
({"type": "enabled", "budget_tokens": 1024}, "high"),
({"type": "enabled", "budget_tokens": 100}, "high"),
({"type": "disabled"}, "none"),
({"type": "adaptive"}, "high"),
({"type": "adaptive", "display": "summarized"}, "high"),
],
)
def test_magistral_sends_thinking_as_a_reasoning_effort_the_model_accepts(
self, local_model_cost_map: None, thinking: object, sent: str
) -> None:
import litellm
# Test thinking mapping for magistral model
optional_params = {}
result = mistral_config.map_openai_params(
non_default_params={"thinking": {"budget": 1000}},
optional_params=optional_params,
model="mistral/magistral-small-2506",
drop_params=False,
optional_params: Final = litellm.get_optional_params(
model="magistral-small-latest",
custom_llm_provider="mistral",
thinking=thinking,
)
assert optional_params["reasoning_effort"] == sent
assert "thinking" not in optional_params
@pytest.mark.parametrize("thinking_first", (False, True))
@pytest.mark.parametrize(
("requested", "thinking", "sent"),
(
("HIGH", {"type": "disabled"}, "none"),
(5, {"type": "disabled"}, "none"),
({"effort": None}, {"type": "disabled"}, "none"),
("HIGH", {"type": "enabled", "budget_tokens": 1024}, "high"),
("HIGH", {"type": "adaptive"}, "high"),
("high", {"type": "disabled"}, "high"),
({"effort": "high", "summary": "auto"}, {"type": "disabled"}, "high"),
("default", {"type": "disabled"}, None),
({"effort": "default", "summary": "auto"}, {"type": "disabled"}, None),
),
)
def test_magistral_uses_thinking_only_when_effort_was_dropped(
self,
local_model_cost_map: None,
requested: object,
thinking: Mapping[str, object],
sent: str | None,
thinking_first: bool,
) -> None:
params: Final = (
{"thinking": thinking, "reasoning_effort": requested}
if thinking_first
else {"reasoning_effort": requested, "thinking": thinking}
)
result: Final = MistralConfig().map_openai_params(
non_default_params=params,
optional_params={},
model="magistral-small-latest",
drop_params=True,
)
expected: Final = {} if sent is None else {"reasoning_effort": sent}
assert result == expected
def test_magistral_prefers_reasoning_effort_over_thinking(self, local_model_cost_map: None) -> None:
import litellm
optional_params: Final = litellm.get_optional_params(
model="magistral-small-latest",
custom_llm_provider="mistral",
reasoning_effort="none",
thinking={"type": "enabled", "budget_tokens": 4096},
)
assert optional_params["reasoning_effort"] == "none"
def test_magistral_thinking_without_a_type_sends_no_reasoning_effort(self, local_model_cost_map: None) -> None:
import litellm
optional_params: Final = litellm.get_optional_params(
model="magistral-small-latest",
custom_llm_provider="mistral",
thinking={"budget_tokens": 4096},
)
assert "reasoning_effort" not in optional_params
assert "thinking" not in optional_params
@pytest.mark.parametrize(
"reasoning", [{"reasoning_effort": "medium"}, {"thinking": {"type": "enabled", "budget_tokens": 1024}}]
)
def test_magistral_request_carries_reasoning_effort_and_the_callers_messages_only(
self, local_model_cost_map: None, reasoning: Mapping[str, object]
) -> None:
import litellm
messages: Final[list[AllMessageValues]] = [{"role": "user", "content": "What is 17*23?"}]
optional_params: Final = litellm.get_optional_params(
model="magistral-small-latest", custom_llm_provider="mistral", **reasoning
)
assert result.get("_add_reasoning_prompt") is True
def test_get_mistral_reasoning_system_prompt(self):
"""Test that the reasoning system prompt is properly formatted."""
prompt = MistralConfig._get_mistral_reasoning_system_prompt()
assert isinstance(prompt, str)
assert len(prompt) > 50 # Ensure it's not empty
def test_add_reasoning_system_prompt_no_existing_system_message(self):
"""Test adding reasoning system prompt when no system message exists."""
mistral_config = MistralConfig()
messages = [{"role": "user", "content": "What is 2+2?"}]
optional_params = {"_add_reasoning_prompt": True}
result = mistral_config._add_reasoning_system_prompt_if_needed(messages, optional_params)
# Should add a new system message at the beginning
assert len(result) == 2
assert result[0]["role"] == "system"
assert "<think>" in result[0]["content"]
assert result[1]["role"] == "user"
assert result[1]["content"] == "What is 2+2?"
# Should remove the internal flag
assert "_add_reasoning_prompt" not in optional_params
def test_add_reasoning_system_prompt_with_existing_system_message(self):
"""Test adding reasoning system prompt when system message already exists."""
mistral_config = MistralConfig()
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is 2+2?"},
]
optional_params = {"_add_reasoning_prompt": True}
result = mistral_config._add_reasoning_system_prompt_if_needed(messages, optional_params)
# Should modify existing system message
assert len(result) == 2
assert result[0]["role"] == "system"
assert "<think>" in result[0]["content"]
assert "You are a helpful assistant." in result[0]["content"]
assert result[1]["role"] == "user"
# Should remove the internal flag
assert "_add_reasoning_prompt" not in optional_params
def test_add_reasoning_system_prompt_with_existing_list_content(self):
"""Test adding reasoning system prompt when system message has list content."""
mistral_config = MistralConfig()
messages = [
{
"role": "system",
"content": [
{"type": "text", "text": "You are a helpful assistant."},
{
"type": "text",
"text": "You always provide detailed explanations.",
},
],
},
{"role": "user", "content": "What is 2+2?"},
]
optional_params = {"_add_reasoning_prompt": True}
result = mistral_config._add_reasoning_system_prompt_if_needed(messages, optional_params)
# Should modify existing system message preserving list format
assert len(result) == 2
assert result[0]["role"] == "system"
assert isinstance(result[0]["content"], list)
# First item should be the reasoning prompt
assert result[0]["content"][0]["type"] == "text"
assert "<think>" in result[0]["content"][0]["text"]
# Original content should be preserved
assert "You are a helpful assistant." in result[0]["content"][1]["text"]
assert "You always provide detailed explanations." in result[0]["content"][2]["text"]
assert result[1]["role"] == "user"
# Should remove the internal flag
assert "_add_reasoning_prompt" not in optional_params
def test_add_reasoning_system_prompt_preserves_content_types(self):
"""Test that reasoning prompt preserves original content types (string vs list)."""
mistral_config = MistralConfig()
# Test with string content
string_messages = [
{"role": "system", "content": "You are helpful."},
{"role": "user", "content": "Hello"},
]
string_params = {"_add_reasoning_prompt": True}
string_result = mistral_config._add_reasoning_system_prompt_if_needed(string_messages, string_params)
assert isinstance(string_result[0]["content"], str)
assert "<think>" in string_result[0]["content"]
assert "You are helpful." in string_result[0]["content"]
# Test with list content
list_messages = [
{
"role": "system",
"content": [{"type": "text", "text": "You are helpful."}],
},
{"role": "user", "content": "Hello"},
]
list_params = {"_add_reasoning_prompt": True}
list_result = mistral_config._add_reasoning_system_prompt_if_needed(list_messages, list_params)
assert isinstance(list_result[0]["content"], list)
assert list_result[0]["content"][0]["type"] == "text"
assert "<think>" in list_result[0]["content"][0]["text"]
assert "You are helpful." in list_result[0]["content"][1]["text"]
def test_add_reasoning_system_prompt_no_flag(self):
"""Test that no modification happens when _add_reasoning_prompt flag is not set."""
mistral_config = MistralConfig()
messages = [{"role": "user", "content": "What is 2+2?"}]
optional_params = {}
result = mistral_config._add_reasoning_system_prompt_if_needed(messages, optional_params)
# Should return messages unchanged
assert result == messages
assert len(result) == 1
def test_transform_request_magistral_with_reasoning(self):
"""Test transform_request method for magistral model with reasoning."""
mistral_config = MistralConfig()
messages = [{"role": "user", "content": "What is 15 * 7?"}]
optional_params = {"_add_reasoning_prompt": True}
result = mistral_config.transform_request(
model="mistral/magistral-medium-2506",
request: Final = MistralConfig().transform_request(
model="magistral-small-latest",
messages=messages,
optional_params=optional_params,
litellm_params={},
headers={},
)
# Should have added system message
assert len(result["messages"]) == 2
assert result["messages"][0]["role"] == "system"
assert "<think>" in result["messages"][0]["content"]
assert result["messages"][1]["role"] == "user"
# Should remove internal flag from optional_params
assert "_add_reasoning_prompt" not in result
def test_transform_request_magistral_without_reasoning(self):
"""Test transform_request method for magistral model without reasoning."""
mistral_config = MistralConfig()
messages = [{"role": "user", "content": "What is 15 * 7?"}]
optional_params = {}
result = mistral_config.transform_request(
model="mistral/magistral-medium-2506",
messages=messages,
optional_params=optional_params,
litellm_params={},
headers={},
)
# Should not modify messages
assert len(result["messages"]) == 1
assert result["messages"][0]["role"] == "user"
def test_transform_request_non_magistral_with_reasoning_params(self):
"""Test that non-magistral models ignore reasoning parameters."""
mistral_config = MistralConfig()
messages = [{"role": "user", "content": "What is 15 * 7?"}]
optional_params = {"_add_reasoning_prompt": True}
result = mistral_config.transform_request(
model="mistral/mistral-large-latest",
messages=messages,
optional_params=optional_params,
litellm_params={},
headers={},
)
# Should not add system message for non-magistral models
assert len(result["messages"]) == 1
assert result["messages"][0]["role"] == "user"
assert request["reasoning_effort"] == "high"
assert request["messages"] == [{"role": "user", "content": "What is 17*23?"}]
def test_case_insensitive_magistral_detection(self):
"""Test that magistral model detection is case-insensitive."""
@ -423,42 +342,6 @@ class TestMistralReasoningSupport:
supported_params = mistral_config.get_supported_openai_params(model)
assert "reasoning_effort" in supported_params, f"Failed for model: {model}"
def test_end_to_end_reasoning_workflow(self):
"""Test the complete workflow from parameter to system prompt injection."""
mistral_config = MistralConfig()
# Step 1: Map parameters
optional_params = {}
mapped_params = mistral_config.map_openai_params(
non_default_params={"reasoning_effort": "high", "temperature": 0.7},
optional_params=optional_params,
model="mistral/magistral-medium-2506",
drop_params=False,
)
assert mapped_params.get("_add_reasoning_prompt") is True
assert mapped_params.get("temperature") == 0.7
# Step 2: Transform request
messages = [{"role": "user", "content": "Solve for x: 2x + 5 = 13"}]
result = mistral_config.transform_request(
model="mistral/magistral-medium-2506",
messages=messages,
optional_params=mapped_params,
litellm_params={},
headers={},
)
# Verify final result
assert len(result["messages"]) == 2
assert result["messages"][0]["role"] == "system"
assert "<think>" in result["messages"][0]["content"]
assert result["messages"][1]["role"] == "user"
assert result["messages"][1]["content"] == "Solve for x: 2x + 5 = 13"
assert result.get("temperature") == 0.7
assert "_add_reasoning_prompt" not in result
def test_mistral_streaming_chunk_preserves_thinking_blocks():
"""Ensure streaming chunks keep magistral reasoning content."""
@ -1716,16 +1599,17 @@ def _warnings_naming(caplog: pytest.LogCaptureFixture, param: str) -> tuple[str,
@pytest.mark.parametrize(
"param, value",
"param, value, sent",
(
("reasoning_effort", "none"),
("reasoning_effort", "xhigh"),
("thinking", {"type": "disabled"}),
("thinking", {"type": ["enabled"]}),
("reasoning_effort", "none", "none"),
("reasoning_effort", "xhigh", "high"),
("thinking", {"type": "disabled"}, "none"),
("thinking", {"type": "enabled", "budget_tokens": 1024}, "high"),
("thinking", {"type": "adaptive"}, "high"),
),
)
def test_magistral_reasoning_request_replaced_by_prompt_warns_once(
param: str, value: object, caplog: pytest.LogCaptureFixture
def test_magistral_reasoning_request_is_sent_as_a_level_without_a_drop_warning(
fixed_reasoning_capabilities: None, param: str, value: object, sent: str, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
result: Final = MistralConfig().map_openai_params(
@ -1735,36 +1619,27 @@ def test_magistral_reasoning_request_replaced_by_prompt_warns_once(
drop_params=False,
)
warnings: Final = _warnings_naming(caplog, param)
assert result["_add_reasoning_prompt"] is True
assert len(warnings) == 1
assert "magistral-medium-latest" in warnings[0]
@pytest.mark.parametrize(
"param, value",
(
("reasoning_effort", "low"),
("reasoning_effort", "medium"),
("reasoning_effort", "high"),
("thinking", {"type": "enabled", "budget_tokens": 1024}),
),
)
def test_magistral_reasoning_request_the_prompt_honors_does_not_warn(
param: str, value: object, caplog: pytest.LogCaptureFixture
) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
result: Final = MistralConfig().map_openai_params(
non_default_params={param: value},
optional_params={},
model="magistral-medium-latest",
drop_params=False,
)
assert result["_add_reasoning_prompt"] is True
assert result["reasoning_effort"] == sent
assert _warnings_naming(caplog, param) == ()
def test_magistral_thinking_budget_dropped_under_drop_params_warns_once_naming_the_model(
caplog: pytest.LogCaptureFixture,
) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
result: Final = MistralConfig().map_openai_params(
non_default_params={"thinking": {"type": "enabled", "budget_tokens": "lots"}},
optional_params={},
model="magistral-small-latest",
drop_params=True,
)
warnings: Final = _warnings_naming(caplog, "thinking.budget_tokens")
assert "reasoning_effort" not in result
assert len(warnings) == 1
assert "model='magistral-small-latest'" in warnings[0]
def test_non_magistral_reasoning_effort_is_sent_without_warning(caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
result: Final = MistralConfig().map_openai_params(
@ -1778,7 +1653,9 @@ def test_non_magistral_reasoning_effort_is_sent_without_warning(caplog: pytest.L
assert _warnings_naming(caplog, "reasoning_effort") == ()
@pytest.mark.parametrize("reasoning_effort", [5, {"effort": 5}, {"effort": 5, "summary": "auto"}])
@pytest.mark.parametrize(
"reasoning_effort", [5, {"effort": 5}, {"effort": 5, "summary": "auto"}, "HIGH", "auto", "", {"effort": "HIGH"}]
)
def test_mistral_warns_once_when_drop_params_drops_a_level_it_cannot_send(
caplog: pytest.LogCaptureFixture, reasoning_effort: object
) -> None:
@ -1797,7 +1674,10 @@ def test_mistral_warns_once_when_drop_params_drops_a_level_it_cannot_send(
assert "(drop_params=True)" in warnings[0]
@pytest.mark.parametrize("reasoning_effort", [{"summary": "auto"}, {"effort": "high", "summary": "auto"}])
@pytest.mark.parametrize(
"reasoning_effort",
[{"summary": "auto"}, {"effort": "high", "summary": "auto"}, {"effort": "default", "summary": "auto"}],
)
def test_mistral_stays_quiet_under_drop_params_when_no_level_is_lost(
caplog: pytest.LogCaptureFixture, reasoning_effort: object
) -> None:
@ -1812,6 +1692,44 @@ def test_mistral_stays_quiet_under_drop_params_when_no_level_is_lost(
assert _warnings_naming(caplog, "reasoning_effort") == ()
@pytest.mark.parametrize("reasoning_effort", ["HIGH", "auto", "", {"effort": "HIGH"}])
def test_magistral_rejects_a_reasoning_effort_that_is_not_a_level(
local_model_cost_map: None, reasoning_effort: object
) -> None:
import litellm
with pytest.raises(litellm.UnsupportedParamsError, match=r"\['reasoning_effort'\]"):
litellm.get_optional_params(
model="magistral-small-latest",
custom_llm_provider="mistral",
reasoning_effort=reasoning_effort,
)
@pytest.mark.parametrize("drop_params", [False, True])
@pytest.mark.parametrize("reasoning_effort", ["default", {"effort": "default"}])
@pytest.mark.parametrize("model", ["magistral-small-latest", "mistral-medium-latest"])
def test_default_reasoning_effort_leaves_the_level_to_mistral(
local_model_cost_map: None,
caplog: pytest.LogCaptureFixture,
model: str,
reasoning_effort: object,
drop_params: bool,
) -> None:
import litellm
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
optional_params: Final = litellm.get_optional_params(
model=model,
custom_llm_provider="mistral",
reasoning_effort=reasoning_effort,
drop_params=drop_params,
)
assert "reasoning_effort" not in optional_params
assert _warnings_naming(caplog, "reasoning_effort") == ()
@pytest.mark.parametrize(
("model", "custom_llm_provider"),
[