diff --git a/litellm/llms/mistral/chat/transformation.py b/litellm/llms/mistral/chat/transformation.py index 0d9577669a4..f6733d2b27c 100644 --- a/litellm/llms/mistral/chat/transformation.py +++ b/litellm/llms/mistral/chat/transformation.py @@ -253,6 +253,8 @@ class MistralConfig(OpenAIGPTConfig): m = strip_none_values_from_message(m) # prevents 'extra_forbidden' error new_messages.append(m) + new_messages = self._strip_extra_fields(new_messages) + if is_async: return super()._transform_messages(new_messages, model, True) else: @@ -266,7 +268,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.""" @@ -275,6 +277,17 @@ 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) and m.get("role") == "assistant": + 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) 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 15694d9f218..0cc92c36e47 100644 --- a/tests/test_litellm/llms/mistral/test_mistral_chat_transformation.py +++ b/tests/test_litellm/llms/mistral/test_mistral_chat_transformation.py @@ -245,6 +245,53 @@ 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"}, + "reasoning_content": "some thinking", + }, + ) + ] + 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 + 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()