From 6ee175dd2482717e9d5fe853616f89b527eccc2a Mon Sep 17 00:00:00 2001 From: Clawd Date: Thu, 7 May 2026 18:34:58 +0800 Subject: [PATCH] fix(websearch): wrap agentic loop response in fake stream when original request was streaming When websearch_interception converts stream=True to stream=False internally, the agentic loop returns a plain dict. Previously this dict was returned directly to the client expecting SSE events, resulting in empty streams. This fix adds _maybe_wrap_in_fake_stream() which checks the websearch_interception_converted_stream flag and wraps the dict response in FakeAnthropicMessagesStreamIterator when needed. Applied to both code paths in _call_agentic_completion_hooks: - async_run_agentic_loop (legacy path) - _execute_anthropic_agentic_plan (plan-based path) Fixes: streaming websearch requests via /v1/messages returning empty events when websearch_interception callback is enabled for bedrock. --- litellm/llms/custom_httpx/llm_http_handler.py | 68 +++++++++++++++---- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 2ffc7acbfb1..790eedaef2f 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -4740,6 +4740,42 @@ class BaseLLMHTTPHandler: **kwargs_for_followup, ) + def _maybe_wrap_in_fake_stream( + self, + response: Any, + logging_obj: "LiteLLMLoggingObj", + ) -> Any: + """ + If the original request was streaming but converted to non-streaming for + WebSearch interception, wrap the dict response in a FakeAnthropicMessagesStreamIterator. + """ + websearch_converted_stream = ( + logging_obj.model_call_details.get( + "websearch_interception_converted_stream", False + ) + if logging_obj is not None + else False + ) + if websearch_converted_stream and isinstance(response, dict): + from typing import cast + + from litellm._logging import verbose_logger + from litellm.llms.anthropic.experimental_pass_through.messages.fake_stream_iterator import ( + FakeAnthropicMessagesStreamIterator, + ) + from litellm.types.llms.anthropic_messages.anthropic_response import ( + AnthropicMessagesResponse, + ) + + verbose_logger.debug( + "WebSearchInterception: Agentic loop completed, " + "converting non-streaming response to fake stream" + ) + return FakeAnthropicMessagesStreamIterator( + response=cast(AnthropicMessagesResponse, response) + ) + return response + async def _call_agentic_completion_hooks( self, response: Any, @@ -4821,7 +4857,7 @@ class BaseLLMHTTPHandler: is not CustomLogger.async_build_agentic_loop_plan ) if not build_plan_overridden: - return await callback.async_run_agentic_loop( + agentic_result = await callback.async_run_agentic_loop( tools=tool_calls, model=model, messages=messages, @@ -4832,6 +4868,9 @@ class BaseLLMHTTPHandler: stream=stream, kwargs=kwargs_with_provider, ) + return self._maybe_wrap_in_fake_stream( + agentic_result, logging_obj + ) plan = await callback.async_build_agentic_loop_plan( tools=tool_calls, @@ -4857,18 +4896,21 @@ class BaseLLMHTTPHandler: if not plan.run_agentic_loop: continue - return await self._execute_anthropic_agentic_plan( - plan=plan, - model=model, - messages=messages, - anthropic_messages_optional_request_params=anthropic_messages_optional_request_params, - logging_obj=logging_obj, - kwargs=kwargs_with_provider, - depth=depth, - max_loops=max_loops, - fingerprints=fingerprints, - fingerprint=fingerprint, - stream=stream, + return self._maybe_wrap_in_fake_stream( + await self._execute_anthropic_agentic_plan( + plan=plan, + model=model, + messages=messages, + anthropic_messages_optional_request_params=anthropic_messages_optional_request_params, + logging_obj=logging_obj, + kwargs=kwargs_with_provider, + depth=depth, + max_loops=max_loops, + fingerprints=fingerprints, + fingerprint=fingerprint, + stream=stream, + ), + logging_obj, ) except Exception as e: _call_id = getattr(logging_obj, "litellm_call_id", "unknown")