🆕 feat: support routing to only websearch supported deployments (#17500)

* support routing to only websearch supported deployments

* add docs
This commit is contained in:
Raghav Jhavar 2025-12-04 17:18:20 -05:00 • committed by GitHub
parent 2abcc77944
commit 72eb4c3a1c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 231 additions and 2 deletions

View file

@ -371,6 +371,22 @@ model_list:
web_search_options: {} # Enables web search with default settings
```
### Advanced
You can configure LiteLLM's router to optionally drop models that do not support WebSearch, for example
```yaml
- model_name: gpt-4.1
litellm_params:
model: openai/gpt-4.1
- model_name: gpt-4.1
litellm_params:
model: azure/gpt-4.1
api_base: "x.openai.azure.com/"
api_version: 2025-03-01-preview
model_info:
supports_web_search: False <---- KEY CHANGE!
```
In this example, LiteLLM will still route LLM requests to both deployments, but for WebSearch, will solely route to OpenAI.
</TabItem>
<TabItem value="custom" label="Custom Search Context">

View file

@ -43,6 +43,10 @@ import litellm
import litellm.litellm_core_utils
import litellm.litellm_core_utils.exception_mapping_utils
from litellm import get_secret_str
from litellm.router_utils.common_utils import (
filter_team_based_models,
filter_web_search_deployments,
)
from litellm._logging import verbose_router_logger
from litellm._uuid import uuid
from litellm.caching.caching import (
@ -7445,7 +7449,6 @@ class Router:
*OR*
- Dict, if specific model chosen
"""
from litellm.router_utils.common_utils import filter_team_based_models
model, healthy_deployments = self._common_checks_available_deployment(
model=model,
@ -7462,6 +7465,15 @@ class Router:
request_kwargs=request_kwargs,
)
verbose_router_logger.debug(f"healthy_deployments after team filter: {healthy_deployments}")
healthy_deployments = filter_web_search_deployments(
healthy_deployments=healthy_deployments,
request_kwargs=request_kwargs,
)
verbose_router_logger.debug(f"healthy_deployments after web search filter: {healthy_deployments}")
if isinstance(healthy_deployments, dict):
return healthy_deployments

View file

@ -6,6 +6,7 @@ if TYPE_CHECKING:
from litellm.types.llms.openai import OpenAIFileObject
from litellm.types.router import CredentialLiteLLMParams
from litellm._logging import verbose_logger
def get_litellm_params_sensitive_credential_hash(litellm_params: dict) -> str:
@ -73,3 +74,55 @@ def filter_team_based_models(
for deployment in healthy_deployments
if deployment.get("model_info", {}).get("id") not in ids_to_remove
]
def _deployment_supports_web_search(deployment: Dict) -> bool:
"""
Check if a deployment supports web search.
Priority:
1. Check config-level override in model_info.supports_web_search
2. Default to True (assume supported unless explicitly disabled)
Note: Ideally we'd fall back to litellm.supports_web_search() but
model_prices_and_context_window.json doesn't have supports_web_search
tags on all models yet. TODO: backfill and add fallback.
"""
model_info = deployment.get("model_info", {})
if "supports_web_search" in model_info:
return model_info["supports_web_search"]
return True
def filter_web_search_deployments(
healthy_deployments: Union[List[Dict], Dict],
request_kwargs: Optional[Dict] = None,
) -> Union[List[Dict], Dict]:
"""
If the request is websearch, filter out deployments that don't support web search
"""
if request_kwargs is None:
return healthy_deployments
# When a specific deployment was already chosen, it's returned as a dict
# rather than a list - nothing to filter, just pass through
if isinstance(healthy_deployments, dict):
return healthy_deployments
is_web_search_request = False
tools = request_kwargs.get("tools", [])
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":
is_web_search_request = True
break
if not is_web_search_request:
return healthy_deployments
# Filter out deployments that don't support web search
final_deployments = [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")
return final_deployments

View file

@ -3,7 +3,11 @@ from unittest.mock import Mock
import pytest
from litellm.router_utils.common_utils import filter_team_based_models
from litellm.router_utils.common_utils import (
_deployment_supports_web_search,
filter_team_based_models,
filter_web_search_deployments,
)
class TestFilterTeamBasedModels:
@ -187,3 +191,147 @@ class TestFilterTeamBasedModels:
expected_ids = ["deployment-1", "deployment-2"]
result_ids = [d.get("model_info", {}).get("id") for d in result]
assert sorted(result_ids) == sorted(expected_ids)
class TestDeploymentSupportsWebSearch:
"""Test cases for _deployment_supports_web_search helper function"""
def test_model_info_true(self):
"""model_info.supports_web_search=True returns True"""
deployment = {"model_info": {"supports_web_search": True}}
assert _deployment_supports_web_search(deployment) is True
def test_model_info_false(self):
"""model_info.supports_web_search=False returns False"""
deployment = {"model_info": {"supports_web_search": False}}
assert _deployment_supports_web_search(deployment) is False
def test_no_config_defaults_to_true(self):
"""When no supports_web_search in config, default to True"""
deployment = {"litellm_params": {"model": "gpt-4"}, "model_info": {"id": "123"}}
assert _deployment_supports_web_search(deployment) is True
def test_empty_deployment_defaults_to_true(self):
"""Empty deployment defaults to True"""
assert _deployment_supports_web_search({}) is True
def test_missing_model_info_defaults_to_true(self):
"""When model_info missing, default to True"""
deployment = {"litellm_params": {"model": "gpt-4"}}
assert _deployment_supports_web_search(deployment) is True
class TestFilterWebSearchDeployments:
"""Test cases for filter_web_search_deployments function"""
@pytest.fixture
def sample_deployments(self) -> List[Dict]:
"""Sample deployments with varying web search support"""
return [
{"model_info": {"id": "deployment-1"}}, # default True
{"model_info": {"id": "deployment-2", "supports_web_search": True}},
{"model_info": {"id": "deployment-3", "supports_web_search": False}},
]
def test_no_request_kwargs_returns_all(self, sample_deployments):
"""When request_kwargs is None, return all deployments"""
result = filter_web_search_deployments(sample_deployments, None)
assert result == sample_deployments
def test_no_tools_returns_all(self, sample_deployments):
"""When no tools in request, return all deployments"""
result = filter_web_search_deployments(sample_deployments, {"other": "value"})
assert result == sample_deployments
def test_empty_tools_returns_all(self, sample_deployments):
"""When tools list is empty, return all deployments"""
result = filter_web_search_deployments(sample_deployments, {"tools": []})
assert result == sample_deployments
def test_non_web_search_tools_returns_all(self, sample_deployments):
"""When tools don't include web_search, return all deployments"""
request_kwargs = {"tools": [{"type": "function", "function": {}}]}
result = filter_web_search_deployments(sample_deployments, request_kwargs)
assert result == sample_deployments
def test_web_search_filters_unsupported(self, sample_deployments):
"""When web_search tool present, filter out deployments that don't support it"""
request_kwargs = {"tools": [{"type": "web_search"}]}
result = filter_web_search_deployments(sample_deployments, request_kwargs)
# Should exclude deployment-3 (supports_web_search=False)
assert len(result) == 2
result_ids = [d["model_info"]["id"] for d in result]
assert "deployment-1" in result_ids
assert "deployment-2" in result_ids
assert "deployment-3" not in result_ids
def test_web_search_preview_filters_unsupported(self, sample_deployments):
"""web_search_preview type should also trigger filtering"""
request_kwargs = {"tools": [{"type": "web_search_preview"}]}
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_web_search_with_other_tools(self, sample_deployments):
"""Web search filtering works when mixed with other tools"""
request_kwargs = {
"tools": [
{"type": "function", "function": {"name": "get_weather"}},
{"type": "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_all_deployments_support_web_search(self):
"""When all deployments support web search, none are filtered"""
deployments = [
{"model_info": {"id": "d1", "supports_web_search": True}},
{"model_info": {"id": "d2", "supports_web_search": True}},
]
request_kwargs = {"tools": [{"type": "web_search"}]}
result = filter_web_search_deployments(deployments, request_kwargs)
assert len(result) == 2
def test_no_deployments_support_web_search(self):
"""When no deployments support web search, all are filtered out"""
deployments = [
{"model_info": {"id": "d1", "supports_web_search": False}},
{"model_info": {"id": "d2", "supports_web_search": False}},
]
request_kwargs = {"tools": [{"type": "web_search"}]}
result = filter_web_search_deployments(deployments, request_kwargs)
assert len(result) == 0
def test_missing_config_defaults_to_supported(self):
"""Deployments without supports_web_search config default to True"""
deployments = [
{"model_info": {"id": "d1"}}, # No supports_web_search - defaults to True
{"model_info": {"id": "d2"}}, # No supports_web_search - defaults to True
{"model_info": {"id": "d3", "supports_web_search": False}}, # Explicit False
]
request_kwargs = {"tools": [{"type": "web_search"}]}
result = filter_web_search_deployments(deployments, request_kwargs)
# d1 and d2 should be included (default True), d3 excluded (explicit False)
assert len(result) == 2
result_ids = [d["model_info"]["id"] for d in result]
assert "d1" in result_ids
assert "d2" in result_ids
assert "d3" not in result_ids
def test_empty_deployments_list(self):
"""Empty deployments list returns empty list"""
request_kwargs = {"tools": [{"type": "web_search"}]}
result = filter_web_search_deployments([], request_kwargs)
assert result == []
def test_dict_deployment_passthrough(self):
"""When deployment is a dict (single deployment), pass through unchanged"""
deployment = {"model_info": {"id": "d1", "supports_web_search": False}}
request_kwargs = {"tools": [{"type": "web_search"}]}
result = filter_web_search_deployments(deployment, request_kwargs)
# Should return the dict unchanged, not filter it
assert result == deployment