From df7155507a0726acebb426b1da25afbf42488635 Mon Sep 17 00:00:00 2001 From: Priyansh Nandwana Date: Sat, 10 Oct 2026 12:35:12 +0530 Subject: [PATCH] test(websearch): run the real search path and flatten the qualify helper tests --- .../test_websearch_responses.py | 40 +++---- .../litellm_core_utils/test_core_helpers.py | 106 +++++++++--------- 2 files changed, 75 insertions(+), 71 deletions(-) diff --git a/tests/unit/integrations/websearch_interception/test_websearch_responses.py b/tests/unit/integrations/websearch_interception/test_websearch_responses.py index 8c5fd0daabe..af6038d0455 100644 --- a/tests/unit/integrations/websearch_interception/test_websearch_responses.py +++ b/tests/unit/integrations/websearch_interception/test_websearch_responses.py @@ -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 diff --git a/tests/unit/litellm_core_utils/test_core_helpers.py b/tests/unit/litellm_core_utils/test_core_helpers.py index faf916429a9..cb9d4a06544 100644 --- a/tests/unit/litellm_core_utils/test_core_helpers.py +++ b/tests/unit/litellm_core_utils/test_core_helpers.py @@ -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" + )