Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/my-website/docs/completion/web_search.md
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
14 changes: 13 additions & 1 deletion litellm/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
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 (
Expand Down Expand Up @@ -7442,7 +7446,6 @@ async def async_get_healthy_deployments(
*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,
Expand All @@ -7459,6 +7462,15 @@ async def async_get_healthy_deployments(
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

Expand Down
53 changes: 53 additions & 0 deletions litellm/router_utils/common_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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:
Expand Down Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this return True I did on purpose. By default, we should assume the deployment supports WebSearch so we don't break existing LiteLLM deployments.



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

150 changes: 149 additions & 1 deletion tests/test_litellm/router_utils/test_router_utils_common_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@

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:
Expand Down Expand Up @@ -187,3 +191,147 @@ def test_filter_team_based_models_none_team_id_in_deployment(self):
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
Loading