test(websearch): run the real search path and flatten the qualify helper tests

This commit is contained in:
Priyansh Nandwana 2026-10-10 12:35:12 +05:30
parent cca1943b6e
commit df7155507a
2 changed files with 75 additions and 71 deletions

View file

@ -11,12 +11,16 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
import litellm
from litellm.integrations.websearch_interception.handler import (
WebSearchInterceptionLogger,
)
from litellm.types.integrations.custom_logger import (
RESPONSES_AGENTIC_SURFACE,
)
from litellm.llms.base_llm.search.transformation import SearchResponse
from litellm.proxy import proxy_server
from litellm.types.utils import CallTypes, LlmProviders
@ -253,10 +257,12 @@ async def test_build_responses_plan_produces_responses_input():
@pytest.mark.asyncio
async def test_build_responses_plan_keeps_the_provider_on_a_sub_path_model():
async def test_build_responses_plan_keeps_the_provider_on_a_sub_path_model(monkeypatch):
"""#38829 on the Responses surface: a provider-stripped model whose remainder still holds a
slash was left unqualified, so the follow-up could not resolve a provider."""
logger = WebSearchInterceptionLogger(enabled_providers=[LlmProviders.BEDROCK])
monkeypatch.setattr(proxy_server, "llm_router", None)
monkeypatch.setattr(litellm, "asearch", AsyncMock(return_value=SearchResponse(object="search", results=[])))
tools_dict = {
"tool_calls": [
@ -274,28 +280,24 @@ async def test_build_responses_plan_keeps_the_provider_on_a_sub_path_model():
"response_format": "responses",
}
with patch.object(
logger,
"_execute_search",
new=AsyncMock(return_value=("a search result", None)),
):
plan = await logger.async_build_responses_agentic_loop_plan(
tools=tools_dict,
model="mantle/anthropic.claude-sonnet-5",
messages=[{"role": "user", "content": "What's the latest AI news?"}],
response=_responses_output_with_web_search(),
optional_params={"tools": [{"type": "function", "name": "litellm_web_search"}]},
logging_obj=MagicMock(),
stream=False,
kwargs={
"custom_llm_provider": "bedrock",
"_agentic_loop_api_surface": RESPONSES_AGENTIC_SURFACE,
},
)
plan = await logger.async_build_responses_agentic_loop_plan(
tools=tools_dict,
model="mantle/anthropic.claude-sonnet-5",
messages=[{"role": "user", "content": "What's the latest AI news?"}],
response=_responses_output_with_web_search(),
optional_params={"tools": [{"type": "function", "name": "litellm_web_search"}]},
logging_obj=MagicMock(),
stream=False,
kwargs={
"custom_llm_provider": "bedrock",
"_agentic_loop_api_surface": RESPONSES_AGENTIC_SURFACE,
},
)
patch_obj = plan.request_patch
assert patch_obj is not None
assert patch_obj.model == "bedrock/mantle/anthropic.claude-sonnet-5"
litellm.asearch.assert_awaited_once()
@pytest.mark.asyncio

View file

@ -590,67 +590,69 @@ class TestProviderResponseHeadersInHiddenParams:
assert "additional_headers" not in sibling._hidden_params
class TestQualifyProviderStrippedModel:
"""#38829: a model whose remainder still holds a slash must keep its provider prefix."""
@pytest.mark.parametrize(
"model,provider,expected",
[
("mantle/anthropic.claude-sonnet-5", "bedrock", "bedrock/mantle/anthropic.claude-sonnet-5"),
("invoke/anthropic.claude-v2", "bedrock", "bedrock/invoke/anthropic.claude-v2"),
("openai/gpt-4o", "openrouter", "openrouter/openai/gpt-4o"),
("gpt-4o", "openai", "openai/gpt-4o"),
("claude-sonnet-4-5", "anthropic", "anthropic/claude-sonnet-4-5"),
],
)
def test_the_provider_prefix_is_restored(model, provider, expected):
assert qualify_provider_stripped_model(model, provider) == expected
@pytest.mark.parametrize(
"model,provider,expected",
[
("mantle/anthropic.claude-sonnet-5", "bedrock", "bedrock/mantle/anthropic.claude-sonnet-5"),
("invoke/anthropic.claude-v2", "bedrock", "bedrock/invoke/anthropic.claude-v2"),
("openai/gpt-4o", "openrouter", "openrouter/openai/gpt-4o"),
("gpt-4o", "openai", "openai/gpt-4o"),
("claude-sonnet-4-5", "anthropic", "anthropic/claude-sonnet-4-5"),
],
@pytest.mark.parametrize(
"model,provider",
[
("bedrock/mantle/anthropic.claude-sonnet-5", "bedrock"),
("openai/gpt-4o", "openai"),
],
)
def test_an_already_qualified_model_is_left_alone(model, provider):
assert qualify_provider_stripped_model(model, provider) == model
def test_a_provider_that_only_shares_a_prefix_is_still_qualified():
assert qualify_provider_stripped_model("openai_like/foo", "openai") == "openai/openai_like/foo"
def test_no_provider_leaves_the_model_untouched():
assert qualify_provider_stripped_model("gpt-4o", "") == "gpt-4o"
def test_a_cross_provider_patched_model_is_dispatched_as_the_hook_asked():
assert qualify_agentic_followup_model("anthropic/claude-sonnet-4-5", "gpt-4o", "openai") == (
"anthropic/claude-sonnet-4-5"
)
def test_the_provider_prefix_is_restored(self, model, provider, expected):
assert qualify_provider_stripped_model(model, provider) == expected
@pytest.mark.parametrize(
"model,provider",
[
("bedrock/mantle/anthropic.claude-sonnet-5", "bedrock"),
("openai/gpt-4o", "openai"),
],
def test_a_bare_patched_model_takes_the_request_provider():
assert qualify_agentic_followup_model("gpt-4o-mini", "gpt-4o", "openai") == "openai/gpt-4o-mini"
def test_a_sub_path_request_model_keeps_its_provider():
assert qualify_agentic_followup_model(None, "mantle/anthropic.claude-sonnet-5", "bedrock") == (
"bedrock/mantle/anthropic.claude-sonnet-5"
)
def test_an_already_qualified_model_is_left_alone(self, model, provider):
assert qualify_provider_stripped_model(model, provider) == model
def test_a_provider_that_only_shares_a_prefix_is_still_qualified(self):
assert qualify_provider_stripped_model("openai_like/foo", "openai") == "openai/openai_like/foo"
def test_no_provider_leaves_the_model_untouched(self):
assert qualify_provider_stripped_model("gpt-4o", "") == "gpt-4o"
class TestQualifyAgenticFollowUpModel:
"""A hook that qualified its own patched model owns that choice, so a cross-provider
follow-up must not be re-prefixed with the original request's provider."""
def test_an_unpatched_ordinary_model_takes_the_request_provider():
assert qualify_agentic_followup_model(None, "gpt-4o", "openai") == "openai/gpt-4o"
def test_a_cross_provider_patched_model_is_dispatched_as_the_hook_asked(self):
assert qualify_agentic_followup_model("anthropic/claude-sonnet-4-5", "gpt-4o", "openai") == (
"anthropic/claude-sonnet-4-5"
)
def test_a_bare_patched_model_takes_the_request_provider(self):
assert qualify_agentic_followup_model("gpt-4o-mini", "gpt-4o", "openai") == "openai/gpt-4o-mini"
def test_a_patched_model_already_holding_the_request_provider_is_left_alone():
assert qualify_agentic_followup_model("openai/gpt-4o", "gpt-4o", "openai") == "openai/gpt-4o"
def test_a_sub_path_request_model_keeps_its_provider(self):
assert qualify_agentic_followup_model(None, "mantle/anthropic.claude-sonnet-5", "bedrock") == (
"bedrock/mantle/anthropic.claude-sonnet-5"
)
def test_an_unpatched_ordinary_model_takes_the_request_provider(self):
assert qualify_agentic_followup_model(None, "gpt-4o", "openai") == "openai/gpt-4o"
@pytest.mark.parametrize("patched", ["", None])
def test_a_patch_without_a_model_falls_back_to_the_request_model(patched):
assert qualify_agentic_followup_model(patched, "gpt-4o", "openai") == "openai/gpt-4o"
def test_a_patched_model_already_holding_the_request_provider_is_left_alone(self):
assert qualify_agentic_followup_model("openai/gpt-4o", "gpt-4o", "openai") == "openai/gpt-4o"
@pytest.mark.parametrize("patched", ["", None])
def test_a_patch_without_a_model_falls_back_to_the_request_model(self, patched):
assert qualify_agentic_followup_model(patched, "gpt-4o", "openai") == "openai/gpt-4o"
def test_a_patched_sub_path_model_still_takes_the_request_provider(self):
assert qualify_agentic_followup_model("mantle/anthropic.claude-sonnet-5", "gpt-4o", "bedrock") == (
"bedrock/mantle/anthropic.claude-sonnet-5"
)
def test_a_patched_sub_path_model_still_takes_the_request_provider():
assert qualify_agentic_followup_model("mantle/anthropic.claude-sonnet-5", "gpt-4o", "bedrock") == (
"bedrock/mantle/anthropic.claude-sonnet-5"
)