mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
🆕 feat: support routing to only websearch supported deployments (#17500)
* support routing to only websearch supported deployments * add docs
This commit is contained in:
parent
2abcc77944
commit
72eb4c3a1c
4 changed files with 231 additions and 2 deletions
|
|
@ -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">
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue