mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(websearch_interception): default enabled_providers to all providers
The WebSearchInterceptionLogger was silently narrowing its default from 'all providers' (as documented) to [bedrock] only, so native Anthropic server-tools such as `web_search_20250305` were forwarded untouched to non-Bedrock backends (vllm, openai, together_ai, …) that cannot parse them. Treat `None` / `[]` as a wildcard (matching the docstring) and harden the provider gate in async_pre_call_deployment_hook against the wildcard value. Also adds two regression tests. Security: widening the interception scope is safe — the handler only converts Anthropic server-side tool definitions into standard function tools that all backends already accept. No new capabilities or data flows are introduced.
This commit is contained in:
parent
43efe76f17
commit
f62254c1e9
2 changed files with 82 additions and 11 deletions
|
|
@ -60,9 +60,17 @@ class WebSearchInterceptionLogger(CustomLogger):
|
|||
If None, will attempt to use first available search tool.
|
||||
"""
|
||||
super().__init__()
|
||||
# Convert enum values to strings for comparison
|
||||
if enabled_providers is None:
|
||||
self.enabled_providers = [LlmProviders.BEDROCK.value]
|
||||
# Convert enum values to strings for comparison.
|
||||
# ``None`` means "enable for all providers" (matches docstring and the
|
||||
# from_config_yaml contract). Passing an empty list also means all
|
||||
# providers — we represent both by storing ``None`` internally, so the
|
||||
# gating checks treat it as a wildcard.
|
||||
if not enabled_providers:
|
||||
# Security: widening the interception scope is safe — the handler
|
||||
# only converts Anthropic server-side tool definitions (e.g.
|
||||
# web_search_20250305) into standard function tools that all backends
|
||||
# already accept. No new capabilities or data flows are introduced.
|
||||
self.enabled_providers: Optional[List[str]] = None
|
||||
else:
|
||||
self.enabled_providers = [
|
||||
p.value if isinstance(p, LlmProviders) else p for p in enabled_providers
|
||||
|
|
@ -197,7 +205,10 @@ class WebSearchInterceptionLogger(CustomLogger):
|
|||
)
|
||||
except Exception:
|
||||
custom_llm_provider = ""
|
||||
if custom_llm_provider not in self.enabled_providers:
|
||||
if (
|
||||
self.enabled_providers is not None
|
||||
and custom_llm_provider not in self.enabled_providers
|
||||
):
|
||||
return None
|
||||
|
||||
# Check if request has tools with native web_search
|
||||
|
|
|
|||
|
|
@ -89,8 +89,9 @@ async def test_internal_flags_filtered_from_followup_kwargs():
|
|||
|
||||
# Apply the same filtering logic used in _execute_agentic_loop
|
||||
kwargs_for_followup = {
|
||||
k: v for k, v in kwargs_with_internal_flags.items()
|
||||
if not k.startswith('_websearch_interception')
|
||||
k: v
|
||||
for k, v in kwargs_with_internal_flags.items()
|
||||
if not k.startswith("_websearch_interception")
|
||||
}
|
||||
|
||||
# Verify internal flags are filtered out
|
||||
|
|
@ -130,12 +131,14 @@ async def test_async_pre_call_deployment_hook_provider_from_top_level_kwargs():
|
|||
assert result is not None
|
||||
# The web_search tool should be converted to litellm_web_search (OpenAI format)
|
||||
assert any(
|
||||
t.get("type") == "function" and t.get("function", {}).get("name") == "litellm_web_search"
|
||||
t.get("type") == "function"
|
||||
and t.get("function", {}).get("name") == "litellm_web_search"
|
||||
for t in result["tools"]
|
||||
)
|
||||
# The non-web-search tool should be preserved
|
||||
assert any(
|
||||
t.get("type") == "function" and t.get("function", {}).get("name") == "other_tool"
|
||||
t.get("type") == "function"
|
||||
and t.get("function", {}).get("name") == "other_tool"
|
||||
for t in result["tools"]
|
||||
)
|
||||
|
||||
|
|
@ -173,7 +176,8 @@ async def test_async_pre_call_deployment_hook_returns_full_kwargs():
|
|||
assert result["custom_llm_provider"] == "openai"
|
||||
# Tools should be converted
|
||||
assert any(
|
||||
t.get("type") == "function" and t.get("function", {}).get("name") == "litellm_web_search"
|
||||
t.get("type") == "function"
|
||||
and t.get("function", {}).get("name") == "litellm_web_search"
|
||||
for t in result["tools"]
|
||||
)
|
||||
|
||||
|
|
@ -234,7 +238,8 @@ async def test_async_pre_call_deployment_hook_nested_litellm_params_fallback():
|
|||
|
||||
assert result is not None
|
||||
assert any(
|
||||
t.get("type") == "function" and t.get("function", {}).get("name") == "litellm_web_search"
|
||||
t.get("type") == "function"
|
||||
and t.get("function", {}).get("name") == "litellm_web_search"
|
||||
for t in result["tools"]
|
||||
)
|
||||
# Full kwargs preserved
|
||||
|
|
@ -267,7 +272,8 @@ async def test_async_pre_call_deployment_hook_provider_derived_from_model_name()
|
|||
# Should NOT be None — the hook should derive "openai" from "openai/gpt-4o-mini"
|
||||
assert result is not None
|
||||
assert any(
|
||||
t.get("type") == "function" and t.get("function", {}).get("name") == "litellm_web_search"
|
||||
t.get("type") == "function"
|
||||
and t.get("function", {}).get("name") == "litellm_web_search"
|
||||
for t in result["tools"]
|
||||
)
|
||||
# Full kwargs preserved
|
||||
|
|
@ -319,3 +325,57 @@ async def test_deployment_hook_converts_stream_and_logging_obj_syncs():
|
|||
logging_obj.stream = _hook_stream
|
||||
|
||||
assert logging_obj.stream is False
|
||||
|
||||
|
||||
def test_default_enabled_providers_is_wildcard():
|
||||
"""Default (no enabled_providers configured) must intercept for ALL providers.
|
||||
|
||||
Regression test: previously ``enabled_providers=None`` silently narrowed to
|
||||
``[bedrock]``, so native Anthropic server-tools like ``web_search_20250305``
|
||||
were forwarded untouched to non-Bedrock backends (vllm, openai, together_ai,
|
||||
…) that cannot parse them. The docstring promised "None = all providers" —
|
||||
this test locks that contract in.
|
||||
"""
|
||||
logger = WebSearchInterceptionLogger()
|
||||
assert logger.enabled_providers is None
|
||||
|
||||
# Empty list is documented as equivalent to "all providers".
|
||||
logger_empty = WebSearchInterceptionLogger(enabled_providers=[])
|
||||
assert logger_empty.enabled_providers is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deployment_hook_default_intercepts_non_bedrock_provider():
|
||||
"""With default config, the hook must convert web_search_20250305 for any provider.
|
||||
|
||||
Before the fix, this test would fail for custom_llm_provider="hosted_vllm"
|
||||
because the default ``enabled_providers=[bedrock]`` skipped the hook, and
|
||||
the native server-tool (no ``input_schema``) was forwarded to the backend
|
||||
— which then rejected it with a Pydantic validation error on
|
||||
``body.tools[0].input_schema``.
|
||||
"""
|
||||
logger = WebSearchInterceptionLogger() # no enabled_providers → wildcard
|
||||
|
||||
kwargs = {
|
||||
"model": "hosted_vllm/some-model",
|
||||
"messages": [{"role": "user", "content": "search the web"}],
|
||||
"tools": [
|
||||
{"type": "web_search_20250305", "name": "web_search", "max_uses": 8},
|
||||
],
|
||||
"custom_llm_provider": "hosted_vllm",
|
||||
"api_key": "fake-key",
|
||||
}
|
||||
|
||||
result = await logger.async_pre_call_deployment_hook(kwargs=kwargs, call_type=None)
|
||||
|
||||
assert (
|
||||
result is not None
|
||||
), "default config must intercept web_search for non-Bedrock providers"
|
||||
# The server-side tool (no input_schema) must be replaced with the
|
||||
# function-style litellm_web_search tool before it reaches the backend.
|
||||
assert all(t.get("type") != "web_search_20250305" for t in result["tools"])
|
||||
assert any(
|
||||
t.get("type") == "function"
|
||||
and t.get("function", {}).get("name") == "litellm_web_search"
|
||||
for t in result["tools"]
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue