From bde115f480bfb1986b1f1196e37c33bec839d47c Mon Sep 17 00:00:00 2001 From: Oxygen56 Date: Mon, 10 Aug 2026 05:09:23 +0800 Subject: [PATCH 1/2] fix(azure): normalize empty namespace tool descriptions --- .../llms/azure/responses/transformation.py | 36 ++++++++++-- .../response/test_azure_transformation.py | 55 +++++++++++++++++++ 2 files changed, 85 insertions(+), 6 deletions(-) diff --git a/litellm/llms/azure/responses/transformation.py b/litellm/llms/azure/responses/transformation.py index 0dd5e87e4ba..4e1c73f25fc 100644 --- a/litellm/llms/azure/responses/transformation.py +++ b/litellm/llms/azure/responses/transformation.py @@ -98,16 +98,40 @@ class AzureOpenAIResponsesAPIConfig(OpenAIResponsesAPIConfig): if isinstance(validated_input, list): filtered_input: Final[list[Any]] = [] for item in validated_input: - if isinstance(item, dict) and item.get("type") == "message": - # Filter out status field from message items - filtered_item = {k: v for k, v in item.items() if k != "status"} - filtered_input.append(filtered_item) - else: - filtered_input.append(item) + filtered_item = ( + {k: v for k, v in item.items() if k != "status"} + if isinstance(item, dict) and item.get("type") == "message" + else item + ) + filtered_input.append(self._normalize_additional_tools_item(filtered_item)) return cast(ResponseInputParam, filtered_input) return validated_input + @staticmethod + def _normalize_namespace_tool_description(tool: Any) -> Any: + if not isinstance(tool, dict) or tool.get("type") != "namespace": + return tool + + description: Final = tool.get("description") + if not isinstance(description, str) or description.strip(): + return tool + + name: Final = tool.get("name") + fallback_description: Final = name.strip() if isinstance(name, str) and name.strip() else "namespace" + return {**tool, "description": fallback_description} + + @classmethod + def _normalize_additional_tools_item(cls, item: Any) -> Any: + if not isinstance(item, dict) or item.get("type") != "additional_tools": + return item + + tools: Final = item.get("tools") + if not isinstance(tools, list): + return item + + return {**item, "tools": [cls._normalize_namespace_tool_description(tool) for tool in tools]} + def transform_responses_api_request( self, model: str, diff --git a/tests/test_litellm/llms/azure/response/test_azure_transformation.py b/tests/test_litellm/llms/azure/response/test_azure_transformation.py index 24ae563fb76..6cec2e72fa5 100644 --- a/tests/test_litellm/llms/azure/response/test_azure_transformation.py +++ b/tests/test_litellm/llms/azure/response/test_azure_transformation.py @@ -534,6 +534,61 @@ class TestAzureResponsesAPIConfig: assert "tools" not in response_api_params + @pytest.mark.parametrize( + ("description", "expected_description"), + [ + ("", "functions"), + (" \t ", "functions"), + ("Existing description", "Existing description"), + ], + ) + def test_azure_responses_api_normalizes_additional_tools_namespace_description( + self, description, expected_description + ): + namespace_tool = { + "type": "namespace", + "name": "functions", + "description": description, + "tools": [ + { + "type": "function", + "name": "ping", + "description": "Return pong.", + "parameters": {"type": "object", "properties": {}}, + "strict": False, + } + ], + } + function_tool = { + "type": "function", + "name": "empty_description_is_allowed_here", + "description": "", + "parameters": {"type": "object", "properties": {}}, + } + input_items = [ + { + "type": "additional_tools", + "role": "developer", + "tools": [namespace_tool, function_tool], + } + ] + original_input_items = deepcopy(input_items) + + result = self.config.transform_responses_api_request( + model=self.model, + input=input_items, + response_api_optional_request_params={}, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert result["input"][0]["tools"][0] == { + **namespace_tool, + "description": expected_description, + } + assert result["input"][0]["tools"][1] == function_tool + assert input_items == original_input_items + def test_azure_responses_api_context_management_unsupported(self): """Test that context_management is not in Azure supported params. From 5d08d5e3096c1751f79905098b8b1c8d3dce5a44 Mon Sep 17 00:00:00 2001 From: Oxygen56 Date: Mon, 10 Aug 2026 06:29:01 +0800 Subject: [PATCH 2/2] refactor(azure): type namespace tool normalization --- litellm/llms/azure/responses/transformation.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/litellm/llms/azure/responses/transformation.py b/litellm/llms/azure/responses/transformation.py index 4e1c73f25fc..61284317e5a 100644 --- a/litellm/llms/azure/responses/transformation.py +++ b/litellm/llms/azure/responses/transformation.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Any, Final, Literal import httpx from openai.types.responses import ResponseReasoningItem +from pydantic import JsonValue, TypeAdapter from litellm._logging import verbose_logger from litellm.litellm_core_utils.url_utils import encode_url_path_segment @@ -21,6 +22,9 @@ else: LiteLLMLoggingObj = Any +_JSON_VALUE_ADAPTER: Final[TypeAdapter[JsonValue]] = TypeAdapter(JsonValue) + + class AzureOpenAIResponsesAPIConfig(OpenAIResponsesAPIConfig): # Parameters not supported by Azure Responses API AZURE_UNSUPPORTED_PARAMS = ["context_management"] @@ -96,20 +100,24 @@ class AzureOpenAIResponsesAPIConfig(OpenAIResponsesAPIConfig): # Then filter out status from message items if isinstance(validated_input, list): - filtered_input: Final[list[Any]] = [] + filtered_input: Final[list[object]] = [] for item in validated_input: filtered_item = ( {k: v for k, v in item.items() if k != "status"} if isinstance(item, dict) and item.get("type") == "message" else item ) - filtered_input.append(self._normalize_additional_tools_item(filtered_item)) + if filtered_item.get("type") == "additional_tools": + validated_additional_tools_item: JsonValue = _JSON_VALUE_ADAPTER.validate_python(filtered_item) + filtered_input.append(self._normalize_additional_tools_item(validated_additional_tools_item)) + else: + filtered_input.append(filtered_item) return cast(ResponseInputParam, filtered_input) return validated_input @staticmethod - def _normalize_namespace_tool_description(tool: Any) -> Any: + def _normalize_namespace_tool_description(tool: JsonValue) -> JsonValue: if not isinstance(tool, dict) or tool.get("type") != "namespace": return tool @@ -122,7 +130,7 @@ class AzureOpenAIResponsesAPIConfig(OpenAIResponsesAPIConfig): return {**tool, "description": fallback_description} @classmethod - def _normalize_additional_tools_item(cls, item: Any) -> Any: + def _normalize_additional_tools_item(cls, item: JsonValue) -> JsonValue: if not isinstance(item, dict) or item.get("type") != "additional_tools": return item