From 1f6c0e4e98c2022437d71cc586e033b33bc22d32 Mon Sep 17 00:00:00 2001 From: Liang Xu Date: Tue, 1 Sep 2026 17:03:46 +0800 Subject: [PATCH 1/3] fix(router): support Anthropic web_search and web_fetch tool capability filtering Fixes #38982 Signed-off-by: Liang Xu --- litellm/router_utils/common_utils.py | 64 ++++++++++++++++--- .../test_router_utils_common_utils.py | 19 ++++++ 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/litellm/router_utils/common_utils.py b/litellm/router_utils/common_utils.py index 280a7defcf8..64ab0f896a4 100644 --- a/litellm/router_utils/common_utils.py +++ b/litellm/router_utils/common_utils.py @@ -163,6 +163,26 @@ def filter_team_based_models( ] +def _is_web_search_tool(tool: dict) -> bool: + tool_type = str(tool.get("type") or "") + tool_name = str(tool.get("name") or "") + return ( + tool_type in ("web_search", "web_search_preview") + or tool_type.startswith("web_search_") + or tool_name == "web_search" + ) + + +def _is_web_fetch_tool(tool: dict) -> bool: + tool_type = str(tool.get("type") or "") + tool_name = str(tool.get("name") or "") + return ( + tool_type in ("web_fetch", "web_fetch_preview") + or tool_type.startswith("web_fetch_") + or tool_name == "web_fetch" + ) + + def _deployment_supports_web_search(deployment: dict) -> bool: """ Check if a deployment supports web search. @@ -183,12 +203,28 @@ def _deployment_supports_web_search(deployment: dict) -> bool: return True +def _deployment_supports_web_fetch(deployment: dict) -> bool: + """ + Check if a deployment supports web fetch. + + Priority: + 1. Check config-level override in model_info.supports_web_fetch + 2. Default to True (assume supported unless explicitly disabled) + """ + model_info: Final = deployment.get("model_info", {}) + + if "supports_web_fetch" in model_info: + return model_info["supports_web_fetch"] + + return True + + def filter_web_search_deployments( healthy_deployments: list[dict] | dict, request_kwargs: dict | None = None, ) -> list[dict] | dict: """ - If the request is websearch, filter out deployments that don't support web search + If the request is websearch or webfetch, filter out deployments that don't support it """ if request_kwargs is None: return healthy_deployments @@ -198,20 +234,32 @@ def filter_web_search_deployments( return healthy_deployments is_web_search_request = False + is_web_fetch_request = False tools: Final = request_kwargs.get("tools") or [] for tool in tools: - # These are the two websearch tools for OpenAI / Azure. - if tool.get("type") == "web_search" or tool.get("type") == "web_search_preview": + if not isinstance(tool, dict): + continue + if _is_web_search_tool(tool): is_web_search_request = True - break + if _is_web_fetch_tool(tool): + is_web_fetch_request = True - if not is_web_search_request: + if not is_web_search_request and not is_web_fetch_request: return healthy_deployments + final_deployments = healthy_deployments # Filter out deployments that don't support web search - final_deployments: Final = [d for d in healthy_deployments if _deployment_supports_web_search(d)] - if len(healthy_deployments) > 0 and len(final_deployments) == 0: - verbose_logger.warning("No deployments support web search for request") + if is_web_search_request: + final_deployments = [d for d in final_deployments if _deployment_supports_web_search(d)] + if len(healthy_deployments) > 0 and len(final_deployments) == 0: + verbose_logger.warning("No deployments support web search for request") + + # Filter out deployments that don't support web fetch + if is_web_fetch_request: + final_deployments = [d for d in final_deployments if _deployment_supports_web_fetch(d)] + if len(healthy_deployments) > 0 and len(final_deployments) == 0: + verbose_logger.warning("No deployments support web fetch for request") + return final_deployments diff --git a/tests/test_litellm/router_utils/test_router_utils_common_utils.py b/tests/test_litellm/router_utils/test_router_utils_common_utils.py index 30f658d7ea2..4e189b1c25c 100644 --- a/tests/test_litellm/router_utils/test_router_utils_common_utils.py +++ b/tests/test_litellm/router_utils/test_router_utils_common_utils.py @@ -352,6 +352,25 @@ class TestFilterWebSearchDeployments: # Should return the dict unchanged, not filter it assert result == deployment + def test_anthropic_versioned_web_search_filters_unsupported(self, sample_deployments): + """Anthropic versioned web_search tool types (e.g. web_search_20250305) should filter unsupported deployments""" + request_kwargs = {"tools": [{"type": "web_search_20250305", "name": "web_search"}]} + result = filter_web_search_deployments(sample_deployments, request_kwargs) + assert len(result) == 2 + result_ids = [d["model_info"]["id"] for d in result] + assert "deployment-3" not in result_ids + + def test_anthropic_web_fetch_filters_unsupported(self): + """Anthropic web_fetch tool types (e.g. web_fetch_20250910) should filter out supports_web_fetch=False deployments""" + deployments = [ + {"model_info": {"id": "d1", "supports_web_fetch": True}}, + {"model_info": {"id": "d2", "supports_web_fetch": False}}, + ] + request_kwargs = {"tools": [{"type": "web_fetch_20250910", "name": "web_fetch"}]} + result = filter_web_search_deployments(deployments, request_kwargs) + assert len(result) == 1 + assert result[0]["model_info"]["id"] == "d1" + def test_invalidate_model_group_info_cache(): """Test that _invalidate_model_group_info_cache clears the LRU cache.""" From 0c951a5ccb1a2a7c1dcc32ea43a975cb96cf961b Mon Sep 17 00:00:00 2001 From: Liang Xu Date: Sun, 6 Sep 2026 04:22:15 +0800 Subject: [PATCH 2/3] fix(router): conform to type discipline budget and LIT conventions --- litellm/router_utils/common_utils.py | 56 +++++++++++++--------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/litellm/router_utils/common_utils.py b/litellm/router_utils/common_utils.py index 64ab0f896a4..50fe9aee308 100644 --- a/litellm/router_utils/common_utils.py +++ b/litellm/router_utils/common_utils.py @@ -163,9 +163,9 @@ def filter_team_based_models( ] -def _is_web_search_tool(tool: dict) -> bool: - tool_type = str(tool.get("type") or "") - tool_name = str(tool.get("name") or "") +def _is_web_search_tool(tool: Mapping[str, object]) -> bool: + tool_type: Final = str(tool.get("type") or "") + tool_name: Final = str(tool.get("name") or "") return ( tool_type in ("web_search", "web_search_preview") or tool_type.startswith("web_search_") @@ -173,9 +173,9 @@ def _is_web_search_tool(tool: dict) -> bool: ) -def _is_web_fetch_tool(tool: dict) -> bool: - tool_type = str(tool.get("type") or "") - tool_name = str(tool.get("name") or "") +def _is_web_fetch_tool(tool: Mapping[str, object]) -> bool: + tool_type: Final = str(tool.get("type") or "") + tool_name: Final = str(tool.get("name") or "") return ( tool_type in ("web_fetch", "web_fetch_preview") or tool_type.startswith("web_fetch_") @@ -203,7 +203,7 @@ def _deployment_supports_web_search(deployment: dict) -> bool: return True -def _deployment_supports_web_fetch(deployment: dict) -> bool: +def _deployment_supports_web_fetch(deployment: Mapping[str, object]) -> bool: """ Check if a deployment supports web fetch. @@ -211,10 +211,9 @@ def _deployment_supports_web_fetch(deployment: dict) -> bool: 1. Check config-level override in model_info.supports_web_fetch 2. Default to True (assume supported unless explicitly disabled) """ - model_info: Final = deployment.get("model_info", {}) - - if "supports_web_fetch" in model_info: - return model_info["supports_web_fetch"] + raw_info: Final = deployment.get("model_info") + if isinstance(raw_info, dict) and "supports_web_fetch" in raw_info: + return bool(raw_info["supports_web_fetch"]) return True @@ -233,31 +232,28 @@ def filter_web_search_deployments( if isinstance(healthy_deployments, dict): return healthy_deployments - is_web_search_request = False - is_web_fetch_request = False tools: Final = request_kwargs.get("tools") or [] - for tool in tools: - if not isinstance(tool, dict): - continue - if _is_web_search_tool(tool): - is_web_search_request = True - if _is_web_fetch_tool(tool): - is_web_fetch_request = True + is_web_search_request: Final = any( + isinstance(tool, dict) and _is_web_search_tool(tool) for tool in tools + ) + is_web_fetch_request: Final = any( + isinstance(tool, dict) and _is_web_fetch_tool(tool) for tool in tools + ) if not is_web_search_request and not is_web_fetch_request: return healthy_deployments - final_deployments = healthy_deployments - # Filter out deployments that don't support web search - if is_web_search_request: - final_deployments = [d for d in final_deployments if _deployment_supports_web_search(d)] - if len(healthy_deployments) > 0 and len(final_deployments) == 0: + # Filter out deployments that don't support requested capabilities + final_deployments: Final = [ + d + for d in healthy_deployments + if (not is_web_search_request or _deployment_supports_web_search(d)) + and (not is_web_fetch_request or _deployment_supports_web_fetch(d)) + ] + if len(healthy_deployments) > 0 and len(final_deployments) == 0: + if is_web_search_request: verbose_logger.warning("No deployments support web search for request") - - # Filter out deployments that don't support web fetch - if is_web_fetch_request: - final_deployments = [d for d in final_deployments if _deployment_supports_web_fetch(d)] - if len(healthy_deployments) > 0 and len(final_deployments) == 0: + if is_web_fetch_request: verbose_logger.warning("No deployments support web fetch for request") return final_deployments From c3fb85557c2ed2bbd209582628fe2878a41e33e0 Mon Sep 17 00:00:00 2001 From: Liang Xu Date: Sun, 6 Sep 2026 04:30:58 +0800 Subject: [PATCH 3/3] style(router): format common_utils.py with ruff format --- litellm/router_utils/common_utils.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/litellm/router_utils/common_utils.py b/litellm/router_utils/common_utils.py index 50fe9aee308..61812d71fc9 100644 --- a/litellm/router_utils/common_utils.py +++ b/litellm/router_utils/common_utils.py @@ -233,12 +233,8 @@ def filter_web_search_deployments( return healthy_deployments tools: Final = request_kwargs.get("tools") or [] - is_web_search_request: Final = any( - isinstance(tool, dict) and _is_web_search_tool(tool) for tool in tools - ) - is_web_fetch_request: Final = any( - isinstance(tool, dict) and _is_web_fetch_tool(tool) for tool in tools - ) + is_web_search_request: Final = any(isinstance(tool, dict) and _is_web_search_tool(tool) for tool in tools) + is_web_fetch_request: Final = any(isinstance(tool, dict) and _is_web_fetch_tool(tool) for tool in tools) if not is_web_search_request and not is_web_fetch_request: return healthy_deployments