From b7a62f4a71f3885584d6ff6aebb1adeba24c3fb8 Mon Sep 17 00:00:00 2001 From: Darsh Joshi Date: Sat, 3 Oct 2026 12:21:24 -0400 Subject: [PATCH] fix(websearch_interception): match domain path rules on whole path segments A rule like example.com/docs/ lost its trailing slash and was then matched as a plain prefix, so it also admitted example.com/docs-private. Match the path exactly or at a segment boundary instead --- .../websearch_interception/transformation.py | 5 +- .../test_websearch_interception_handler.py | 52 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/litellm/integrations/websearch_interception/transformation.py b/litellm/integrations/websearch_interception/transformation.py index 44821855335..634735dbfc4 100644 --- a/litellm/integrations/websearch_interception/transformation.py +++ b/litellm/integrations/websearch_interception/transformation.py @@ -34,12 +34,13 @@ def domain_host(domain: str) -> str: def _url_matches_domain(url: str, domain: str) -> bool: - """Anthropic web_search domain semantics: subdomains are included and an optional path is a prefix.""" + """Anthropic web_search domain semantics: subdomains are included and an optional path matches whole path segments.""" target: Final = urlsplit(url) host: Final = (target.hostname or "").lower() rule_host, rule_path = _split_domain(domain) host_matches: Final = host == rule_host or host.endswith(f".{rule_host}") - return host_matches and target.path.startswith(rule_path) + path_matches: Final = not rule_path or target.path == rule_path or target.path.startswith(f"{rule_path}/") + return host_matches and path_matches def _url_passes_domain_filter(url: str, domains: WebSearchDomainFilter) -> bool: diff --git a/tests/unit/integrations/websearch_interception/test_websearch_interception_handler.py b/tests/unit/integrations/websearch_interception/test_websearch_interception_handler.py index 15e7e80660c..8cacbc21726 100644 --- a/tests/unit/integrations/websearch_interception/test_websearch_interception_handler.py +++ b/tests/unit/integrations/websearch_interception/test_websearch_interception_handler.py @@ -948,6 +948,58 @@ async def test_messages_web_search_honors_the_native_tool_domain_filter( assert mock_asearch.await_args.kwargs.get("search_domain_filter") == expected_provider_filter +_PATH_RULE_URLS = ( + "https://example.com/docs/intro", + "https://example.com/docs-private/x", + "https://example.com/other/y", +) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("domain_field", "rule", "expected_urls"), + [ + ("allowed_domains", "example.com/docs/", ["https://example.com/docs/intro"]), + ("allowed_domains", "example.com/docs", ["https://example.com/docs/intro"]), + ("blocked_domains", "example.com/docs/", ["https://example.com/docs-private/x", "https://example.com/other/y"]), + ], +) +async def test_messages_web_search_domain_path_rules_match_whole_path_segments( + monkeypatch: pytest.MonkeyPatch, + domain_field: str, + rule: str, + expected_urls: list[str], +): + import litellm + from litellm.llms.anthropic.pass_through.messages.handler import anthropic_messages + from litellm.llms.base_llm.search.transformation import SearchResult + from litellm.proxy import proxy_server + + monkeypatch.setattr(proxy_server, "llm_router", _perplexity_router()) + monkeypatch.setattr( + litellm, + "asearch", + AsyncMock( + return_value=SearchResponse( + object="search", + results=[SearchResult(title=url, url=url, snippet="snippet") for url in _PATH_RULE_URLS], + ) + ), + ) + monkeypatch.setattr(litellm, "callbacks", [WebSearchInterceptionLogger(enabled_providers=["bedrock"])]) + + response = await anthropic_messages( + max_tokens=512, + messages=[{"role": "user", "content": "litellm"}], + model="bedrock/converse/test-model", + custom_llm_provider="bedrock", + tools=[{"type": "web_search_20250305", "name": "web_search", domain_field: [rule]}], + ) + + text = next(block["text"] for block in response["content"] if block["type"] == "text") + assert [url for url in _PATH_RULE_URLS if url in text] == expected_urls + + @pytest.mark.asyncio @pytest.mark.parametrize( ("domain_field", "expected_urls", "expected_provider_filter"),