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.
This commit is contained in:
vrlo 2026-07-17 16:20:49 +02:00
parent bd44c9e305
commit ec4c331684
2 changed files with 109 additions and 0 deletions

View file

@ -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].<field>'``. ``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

View file

@ -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