From 3c3081a502e74dae9c17f8317a8e54fc4fecac2f Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Wed, 4 Nov 2026 21:01:57 -0800 Subject: [PATCH] fix(mistral): leave tool_choice to the no-tools removal when no tools are sent An unusable tool_choice on a request with no tools answered 400, while the merge base and every other OpenAI-compatible route strip tool_choice from such a request and answer 200. Map tool_choice only when tools are present. --- litellm/llms/mistral/chat/transformation.py | 2 +- .../mistral/test_mistral_chat_transformation.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/litellm/llms/mistral/chat/transformation.py b/litellm/llms/mistral/chat/transformation.py index ec2402e5f0b..61d237de2d8 100644 --- a/litellm/llms/mistral/chat/transformation.py +++ b/litellm/llms/mistral/chat/transformation.py @@ -282,7 +282,7 @@ 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 == "tool_choice": + if param == "tool_choice" and "tools" in non_default_params and non_default_params["tools"]: match self._tool_choice_to_send( model, cast(object, value), # cast-ok: untyped request params, the match narrows it diff --git a/tests/unit/llms/mistral/test_mistral_chat_transformation.py b/tests/unit/llms/mistral/test_mistral_chat_transformation.py index 8769ba655ac..42673e17acf 100644 --- a/tests/unit/llms/mistral/test_mistral_chat_transformation.py +++ b/tests/unit/llms/mistral/test_mistral_chat_transformation.py @@ -2084,14 +2084,17 @@ class TestMistralToolChoice: ) def _request_body( - self, tool_choice: str | Mapping[str, object], drop_params: bool = False + self, + tool_choice: str | Mapping[str, object], + drop_params: bool = False, + tools: Sequence[ChatCompletionToolParam] | None = TOOLS, ) -> Mapping[str, object]: import litellm optional_params: Final = litellm.get_optional_params( model="mistral-large-latest", custom_llm_provider="mistral", - tools=list(self.TOOLS), + tools=None if tools is None else list(tools), tool_choice=tool_choice, drop_params=drop_params, ) @@ -2153,6 +2156,16 @@ class TestMistralToolChoice: with pytest.raises(litellm.UnsupportedParamsError, match="tool_choice"): self._request_body(tool_choice) + @pytest.mark.parametrize("tools", [None, ()]) + @pytest.mark.parametrize("tool_choice", UNUSABLE_TOOL_CHOICES) + def test_unusable_tool_choice_without_tools_is_not_sent( + self, + local_model_cost_map: None, + tool_choice: str | Mapping[str, object], + tools: Sequence[ChatCompletionToolParam] | None, + ) -> None: + assert "tool_choice" not in self._request_body(tool_choice, tools=tools) + @pytest.mark.parametrize("tool_choice", UNUSABLE_TOOL_CHOICES) 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