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:
parent
c8bcfbb20c
commit
dba1f2d3f2
@ -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 []
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user