From 087924c2aa0fa4dd2240458732f8b8f64be6f106 Mon Sep 17 00:00:00 2001 From: Hashwanth Sutharapu Date: Thu, 12 Feb 2026 00:00:10 -0800 Subject: [PATCH 1/2] Fix HostedWebSearchTool failing with AzureOpenAIChatClient (#3629) Override _prepare_tools_for_openai() in AzureOpenAIChatClient to raise ServiceInvalidRequestError when web search tools are used, since Azure OpenAI's Chat Completions API does not support the web_search_options parameter. Fixes #3629 --- .../agent_framework/azure/_chat_client.py | 29 +++++++++++++++++-- .../tests/azure/test_azure_chat_client.py | 28 +++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/python/packages/core/agent_framework/azure/_chat_client.py b/python/packages/core/agent_framework/azure/_chat_client.py index 0fcf99823a..fad0630a78 100644 --- a/python/packages/core/agent_framework/azure/_chat_client.py +++ b/python/packages/core/agent_framework/azure/_chat_client.py @@ -5,7 +5,7 @@ import json import logging import sys -from collections.abc import Mapping, Sequence +from collections.abc import Mapping, MutableMapping, Sequence from typing import TYPE_CHECKING, Any, Generic from azure.core.credentials import TokenCredential @@ -23,7 +23,7 @@ FunctionInvocationConfiguration, FunctionInvocationLayer, ) -from agent_framework.exceptions import ServiceInitializationError +from agent_framework.exceptions import ServiceInitializationError, ServiceInvalidRequestError from agent_framework.observability import ChatTelemetryLayer from agent_framework.openai import OpenAIChatOptions from agent_framework.openai._chat_client import RawOpenAIChatClient @@ -287,6 +287,31 @@ class MyOptions(AzureOpenAIChatOptions, total=False): **kwargs, ) + @override + def _prepare_tools_for_openai(self, tools: Sequence[Any]) -> dict[str, Any]: + """Prepare tools for the Azure OpenAI Chat Completions API. + + Overrides the base implementation to raise an error when web search tools + are used, since Azure OpenAI's Chat Completions API does not support the + ``web_search_options`` parameter. + + Args: + tools: Sequence of tools to prepare. + + Returns: + Dict containing tools. + + Raises: + ServiceInvalidRequestError: If a web search tool is included. + """ + for tool in tools: + if isinstance(tool, MutableMapping) and tool.get("type") == "web_search": + raise ServiceInvalidRequestError( + "Web search tools are not supported by Azure OpenAI's Chat Completions API. " + "Use OpenAIChatClient or AzureOpenAIResponsesClient instead." + ) + return super()._prepare_tools_for_openai(tools) + @override def _parse_text_from_openai(self, choice: Choice | ChunkChoice) -> Content | None: """Parse the choice into a Content object with type='text'. diff --git a/python/packages/core/tests/azure/test_azure_chat_client.py b/python/packages/core/tests/azure/test_azure_chat_client.py index f0b34cc13d..9907cca238 100644 --- a/python/packages/core/tests/azure/test_azure_chat_client.py +++ b/python/packages/core/tests/azure/test_azure_chat_client.py @@ -28,7 +28,7 @@ ) from agent_framework._telemetry import USER_AGENT_KEY from agent_framework.azure import AzureOpenAIChatClient -from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException +from agent_framework.exceptions import ServiceInitializationError, ServiceInvalidRequestError, ServiceResponseException from agent_framework.openai import ( ContentFilterResultSeverity, OpenAIContentFilterException, @@ -648,6 +648,32 @@ def get_weather(location: str) -> str: return f"The weather in {location} is sunny and 72°F." +def test_web_search_tool_raises_error(azure_openai_unit_test_env: dict[str, str]) -> None: + """Test that web search tools raise ServiceInvalidRequestError on Azure.""" + client = AzureOpenAIChatClient() + + web_search_tool = {"type": "web_search"} + with pytest.raises(ServiceInvalidRequestError, match="Web search tools are not supported"): + client._prepare_tools_for_openai([web_search_tool]) + + # Also test with additional options + web_search_tool_with_options = { + "type": "web_search", + "user_location": {"type": "approximate", "approximate": {"city": "Seattle", "country": "US"}}, + } + with pytest.raises(ServiceInvalidRequestError, match="Web search tools are not supported"): + client._prepare_tools_for_openai([web_search_tool_with_options]) + + +def test_prepare_tools_normal_tools_work(azure_openai_unit_test_env: dict[str, str]) -> None: + """Test that normal function tools still work through Azure's _prepare_tools_for_openai.""" + client = AzureOpenAIChatClient() + + result = client._prepare_tools_for_openai([get_weather]) + assert "tools" in result + assert "web_search_options" not in result + + @pytest.mark.flaky @skip_if_azure_integration_tests_disabled async def test_azure_openai_chat_client_response() -> None: From 40963dd505df1c1fe3bc72376351d6dd7fa31095 Mon Sep 17 00:00:00 2001 From: Hashwanth Sutharapu Date: Thu, 12 Feb 2026 00:08:41 -0800 Subject: [PATCH 2/2] Update fix to gracefully degrade instead of raising error Changed _prepare_tools_for_openai to filter out unsupported web search tools and log a warning, rather than raising ServiceInvalidRequestError. This ensures that requests with multiple tools don't fail entirely just because a web search tool was included. --- .../agent_framework/azure/_chat_client.py | 26 ++++++------ .../tests/azure/test_azure_chat_client.py | 40 +++++++++++++------ 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/python/packages/core/agent_framework/azure/_chat_client.py b/python/packages/core/agent_framework/azure/_chat_client.py index fad0630a78..2438d73be1 100644 --- a/python/packages/core/agent_framework/azure/_chat_client.py +++ b/python/packages/core/agent_framework/azure/_chat_client.py @@ -23,7 +23,7 @@ FunctionInvocationConfiguration, FunctionInvocationLayer, ) -from agent_framework.exceptions import ServiceInitializationError, ServiceInvalidRequestError +from agent_framework.exceptions import ServiceInitializationError from agent_framework.observability import ChatTelemetryLayer from agent_framework.openai import OpenAIChatOptions from agent_framework.openai._chat_client import RawOpenAIChatClient @@ -291,26 +291,28 @@ class MyOptions(AzureOpenAIChatOptions, total=False): def _prepare_tools_for_openai(self, tools: Sequence[Any]) -> dict[str, Any]: """Prepare tools for the Azure OpenAI Chat Completions API. - Overrides the base implementation to raise an error when web search tools - are used, since Azure OpenAI's Chat Completions API does not support the - ``web_search_options`` parameter. + Overrides the base implementation to filter out web search tools, + since Azure OpenAI's Chat Completions API does not support the + ``web_search_options`` parameter. Web search tools are silently + removed with a warning logged. Args: tools: Sequence of tools to prepare. Returns: - Dict containing tools. - - Raises: - ServiceInvalidRequestError: If a web search tool is included. + Dict containing prepared tools, with web search tools filtered out. """ + filtered_tools: list[Any] = [] for tool in tools: if isinstance(tool, MutableMapping) and tool.get("type") == "web_search": - raise ServiceInvalidRequestError( - "Web search tools are not supported by Azure OpenAI's Chat Completions API. " - "Use OpenAIChatClient or AzureOpenAIResponsesClient instead." + logger.warning( + "Web search tools are not supported by Azure OpenAI's Chat Completions API " + "and will be skipped. Use OpenAIChatClient or AzureOpenAIResponsesClient " + "for web search support." ) - return super()._prepare_tools_for_openai(tools) + continue + filtered_tools.append(tool) + return super()._prepare_tools_for_openai(filtered_tools) @override def _parse_text_from_openai(self, choice: Choice | ChunkChoice) -> Content | None: diff --git a/python/packages/core/tests/azure/test_azure_chat_client.py b/python/packages/core/tests/azure/test_azure_chat_client.py index 9907cca238..1cde328957 100644 --- a/python/packages/core/tests/azure/test_azure_chat_client.py +++ b/python/packages/core/tests/azure/test_azure_chat_client.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft. All rights reserved. import json +import logging import os from unittest.mock import AsyncMock, MagicMock, patch @@ -28,7 +29,7 @@ ) from agent_framework._telemetry import USER_AGENT_KEY from agent_framework.azure import AzureOpenAIChatClient -from agent_framework.exceptions import ServiceInitializationError, ServiceInvalidRequestError, ServiceResponseException +from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException from agent_framework.openai import ( ContentFilterResultSeverity, OpenAIContentFilterException, @@ -648,21 +649,36 @@ def get_weather(location: str) -> str: return f"The weather in {location} is sunny and 72°F." -def test_web_search_tool_raises_error(azure_openai_unit_test_env: dict[str, str]) -> None: - """Test that web search tools raise ServiceInvalidRequestError on Azure.""" +def test_web_search_tool_filtered_with_warning(azure_openai_unit_test_env: dict[str, str], caplog: pytest.LogCaptureFixture) -> None: + """Test that web search tools are filtered out with a warning on Azure.""" client = AzureOpenAIChatClient() web_search_tool = {"type": "web_search"} - with pytest.raises(ServiceInvalidRequestError, match="Web search tools are not supported"): - client._prepare_tools_for_openai([web_search_tool]) + + with caplog.at_level(logging.WARNING): + result = client._prepare_tools_for_openai([web_search_tool]) + + assert "Web search tools are not supported" in caplog.text + # Web search should be filtered out, result should have no tools or web_search_options + assert "web_search_options" not in result + assert "tools" not in result - # Also test with additional options - web_search_tool_with_options = { - "type": "web_search", - "user_location": {"type": "approximate", "approximate": {"city": "Seattle", "country": "US"}}, - } - with pytest.raises(ServiceInvalidRequestError, match="Web search tools are not supported"): - client._prepare_tools_for_openai([web_search_tool_with_options]) + +def test_web_search_tool_filtered_preserves_other_tools(azure_openai_unit_test_env: dict[str, str], caplog: pytest.LogCaptureFixture) -> None: + """Test that filtering web search tools preserves other tools in the list.""" + client = AzureOpenAIChatClient() + + web_search_tool = {"type": "web_search", "search_context_size": "medium"} + + with caplog.at_level(logging.WARNING): + result = client._prepare_tools_for_openai([get_weather, web_search_tool]) + + assert "Web search tools are not supported" in caplog.text + + # Normal tool should still be present + assert "tools" in result + assert len(result["tools"]) == 1 + assert "web_search_options" not in result def test_prepare_tools_normal_tools_work(azure_openai_unit_test_env: dict[str, str]) -> None: