From ec4c331684955eb12a2b0a496bd970b28b7dc147 Mon Sep 17 00:00:00 2001 From: vrlo Date: Fri, 17 Jul 2026 16:20:49 +0200 Subject: [PATCH] fix(azure_ai): strip thinking_blocks and other output-only fields from chat messages AzureAIStudioConfig._transform_messages only converted content lists to strings and passed image/audio blocks through. It did not drop the output-only / Anthropic-only fields that LiteLLM's Anthropic /v1/messages adapter attaches to the OpenAI-format assistant messages it builds when replaying multi-turn history from an Anthropic-format client such as Claude Code. Those fields (thinking_blocks, reasoning_content, provider_specific_fields, and the Anthropic cache_control annotation) are not part of the OpenAI chat-completions message schema, so they reached Azure AI Foundry-hosted backends whose chat-message schema sets additionalProperties: false (for example Fireworks GLM) and triggered 400 Extra inputs are not permitted, field: 'messages[n].thinking_blocks'. Strip those fields from each message before the request is serialized, mirroring what the direct fireworks_ai provider (FireworksAIConfig._transform_messages_helper) and the mistral provider (MistralChatConfig._strip_output_only_fields) already do. The strip reuses the existing filter_value_from_dict helper for cache_control and pops the flat output-only fields directly. Regression tests cover both _transform_messages and the full transform_request path that the reported end-to-end scenario hits. --- litellm/llms/azure_ai/chat/transformation.py | 30 +++++++ .../chat/test_azure_ai_transformation.py | 79 +++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/litellm/llms/azure_ai/chat/transformation.py b/litellm/llms/azure_ai/chat/transformation.py index 27a98347087..ce12b56b2a4 100644 --- a/litellm/llms/azure_ai/chat/transformation.py +++ b/litellm/llms/azure_ai/chat/transformation.py @@ -11,6 +11,7 @@ from litellm._logging import verbose_logger from litellm.litellm_core_utils.prompt_templates.common_utils import ( _audio_or_image_in_message_content, convert_content_list_to_str, + filter_value_from_dict, ) from litellm.llms.azure.common_utils import BaseAzureLLM from litellm.llms.base_llm.chat.transformation import LiteLLMLoggingObj @@ -28,6 +29,34 @@ class AzureFoundryErrorStrings(str, enum.Enum): SET_EXTRA_PARAMETERS_TO_PASS_THROUGH = "Set extra-parameters to 'pass-through'" +_OUTPUT_ONLY_MESSAGE_FIELDS: Tuple[str, ...] = ( + "thinking_blocks", + "reasoning_content", + "provider_specific_fields", +) + + +def _strip_unsupported_message_fields(message: AllMessageValues) -> None: + """Remove input-forbidden fields from a chat message before it is sent to + an Azure AI Foundry hosted model. + + Azure AI Foundry fronts many OpenAI-compatible backends (Fireworks, + Mistral, xAI, ...) whose chat-message schemas set ``additionalProperties: + false``. LiteLLM-internal / output-only fields that the Anthropic + ``/v1/messages`` adapter attaches when replaying assistant turns from an + Anthropic-format client (e.g. Claude Code) are not part of that schema and + trigger ``400 Extra inputs are not permitted, field: + 'messages[n].'``. ``cache_control`` is an Anthropic prompt-caching + annotation with the same problem. ``thinking_blocks`` and + ``reasoning_content`` are output-only; ``provider_specific_fields`` carries + Anthropic thought signatures. None are valid OpenAI input, so drop them. + """ + filter_value_from_dict(cast(dict, message), "cache_control") + m = cast(dict, message) + for field in _OUTPUT_ONLY_MESSAGE_FIELDS: + m.pop(field, None) + + class AzureAIStudioConfig(OpenAIConfig): def get_supported_openai_params(self, model: str) -> List: model_supports_tool_choice = True # azure ai supports this by default @@ -171,6 +200,7 @@ class AzureAIStudioConfig(OpenAIConfig): 2. If message contains an image or audio, send as is (user-intended) """ for message in messages: + _strip_unsupported_message_fields(message) # Do nothing if the message contains an image or audio if _audio_or_image_in_message_content(message): continue diff --git a/tests/test_litellm/llms/azure_ai/chat/test_azure_ai_transformation.py b/tests/test_litellm/llms/azure_ai/chat/test_azure_ai_transformation.py index 2e75039139c..6ed6a604dc4 100644 --- a/tests/test_litellm/llms/azure_ai/chat/test_azure_ai_transformation.py +++ b/tests/test_litellm/llms/azure_ai/chat/test_azure_ai_transformation.py @@ -262,3 +262,82 @@ def test_drop_tool_level_extra_fields_strips_copilot_mcp_server_name(): assert "copilot_mcp_server_name" not in tool assert result["tools"][0]["type"] == "function" assert result["tools"][1]["function"]["name"] == "read_file" + + +def test_transform_messages_strips_anthropic_replay_fields(): + """Regression: Anthropic /v1/messages adapter attaches ``thinking_blocks`` + (and ``provider_specific_fields`` / ``cache_control``) to the OpenAI-format + assistant messages it builds when replaying Claude Code turns. Azure AI + Foundry hosted model backends with strict chat-message schemas (e.g. + Fireworks GLM) reject them with + ``Extra inputs are not permitted, field: 'messages[n].thinking_blocks'``. + ``_transform_messages`` must drop them before the request is sent. + """ + config = AzureAIStudioConfig() + messages = [ + {"role": "user", "content": "Read a file."}, + { + "role": "assistant", + "content": "I can help.", + "thinking_blocks": [ + { + "type": "thinking", + "thinking": "The user wants me to read a file.", + "signature": "", + "cache_control": {}, + } + ], + "reasoning_content": "internal reasoning", + "provider_specific_fields": {"thought_signature": "sig"}, + "cache_control": {"type": "ephemeral"}, + }, + {"role": "user", "content": "go ahead"}, + ] + + out = config._transform_messages(messages, model="glm-5.2") + + assert "thinking_blocks" not in out[1] + assert "reasoning_content" not in out[1] + assert "provider_specific_fields" not in out[1] + assert "cache_control" not in out[1] + assert out[1]["content"] == "I can help." + + +def test_transform_request_drops_thinking_blocks_for_fireworks_model(): + """End-to-end of the reported scenario: a Fireworks model deployed on Azure + AI Foundry receives a replayed assistant turn carrying ``thinking_blocks``. + The outgoing request payload built by ``transform_request`` must not carry + it, or the Fireworks backend 400s with + ``Extra inputs are not permitted, field: 'messages[2].thinking_blocks'``. + """ + config = AzureAIStudioConfig() + messages = [ + {"role": "user", "content": "Read a file."}, + { + "role": "assistant", + "content": "I can help.", + "thinking_blocks": [ + { + "type": "thinking", + "thinking": "The user wants me to read a file.", + "signature": "", + "cache_control": {}, + } + ], + }, + {"role": "user", "content": "go ahead"}, + ] + + payload = config.transform_request( + model="glm-5.2", + messages=messages, + optional_params={}, + litellm_params={}, + headers={}, + ) + + for message in payload["messages"]: + assert "thinking_blocks" not in message + assert "reasoning_content" not in message + assert "provider_specific_fields" not in message + assert "cache_control" not in message