From d19d83925cea15ee6ff2c44b1cc29a1faf945909 Mon Sep 17 00:00:00 2001 From: factnn <166481866+factnn@users.noreply.github.com> Date: Sun, 21 Jun 2026 15:48:17 +0800 Subject: [PATCH 1/6] fix: strip extra fields from messages for Mistral chat Mistral API enforces additionalProperties: false on its message schema, rejecting extra fields like metadata, provider_specific_fields, thinking_blocks, and cache_control. Strip these before sending. Fixes #30882 --- litellm/llms/mistral/chat/transformation.py | 7 +++++ .../test_mistral_chat_transformation.py | 30 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/litellm/llms/mistral/chat/transformation.py b/litellm/llms/mistral/chat/transformation.py index 0f202a22c96..9de93ef94a6 100644 --- a/litellm/llms/mistral/chat/transformation.py +++ b/litellm/llms/mistral/chat/transformation.py @@ -265,6 +265,13 @@ class MistralConfig(OpenAIGPTConfig): if MistralConfig._is_empty_assistant_message(m): continue m = strip_none_values_from_message(m) # prevents 'extra_forbidden' error + # GH#30882: strip fields not permitted by Mistral + # (additionalProperties: false on their message schema) + if isinstance(m, dict): + m.pop("metadata", None) + m.pop("provider_specific_fields", None) + m.pop("thinking_blocks", None) + m.pop("cache_control", None) new_messages.append(m) if is_async: 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..3b54aa18a3c 100644 --- a/tests/test_litellm/llms/mistral/test_mistral_chat_transformation.py +++ b/tests/test_litellm/llms/mistral/test_mistral_chat_transformation.py @@ -249,6 +249,36 @@ class TestMistralReasoningSupport: assert result == messages assert len(result) == 1 + def test_transform_messages_strips_extra_fields(self): + """GH#30882: extra fields like metadata should be stripped + from messages before sending to Mistral API.""" + mistral_config = MistralConfig() + messages: List[AllMessageValues] = [ + cast( + AllMessageValues, + { + "role": "assistant", + "content": "hello", + "metadata": { + "tool_outputs_trimmed": True, + "trimmed_by": "async_context_compression", + }, + "provider_specific_fields": {"foo": "bar"}, + "thinking_blocks": [], + "cache_control": {"type": "ephemeral"}, + }, + ) + ] + result = mistral_config._transform_messages(messages, "mistral/mistral-large-latest") + assert len(result) == 1 + msg = cast(dict, result[0]) + assert msg["role"] == "assistant" + assert msg["content"] == "hello" + assert "metadata" not in msg + assert "provider_specific_fields" not in msg + assert "thinking_blocks" not in msg + assert "cache_control" not in msg + def test_transform_request_magistral_with_reasoning(self): """Test transform_request method for magistral model with reasoning.""" mistral_config = MistralConfig() From 159a5e47f74ead513c905e3848ed6c6e49903887 Mon Sep 17 00:00:00 2001 From: factnn <166481866+factnn@users.noreply.github.com> Date: Sun, 21 Jun 2026 15:52:28 +0800 Subject: [PATCH 2/6] fix: also strip extra fields in image/file message paths for Mistral _extract_fields helper now also runs in _transform_messages_sync and _transform_messages_async for the image/file early-return path, preventing the same "Extra inputs are not permitted" error when messages mix image/file content with extra fields. --- litellm/llms/mistral/chat/transformation.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/litellm/llms/mistral/chat/transformation.py b/litellm/llms/mistral/chat/transformation.py index 9de93ef94a6..ba44d9f2340 100644 --- a/litellm/llms/mistral/chat/transformation.py +++ b/litellm/llms/mistral/chat/transformation.py @@ -287,7 +287,7 @@ class MistralConfig(OpenAIGPTConfig): # and then apply Mistral-specific handling for files messages = await super()._transform_messages(messages, model, True) messages = self._handle_message_with_file(messages) - return messages + return self._strip_extra_fields(messages) def _transform_messages_sync(self, messages: List[AllMessageValues], model: str) -> List[AllMessageValues]: """Handle modification of messages for Mistral API in a sync context.""" @@ -296,6 +296,16 @@ class MistralConfig(OpenAIGPTConfig): # This is the sync version of the async method above messages = super()._transform_messages(messages, model, False) messages = self._handle_message_with_file(messages) + return self._strip_extra_fields(messages) + + @staticmethod + def _strip_extra_fields(messages: List[AllMessageValues]) -> List[AllMessageValues]: + for m in messages: + if isinstance(m, dict): + m.pop("metadata", None) + m.pop("provider_specific_fields", None) + m.pop("thinking_blocks", None) + m.pop("cache_control", None) return messages def _handle_message_with_file(self, messages: List[AllMessageValues]) -> List[AllMessageValues]: From d600d7a6685546f46fd97dafbec4c8c5b91a7bd2 Mon Sep 17 00:00:00 2001 From: factnn <166481866+factnn@users.noreply.github.com> Date: Sun, 21 Jun 2026 15:56:43 +0800 Subject: [PATCH 3/6] fix: also strip reasoning_content from Mistral messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #30835 — reasoning_content in assistant messages causes 422 extra_forbidden when forwarded to Mistral API. --- litellm/llms/mistral/chat/transformation.py | 2 ++ .../llms/mistral/test_mistral_chat_transformation.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/litellm/llms/mistral/chat/transformation.py b/litellm/llms/mistral/chat/transformation.py index ba44d9f2340..8daddde19de 100644 --- a/litellm/llms/mistral/chat/transformation.py +++ b/litellm/llms/mistral/chat/transformation.py @@ -272,6 +272,7 @@ class MistralConfig(OpenAIGPTConfig): m.pop("provider_specific_fields", None) m.pop("thinking_blocks", None) m.pop("cache_control", None) + m.pop("reasoning_content", None) new_messages.append(m) if is_async: @@ -306,6 +307,7 @@ class MistralConfig(OpenAIGPTConfig): m.pop("provider_specific_fields", None) m.pop("thinking_blocks", None) m.pop("cache_control", None) + m.pop("reasoning_content", None) return messages def _handle_message_with_file(self, messages: List[AllMessageValues]) -> List[AllMessageValues]: 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 3b54aa18a3c..c0ed90efda0 100644 --- a/tests/test_litellm/llms/mistral/test_mistral_chat_transformation.py +++ b/tests/test_litellm/llms/mistral/test_mistral_chat_transformation.py @@ -266,6 +266,7 @@ class TestMistralReasoningSupport: "provider_specific_fields": {"foo": "bar"}, "thinking_blocks": [], "cache_control": {"type": "ephemeral"}, + "reasoning_content": "some thinking", }, ) ] @@ -278,6 +279,7 @@ class TestMistralReasoningSupport: assert "provider_specific_fields" not in msg assert "thinking_blocks" not in msg assert "cache_control" not in msg + assert "reasoning_content" not in msg def test_transform_request_magistral_with_reasoning(self): """Test transform_request method for magistral model with reasoning.""" From ccfdc915cbc04838cf93416b4087596e924f7044 Mon Sep 17 00:00:00 2001 From: factnn <166481866+factnn@users.noreply.github.com> Date: Sun, 21 Jun 2026 15:58:29 +0800 Subject: [PATCH 4/6] fix: use list instead of typing.List for ruff UP006 --- litellm/llms/mistral/chat/transformation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/llms/mistral/chat/transformation.py b/litellm/llms/mistral/chat/transformation.py index 8daddde19de..7d5602acd2d 100644 --- a/litellm/llms/mistral/chat/transformation.py +++ b/litellm/llms/mistral/chat/transformation.py @@ -300,7 +300,7 @@ class MistralConfig(OpenAIGPTConfig): return self._strip_extra_fields(messages) @staticmethod - def _strip_extra_fields(messages: List[AllMessageValues]) -> List[AllMessageValues]: + def _strip_extra_fields(messages: list[AllMessageValues]) -> list[AllMessageValues]: for m in messages: if isinstance(m, dict): m.pop("metadata", None) From 657244be1578ab23c01032a53eadf8b1478d4662 Mon Sep 17 00:00:00 2001 From: factnn <166481866+factnn@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:34:46 +0800 Subject: [PATCH 5/6] fix: deduplicate extra-field stripping, use _strip_extra_fields in main loop --- litellm/llms/mistral/chat/transformation.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/litellm/llms/mistral/chat/transformation.py b/litellm/llms/mistral/chat/transformation.py index 7d5602acd2d..6e8f465da85 100644 --- a/litellm/llms/mistral/chat/transformation.py +++ b/litellm/llms/mistral/chat/transformation.py @@ -265,16 +265,10 @@ class MistralConfig(OpenAIGPTConfig): if MistralConfig._is_empty_assistant_message(m): continue m = strip_none_values_from_message(m) # prevents 'extra_forbidden' error - # GH#30882: strip fields not permitted by Mistral - # (additionalProperties: false on their message schema) - if isinstance(m, dict): - m.pop("metadata", None) - m.pop("provider_specific_fields", None) - m.pop("thinking_blocks", None) - m.pop("cache_control", None) - m.pop("reasoning_content", None) new_messages.append(m) + new_messages = self._strip_extra_fields(new_messages) + if is_async: return super()._transform_messages(new_messages, model, True) else: From 7c3c805154e1419baa02c00453d3a01bf70a045f Mon Sep 17 00:00:00 2001 From: factnn <166481866+factnn@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:51:50 +0800 Subject: [PATCH 6/6] fix: only strip extra fields from assistant messages in Mistral --- litellm/llms/mistral/chat/transformation.py | 2 +- .../mistral/test_mistral_chat_transformation.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/litellm/llms/mistral/chat/transformation.py b/litellm/llms/mistral/chat/transformation.py index 6e8f465da85..b21998aa273 100644 --- a/litellm/llms/mistral/chat/transformation.py +++ b/litellm/llms/mistral/chat/transformation.py @@ -296,7 +296,7 @@ class MistralConfig(OpenAIGPTConfig): @staticmethod def _strip_extra_fields(messages: list[AllMessageValues]) -> list[AllMessageValues]: for m in messages: - if isinstance(m, dict): + if isinstance(m, dict) and m.get("role") == "assistant": m.pop("metadata", None) m.pop("provider_specific_fields", None) m.pop("thinking_blocks", None) 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 c0ed90efda0..b03d7a39897 100644 --- a/tests/test_litellm/llms/mistral/test_mistral_chat_transformation.py +++ b/tests/test_litellm/llms/mistral/test_mistral_chat_transformation.py @@ -281,6 +281,21 @@ class TestMistralReasoningSupport: assert "cache_control" not in msg assert "reasoning_content" not in msg + def test_user_message_extra_fields_are_preserved(self): + """GH#30882: user messages should not be stripped of extra fields.""" + mistral_config = MistralConfig() + messages: List[AllMessageValues] = [ + cast( + AllMessageValues, + {"role": "user", "content": "Question?", "reasoning_content": "noise"}, + ) + ] + result = mistral_config._transform_messages(messages, "mistral/mistral-large-latest") + assert len(result) == 1 + msg = cast(dict, result[0]) + assert msg["role"] == "user" + assert msg["reasoning_content"] == "noise" + def test_transform_request_magistral_with_reasoning(self): """Test transform_request method for magistral model with reasoning.""" mistral_config = MistralConfig()