fix(websearch): wrap agentic loop response in fake stream for streaming requests - #31484
Conversation
…ng requests 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. Added _maybe_wrap_in_fake_stream() which checks the websearch_interception_converted_stream flag and wraps dict responses in FakeAnthropicMessagesStreamIterator. Applied to all return paths in _call_agentic_completion_hooks: - async_run_agentic_loop (legacy path) - _execute_anthropic_agentic_plan (plan-based path) - plan.response_override - plan.terminate Includes unit tests for _maybe_wrap_in_fake_stream().
|
Generated by Claude Code |
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
|
Generated by Claude Code |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR fixes streaming responses for Anthropic Messages websearch interception. The main changes are:
Confidence Score: 4/5The change is narrowly scoped to websearch interception wrapping for Anthropic Messages streaming requests and is covered across the affected return paths. The implementation is gated by API surface and the converted-stream flag, and focused tests exercise helper behavior plus the relevant hook branches. Remaining risk is limited to integration behavior around live provider streaming and websearch interception. No specific files need additional attention beyond normal CI coverage for
What T-Rex did
Reviews (4): Last reviewed commit: "test(websearch): cover _execute_anthropi..." | Re-trigger Greptile |
…nthropic_messages surface Guard _maybe_wrap_in_fake_stream on api_surface == anthropic_messages so the responses API surface is never wrapped in an Anthropic SSE iterator, and type logging_obj as Optional to match the None call sites. Adds regression tests that drive the legacy, response_override, and terminate return paths of _call_agentic_completion_hooks end to end.
|
Both findings were valid and are addressed in 2a8e484. Generated by Claude Code |
…paths Drives the remaining two fake-stream return paths of _call_agentic_completion_hooks (the _execute_anthropic_agentic_plan branch via a stubbed handler, and the tail path when no agentic loop runs) so every converted-stream return path is regression-tested.
|
Added two more regression tests in bec6c87 covering the remaining Generated by Claude Code |
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit bec6c87. Configure here.
Relevant issues
Copy of #27449 by @vokako, re-targeted onto an internal branch so it can run through CircleCI. Original authorship is preserved on the commit
Linear ticket
Pre-Submission checklist
@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewScreenshots / Proof of Fix
Fully end to end against a live proxy with real web search, no mocks. A proxy runs
websearch_interceptionon theanthropicprovider wired to a real Tavilysearch_toolsbackend, then a streaming/v1/messagesrequest whose prompt forces the model to callweb_search, so the agentic loop runs a real search and synthesizes a grounded answer. The only thing that changes between before and after is how that final answer is framed back to the client: one JSON body, or an SSE streamSetup is identical for before and after (any configured
search_providerworks; Tavily shown here):Reproduction request (streaming, native
web_searchtool, prompt forces a tool call so the agentic loop runs):Before (on
litellm_internal_staging, without this PR)The real Tavily search runs and the model answers (note the live
web_search_resultcitations), but the whole thing comes back as a single JSON body withcontent-type: application/json, so an SSE client reads zero eventsAfter (with this PR)
Same request, same real search, but the response is re-wrapped as an Anthropic SSE stream with
content-type: text/event-streamand the grounded answer streams astext_deltaeventsType
🐛 Bug Fix
Changes
When
websearch_interceptionis enabled and a client sends a streaming request via/v1/messages, the handler convertsstream=Truetostream=Falseinternally to execute the search. After the agentic loop completes, the non-streaming dict response was returned directly to the client expecting SSE events, resulting in empty streamsIn
_call_agentic_completion_hooks(), theFakeAnthropicMessagesStreamIteratorwrapping previously only ran when no agentic loop executed. When the agentic loop did run, the return paths returned the dict directly without wrappingThis adds a
_maybe_wrap_in_fake_stream()helper that checks thewebsearch_interception_converted_streamflag and wraps dict responses inFakeAnthropicMessagesStreamIterator, applied to all return paths in_call_agentic_completion_hooks(async_run_agentic_loop,_execute_anthropic_agentic_plan,plan.response_override, andplan.terminate)The helper is gated on
api_surface == "anthropic_messages"; the converted-stream flag is only ever set by anthropic-messages websearch interception and the wrapper rebuilds an Anthropic SSE stream, so other surfaces (e.g. the responses API) are left untouched on every return path.logging_objis typedOptionalto match theNonecall sitesThis copy resolves merge conflicts against
litellm_internal_staging; the base had since added aresponsesAPI-surface path, which is kept intact while the anthropic_messages path gets the fake-stream wrappingTests drive the helper across all branches and additionally exercise the legacy,
response_override, andterminatereturn paths of_call_agentic_completion_hooksend to end, asserting the dict response is wrapped only when the converted-stream flag is set on the anthropic_messages surfaceFiles touched:
litellm/llms/custom_httpx/llm_http_handler.pyandtests/test_litellm/integrations/websearch_interception/test_websearch_streaming_wrap.pyNote
Medium Risk
Touches agentic completion hook return behavior for Anthropic messages and websearch interception; scope is narrow (gated flag + surface) but affects streaming contract for clients using that path.
Overview
Fixes empty SSE streams when websearch interception runs an agentic loop on
/v1/messagesrequests that were originallystream=Truebut forced to non-streaming internally.Introduces
_maybe_wrap_in_fake_stream()onBaseLLMHTTPHandler, which readswebsearch_interception_converted_streamon the logging object and, forapi_surface == "anthropic_messages"dict responses, returns aFakeAnthropicMessagesStreamIteratorinstead of the raw message dict.That helper is now applied on every exit path from
_call_agentic_completion_hooks(legacyasync_run_agentic_loop, planresponse_override,terminate,_execute_anthropic_agentic_plan, and the no-loop tail), replacing logic that only wrapped when no agentic loop ran. Non-anthropic surfaces (e.g. responses API) are unchanged.Adds
test_websearch_streaming_wrap.pycovering the helper branches and hook return paths with and without the converted-stream flag.Reviewed by Cursor Bugbot for commit bec6c87. Bugbot is set up for automated code reviews on this repo. Configure here.