fix(mistral): warn when drop_params drops an unusable tool_choice

Return the unsupported tool_choice error as a value and raise it in
map_openai_params, so the helper models the failure and the caller owns
the exception. A tool_choice dropped under drop_params now logs a
warning naming the value and why Mistral cannot take it.
This commit is contained in:
mateo-berri 2026-11-04 20:25:13 -08:00
parent f8a9da87e0
commit 6de3708969
2 changed files with 44 additions and 30 deletions

View file

@ -6,6 +6,7 @@ Why separate file? Make it easy to see how transformation works
Docs - https://docs.mistral.ai/api/
"""
import reprlib
from collections.abc import AsyncIterator, Coroutine, Iterator, Mapping, Sequence
from itertools import accumulate, count
from typing import TYPE_CHECKING, Final, Literal, cast, get_type_hints, overload
@ -85,6 +86,10 @@ _REASONING_PROMPT_REASON: Final = "litellm sends Magistral a reasoning system pr
_NON_LEVEL_EFFORT_REASON: Final = (
"the Mistral API takes it only as a level such as 'high', so Mistral applies its default effort."
)
_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"})
@ -214,15 +219,19 @@ class MistralConfig(OpenAIGPTConfig):
def _tool_choice_to_send(
self, model: str, tool_choice: object, drop_params: bool
) -> str | ChatCompletionToolChoiceObjectParam | None:
) -> str | ChatCompletionToolChoiceObjectParam | UnsupportedParamsError | None:
mapped: Final = self._map_tool_choice(tool_choice)
if mapped is not None or drop_params or litellm.drop_params is True:
if mapped is not None:
return mapped
raise UnsupportedParamsError(
if drop_params or litellm.drop_params is True:
warn_dropped_reasoning(
"tool_choice", tool_choice, model, _UNUSABLE_TOOL_CHOICE_REASON, drop_params_gated=True
)
return None
return UnsupportedParamsError(
message=(
f"{self.custom_llm_provider} does not support tool_choice={tool_choice!r}, for model={model}, "
"since the Mistral API takes only 'auto', 'none', 'any', 'required', or "
"{'type': 'function', 'function': {'name': <tool name>}}. "
f"Unsupported tool_choice={reprlib.repr(tool_choice)} for model={model}, since "
f"{_UNUSABLE_TOOL_CHOICE_REASON} "
"To drop it, set `litellm.drop_params=True` or for proxy:\n\n`litellm_settings:\n drop_params: true`\n"
),
llm_provider=self.custom_llm_provider,
@ -273,28 +282,20 @@ class MistralConfig(OpenAIGPTConfig):
optional_params["tools"] = self._clean_tool_schema_for_mistral(value)
if param == "stream" and value is True:
optional_params["stream"] = value
if param == "temperature":
optional_params["temperature"] = value
if param == "top_p":
optional_params["top_p"] = value
if param == "stop":
optional_params["stop"] = value
if (
param == "tool_choice"
and (
mapped_tool_choice := self._tool_choice_to_send(
model,
cast(object, value), # cast-ok: untyped request params, the match narrows it
drop_params,
)
)
is not None
):
optional_params["tool_choice"] = mapped_tool_choice
if param == "tool_choice":
match self._tool_choice_to_send(
model,
cast(object, value), # cast-ok: untyped request params, the match narrows it
drop_params,
):
case UnsupportedParamsError() as unusable_tool_choice:
raise unusable_tool_choice
case None:
pass
case tool_choice:
optional_params["tool_choice"] = tool_choice
if param == "seed":
optional_params["extra_body"] = {"random_seed": value}
if param == "response_format":
optional_params["response_format"] = value
if (
param == "reasoning_effort"
and "magistral" not in model.lower()
@ -310,7 +311,7 @@ class MistralConfig(OpenAIGPTConfig):
# 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)
if param in ("parallel_tool_calls", "prompt_cache_key"):
if param in ("temperature", "top_p", "stop", "response_format", "parallel_tool_calls", "prompt_cache_key"):
optional_params[param] = value
return optional_params

View file

@ -2154,7 +2154,20 @@ class TestMistralToolChoice:
self._request_body(tool_choice)
@pytest.mark.parametrize("tool_choice", UNUSABLE_TOOL_CHOICES)
def test_unusable_tool_choice_is_dropped_with_drop_params(
self, local_model_cost_map: None, tool_choice: str | Mapping[str, object]
def test_unusable_tool_choice_is_dropped_with_a_warning_under_drop_params(
self, local_model_cost_map: None, tool_choice: str | Mapping[str, object], caplog: pytest.LogCaptureFixture
) -> None:
assert "tool_choice" not in self._request_body(tool_choice, drop_params=True)
with caplog.at_level(logging.WARNING, logger=verbose_logger.name):
body: Final = self._request_body(tool_choice, drop_params=True)
assert "tool_choice" not in body
assert len(_warnings_naming(caplog, "tool_choice")) == 1
def test_unusable_tool_choice_is_dropped_under_proxy_wide_drop_params(
self, local_model_cost_map: None, monkeypatch: pytest.MonkeyPatch
) -> None:
import litellm
monkeypatch.setattr(litellm, "drop_params", True)
assert "tool_choice" not in self._request_body({"type": "function", "function": {}})