fix(azure_ai): strip tool-level extra fields on 400 and retry (#29479)

* fix(azure_ai): strip tool-level extra fields (e.g. copilot_mcp_server_name) before retrying

* fix(azure_ai): move re import to top-level; fix regex to handle hyphenated field names
This commit is contained in:
Sameer Kankute 2026-06-02 18:51:25 +05:30 committed by GitHub
parent c8bcfbb20c
commit dba1f2d3f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 95 additions and 13 deletions

View File

@ -1,4 +1,5 @@
import enum
import re
from typing import Any, List, Optional, Tuple, cast
from urllib.parse import urlparse
@ -275,21 +276,25 @@ class AzureAIStudioConfig(OpenAIConfig):
should_drop_params = litellm_params.get("drop_params") or litellm.drop_params
error_text = e.response.text
if should_drop_params and "Extra inputs are not permitted" in error_text:
if "Extra inputs are not permitted" in error_text:
if should_drop_params or self._error_has_tool_level_extra_fields(
error_text
):
return True
if "unknown field: parameter index is not a valid field" in error_text:
return True
elif (
"unknown field: parameter index is not a valid field" in error_text
): # remove index from tool calls
return True
elif (
if (
AzureFoundryErrorStrings.SET_EXTRA_PARAMETERS_TO_PASS_THROUGH.value
in error_text
): # remove extra-parameters from tool calls
):
return True
return super().should_retry_llm_api_inside_llm_translation_on_http_error(
e=e, litellm_params=litellm_params
)
def _error_has_tool_level_extra_fields(self, error_text: str) -> bool:
return bool(re.search(r"tools\[\d+\]\.", error_text))
@property
def max_retry_on_unprocessable_entity_error(self) -> int:
return 2
@ -297,9 +302,10 @@ class AzureAIStudioConfig(OpenAIConfig):
def transform_request_on_unprocessable_entity_error(
self, e: httpx.HTTPStatusError, request_data: dict
) -> dict:
error_text = e.response.text
_messages = cast(Optional[List[AllMessageValues]], request_data.get("messages"))
if (
"unknown field: parameter index is not a valid field" in e.response.text
"unknown field: parameter index is not a valid field" in error_text
and _messages is not None
):
litellm.remove_index_from_tool_calls(
@ -307,14 +313,31 @@ class AzureAIStudioConfig(OpenAIConfig):
)
elif (
AzureFoundryErrorStrings.SET_EXTRA_PARAMETERS_TO_PASS_THROUGH.value
in e.response.text
in error_text
):
request_data = self._drop_extra_params_from_request_data(
request_data, e.response.text
request_data, error_text
)
if (
"Extra inputs are not permitted" in error_text
and self._error_has_tool_level_extra_fields(error_text)
):
request_data = self._drop_tool_level_extra_fields(request_data, error_text)
data = drop_params_from_unprocessable_entity_error(e=e, data=request_data)
return data
def _drop_tool_level_extra_fields(
self, request_data: dict, error_text: str
) -> dict:
fields_to_drop = set(re.findall(r"tools\[\d+\]\.([\w-]+)", error_text))
tools = request_data.get("tools")
if fields_to_drop and isinstance(tools, list):
for tool in tools:
if isinstance(tool, dict):
for field in fields_to_drop:
tool.pop(field, None)
return request_data
def _drop_extra_params_from_request_data(
self, request_data: dict, error_text: str
) -> dict:
@ -332,9 +355,6 @@ class AzureAIStudioConfig(OpenAIConfig):
Error text looks like this"
"Extra parameters ['stream_options', 'extra-parameters'] are not allowed when extra-parameters is not set or set to be 'error'.
"""
import re
# Extract parameters within square brackets
match = re.search(r"\[(.*?)\]", error_text)
if not match:
return []

View File

@ -200,3 +200,65 @@ def test_azure_model_router_response_shows_actual_model():
f"Expected model to be 'azure_ai/gpt-5-nano-2025-08-07' (actual model used), "
f"but got '{result.model}'"
)
def test_drop_tool_level_extra_fields_strips_copilot_mcp_server_name():
"""
Regression test: Azure AI returns 400 when tools contain copilot_mcp_server_name.
LiteLLM should strip the field and retry automatically.
"""
import httpx
config = AzureAIStudioConfig()
error_text = json.dumps(
{
"error": {
"message": "2 request validation errors: Extra inputs are not permitted, field: 'tools[0].copilot_mcp_server_name', value: 'github-mcp-server'; Extra inputs are not permitted, field: 'tools[1].copilot_mcp_server_name', value: 'ide'"
}
}
)
mock_response = MagicMock(spec=httpx.Response)
mock_response.text = error_text
mock_response.json.return_value = json.loads(error_text)
mock_response.status_code = 400
e = httpx.HTTPStatusError(
message="400", request=MagicMock(), response=mock_response
)
assert config._error_has_tool_level_extra_fields(error_text) is True
assert (
config.should_retry_llm_api_inside_llm_translation_on_http_error(e, {}) is True
)
request_data = {
"model": "FW-Kimi-K2.6",
"messages": [{"role": "user", "content": "Say hi."}],
"tools": [
{
"type": "function",
"copilot_mcp_server_name": "github-mcp-server",
"function": {
"name": "github_search_code",
"description": "Search code",
"parameters": {"type": "object", "properties": {}},
},
},
{
"type": "function",
"copilot_mcp_server_name": "ide",
"function": {
"name": "read_file",
"description": "Read a file",
"parameters": {"type": "object", "properties": {}},
},
},
],
}
result = config.transform_request_on_unprocessable_entity_error(e, request_data)
for tool in result["tools"]:
assert "copilot_mcp_server_name" not in tool
assert result["tools"][0]["type"] == "function"
assert result["tools"][1]["function"]["name"] == "read_file"