diff --git a/litellm/integrations/websearch_interception/handler.py b/litellm/integrations/websearch_interception/handler.py index 84040280cd3..1a25842ba33 100644 --- a/litellm/integrations/websearch_interception/handler.py +++ b/litellm/integrations/websearch_interception/handler.py @@ -36,6 +36,7 @@ from litellm.integrations.websearch_interception.transformation import ( from litellm.litellm_core_utils.agentic_loop_settings import ( validated_max_agentic_loops, ) +from litellm.litellm_core_utils.core_helpers import qualify_provider_stripped_model from litellm.llms.base_llm.search.transformation import SearchResponse from litellm.types.integrations.custom_logger import ( CHAT_COMPLETION_AGENTIC_SURFACE, @@ -1242,9 +1243,10 @@ class WebSearchInterceptionLogger(CustomLogger): } } - full_model_name = model - if "/" not in model and isinstance(kwargs.get("custom_llm_provider"), str): - full_model_name = f"{kwargs['custom_llm_provider']}/{model}" + custom_llm_provider: Final = kwargs.get("custom_llm_provider") + full_model_name: Final = qualify_provider_stripped_model( + model, custom_llm_provider if isinstance(custom_llm_provider, str) else "" + ) verbose_logger.debug( "WebSearchInterception: Built responses request patch model=%s input_items=%d searches=%d", @@ -1910,8 +1912,6 @@ class WebSearchInterceptionLogger(CustomLogger): k: v for k, v in kwargs.items() if not k.startswith("_websearch_interception") and k not in internal_params } - from litellm.litellm_core_utils.core_helpers import qualify_provider_stripped_model - full_model_name: Final = qualify_provider_stripped_model(model, kwargs.get("custom_llm_provider", "")) verbose_logger.debug( diff --git a/tests/unit/integrations/websearch_interception/test_websearch_responses.py b/tests/unit/integrations/websearch_interception/test_websearch_responses.py index 00b7299cc31..8c5fd0daabe 100644 --- a/tests/unit/integrations/websearch_interception/test_websearch_responses.py +++ b/tests/unit/integrations/websearch_interception/test_websearch_responses.py @@ -252,6 +252,52 @@ async def test_build_responses_plan_produces_responses_input(): assert patch_obj.model == "openai/gpt-4o" +@pytest.mark.asyncio +async def test_build_responses_plan_keeps_the_provider_on_a_sub_path_model(): + """#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]) + + tools_dict = { + "tool_calls": [ + { + "id": "fc_1", + "call_id": "fc_1", + "type": "function_call", + "name": "litellm_web_search", + "arguments": '{"query": "latest ai news"}', + "input": {"query": "latest ai news"}, + } + ], + "tool_type": "websearch", + "provider": "bedrock", + "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, + }, + ) + + patch_obj = plan.request_patch + assert patch_obj is not None + assert patch_obj.model == "bedrock/mantle/anthropic.claude-sonnet-5" + + @pytest.mark.asyncio async def test_deployment_hook_converts_native_responses_web_search_tool(): """async_pre_call_deployment_hook converts a native Responses web_search tool