From 9c2f9e95c1cabe861d3031cb8479ad6b611f5901 Mon Sep 17 00:00:00 2001 From: Christian Sidak Date: Thu, 9 Apr 2026 19:51:25 -0700 Subject: [PATCH] fix(azure_ai): strip max_retries from Azure AI Anthropic /v1/messages request body (#18746) Azure AI Foundry's Anthropic endpoint rejects max_retries, extra_body, and stream_options with "Extra inputs are not permitted". These params can leak into the request body from the router's default litellm params. The chat/completions path already strips them (transformation.py); this applies the same fix to the /v1/messages path. Fixes https://github.com/BerriAI/litellm/issues/18746 --- .../anthropic/messages_transformation.py | 7 ++ ...azure_anthropic_messages_transformation.py | 69 ++++++++++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/litellm/llms/azure_ai/anthropic/messages_transformation.py b/litellm/llms/azure_ai/anthropic/messages_transformation.py index 59d8fb02c6d..377b5f20b02 100644 --- a/litellm/llms/azure_ai/anthropic/messages_transformation.py +++ b/litellm/llms/azure_ai/anthropic/messages_transformation.py @@ -1,6 +1,7 @@ """ Azure Anthropic messages transformation config - extends AnthropicMessagesConfig with Azure authentication """ + from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( @@ -163,4 +164,10 @@ class AzureAnthropicMessagesConfig(AnthropicMessagesConfig): headers=headers, ) self._remove_scope_from_cache_control(anthropic_messages_request) + + # Remove parameters that Azure AI Foundry's Anthropic endpoint rejects. + # These may leak into the request body from the router's default litellm + # params or from the proxy layer. + for unsupported_param in ("max_retries", "extra_body", "stream_options"): + anthropic_messages_request.pop(unsupported_param, None) return anthropic_messages_request diff --git a/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py index 83653bc037b..90659af9313 100644 --- a/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py +++ b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_messages_transformation.py @@ -59,7 +59,9 @@ class TestAzureAnthropicMessagesConfig: assert result["x-api-key"] == "test-api-key" assert "api-key" not in result - def test_validate_anthropic_messages_environment_converts_api_key_to_x_api_key(self): + def test_validate_anthropic_messages_environment_converts_api_key_to_x_api_key( + self, + ): """Test that api-key header is converted to x-api-key""" config = AzureAnthropicMessagesConfig() headers = {} @@ -231,7 +233,7 @@ class TestAzureAnthropicMessagesConfig: config = AzureAnthropicMessagesConfig() model = "claude-sonnet-4-5" params = config.get_supported_anthropic_messages_params(model) - + assert "messages" in params assert "model" in params assert "max_tokens" in params @@ -281,7 +283,9 @@ class TestAzureAnthropicMessagesConfig: assert "scope" not in result["system"][0]["cache_control"] assert result["system"][0]["cache_control"]["type"] == "ephemeral" assert "scope" not in result["messages"][0]["content"][0]["cache_control"] - assert result["messages"][0]["content"][0]["cache_control"]["type"] == "ephemeral" + assert ( + result["messages"][0]["content"][0]["cache_control"]["type"] == "ephemeral" + ) class TestProviderConfigManagerAzureAnthropicMessages: @@ -327,3 +331,62 @@ class TestProviderConfigManagerAzureAnthropicMessages: ) assert config is None + + +class TestAzureAnthropicMessagesTransformRequest: + """Test that unsupported parameters are stripped from the request body (issue #18746)""" + + def test_transform_request_removes_max_retries(self): + """Test that max_retries is removed from the request body. + + Regression test for https://github.com/BerriAI/litellm/issues/18746 + Azure AI Foundry rejects max_retries with 'Extra inputs are not permitted'. + """ + config = AzureAnthropicMessagesConfig() + model = "claude-haiku-4-5" + messages = [{"role": "user", "content": [{"type": "text", "text": "hi"}]}] + optional_params = { + "max_tokens": 1000, + "max_retries": 0, + } + litellm_params = GenericLiteLLMParams() + headers = {} + + result = config.transform_anthropic_messages_request( + model=model, + messages=messages, + anthropic_messages_optional_request_params=optional_params, + litellm_params=litellm_params, + headers=headers, + ) + + assert "max_retries" not in result + assert result["model"] == model + assert result["max_tokens"] == 1000 + assert result["messages"] == messages + + def test_transform_request_removes_extra_body_and_stream_options(self): + """Test that extra_body and stream_options are also removed.""" + config = AzureAnthropicMessagesConfig() + model = "claude-haiku-4-5" + messages = [{"role": "user", "content": [{"type": "text", "text": "hi"}]}] + optional_params = { + "max_tokens": 1000, + "extra_body": {"foo": "bar"}, + "stream_options": {"include_usage": True}, + } + litellm_params = GenericLiteLLMParams() + headers = {} + + result = config.transform_anthropic_messages_request( + model=model, + messages=messages, + anthropic_messages_optional_request_params=optional_params, + litellm_params=litellm_params, + headers=headers, + ) + + assert "extra_body" not in result + assert "stream_options" not in result + assert result["model"] == model + assert result["max_tokens"] == 1000