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.
This commit is contained in:
mateo-berri 2026-11-06 16:55:11 -08:00
parent c3118e96ee
commit 9eea89f700
2 changed files with 54 additions and 5 deletions

View file

@ -27,6 +27,7 @@ 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,
)
@ -92,6 +93,8 @@ _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)
_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', so Mistral applies its default effort."
)
@ -294,13 +297,16 @@ class MistralConfig(OpenAIGPTConfig):
) -> str | 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
)

View file

@ -1556,7 +1556,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:
@ -1575,7 +1577,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:
@ -1590,6 +1595,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"),
[