fix(websearch): restore snippet text in native web_search_tool_result blocks (LIT-5315) - #36228
Conversation
… blocks (LIT-5315) The build_web_search_tool_result_block method copied url/title/page_age but hardcoded encrypted_content to empty string, never reading SearchResult.snippet. This left every native block content-free, forcing clients to web_fetch each result to recover evidence—the reported symptom. The Anthropic spec carries page text only in encrypted_content (an opaque server-issued blob we cannot mint), so snippet is emitted as an additive key alongside the spec fields. encrypted_content stays empty rather than holding plaintext, which would assert encryption semantics that don't hold. The anthropic SDK's BaseModel sets extra='allow', so the additive snippet key survives SDK parsing. litellm has no typed model for web_search_result at all, so nothing drops it internally. Turn-2 replay behavior is unaffected: the empty encrypted_content already exists today. Tests: - Updated test_shape_with_results to assert snippet present - Added test_snippet_carried_for_every_result to cover multi-result ordering - Added test_missing_snippet_degrades_to_empty_string for edge case - Mutation check: reverting source-only yields 3 test failures, restored to 117 passed Fixes: LIT-5315 Co-Authored-By: Claude <noreply@anthropic.com>
Greptile SummaryThe PR preserves intercepted web-search snippets and converts replayed unencrypted native search blocks into provider-compatible text. It also pairs emitted results with server tool-use blocks, handles empty searches, and verifies repeated flattening remains idempotent
Confidence Score: 5/5The PR appears safe to merge No blocking failure remains
|
| Filename | Overview |
|---|---|
| litellm/integrations/websearch_interception/handler.py | Emits paired server-tool-use and web-search-result blocks with generated matching identifiers |
| litellm/integrations/websearch_interception/transformation.py | Preserves each search result’s snippet in the synthesized native result block |
| litellm/llms/anthropic/common_utils.py | Rewrites unencrypted replayed search results into text while preserving encrypted native blocks |
| litellm/llms/anthropic/experimental_pass_through/messages/handler.py | Applies replay flattening consistently across asynchronous and synchronous dispatch paths |
| tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py | Covers evidence preservation, resultless searches, native encrypted blocks, and repeated idempotent flattening |
| tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py | Confirms Bedrock receives text evidence without unsupported native search block types |
Reviews (2): Last reviewed commit: "test(websearch): pin flatten idempotency..." | Re-trigger Greptile
|
bugbot run |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…ve clients Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
Pushed 878c127: snippet alone still 400s on replay, so blocks now pair with a |
|
Gap: |
… accepts the next turn The flatten added for LIT-5315 bails when the replayed web_search_tool_result carries an empty content list, but that is exactly what the interceptor emits when a search legitimately returns nothing and when a search raises. The block survived into the outbound body, Bedrock rejected the tag, and the conversation died on the following turn just as it did before the flatten existed. An empty content list has no encrypted_content to respect and no evidence to preserve, so it flattens safely, and its paired server_tool_use goes with it. The rendered text now says so explicitly rather than emitting a bare header. Adds the multi-turn replay coverage that existed nowhere: the outbound Bedrock invoke body is asserted free of both block types, parametrized over the results-present and resultless cases, and built from the interceptor's own builder so the fixture cannot drift from what it emits. Resolves LIT-5320
|
@greptileai pushed |
|
|
|
The fix looks correct. The three-part solution is coherent:
The Bedrock integration test is the right regression anchor — it exercises the actual One minor observation: LGTM. |
| "title": title, | ||
| "page_age": page_age, | ||
| "encrypted_content": "", | ||
| "snippet": getattr(r, "snippet", "") or "", |
There was a problem hiding this comment.
Low: Search snippets bypass output guardrails
This exposes raw page text in a web_search_tool_result, but the Anthropic output-guardrail translator only extracts text and tool_use blocks. An authenticated caller can choose a query whose result contains prohibited data and receive the snippet unchanged even when the synthesized answer is blocked or masked. Extend the guardrail translation to scan these snippets and apply transformed values back before returning them.
There was a problem hiding this comment.
Real and confirmed: extraction is limited to text/input_text/output_text, so nested snippets are never scanned. Tracked as LIT-5321, separate from the replay fix
The agentic loop re-enters the same /v1/messages entry point for its follow-up call and hands it the original client history, so the flatten runs again over already-flattened messages once per iteration. Bedrock always takes that path, since its config reports web search as natively handled and the short-circuit is skipped. A pass that appended the rendered text instead of replacing the block would duplicate the evidence on every iteration and re-ship the unsupported tag, and no existing single-pass test sees it. Mutation checked: keeping the original block alongside the rendered text fails this test on its own.
PR overviewThis pull request restores search-result snippet text in native One security issue remains open: snippet text is returned without passing through the configured Anthropic output guardrails. An authenticated caller could therefore retrieve prohibited content from chosen search results even when the synthesized response is blocked or masked, and no issues have yet been addressed. Open issues (1)
Fixed/addressed: 0 · PR risk: 5/10 |
|
@greptileai please review the current head |
|
bugbot run |
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 f3958ab. Configure here.
TLDR
Problem this solves:
How it solves it:
web_search_resultsrvtoolu_server_tool_useUser Flow
Before: someone using a native Anthropic client against a Bedrock-backed gateway with web search interception on loses the conversation on the turn after any search
400withInput tag 'server_tool_use' found using 'type' does not match any of the expected tags: 'document', 'image', 'redacted_thinking', 'search_result', 'text', 'thinking', 'tool_result', 'tool_use'After: the same follow-up succeeds, and the model can still answer from the search evidence
200and the model answers from the replayed evidence rather than fetching each url again400Relevant issues
Linear ticket
Resolves LIT-5315
Resolves LIT-5320
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Three live proxies against real Bedrock,
bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0inus-east-1, billed to a real account. Config is the one this feature ships for:The replayed assistant turn is generated by
WebSearchTransformation.build_web_search_tool_result_blockitself rather than written by hand, so it is byte-for-byte what a native client would be handed:Each request replays one assistant turn holding
server_tool_use+web_search_tool_resultand then asksReply with only the year you just gave me.Before, at
1e7b39d15c(litellm_internal_staging), port 4071The control answers correctly on the unfixed build, so the
400is attributable to the replayed block and to nothing else in the request.Intermediate, at
878c1278e2, port 4072The resultless case was still dead here, which is what
b9c0eee88bfixes.After, at
b9c0eee88b, port 4073The 651 vs 620 input-token gap on the results-present run is the flattened search evidence still being carried into the prompt, so the model is answering from the sources rather than from a stripped history.
Type
🐛 Bug Fix
Changes
build_web_search_tool_result_blockcarriessnippeton each result, and emits its ownsrvtoolu_server_tool_usepaired with each result block, since a bare result block is rejected on replay.flatten_unencrypted_web_search_results_in_anthropic_messagesrewrites a replayed block that carries noencrypted_contentinto a text block holding the same title, url and snippet, and drops the pairedserver_tool_usewith it. Blocks Anthropic itself issued keep a realencrypted_contentand are left untouched, so native Anthropic behaviour is unchanged.An empty
contentlist flattens on the same path. That is what the interceptor emits both when a search legitimately returns nothing and when a search raises, and it holds neither evidence to preserve nor anencrypted_contentto respect, so leaving it in place only bought back the400the flatten exists to avoid. The rendered text saysNo results were returned.rather than emitting a bare header.Tests: the outbound Bedrock invoke body is asserted free of both block types, parametrized over the results-present and resultless cases, built from the interceptor's own builder so the fixture cannot drift from what it emits. Mutation checked per site: restoring the empty-content bail fails 3, dropping the resultless rendering fails 3, restored 7 pass. Affected suites 916 passed, 2 skipped.
Scope note: the
/chat/completionssurfaces mishandle a replayed search turn too, and are deliberately left alone. Interception never mintssrvtoolu_ids orweb_search_tool_resultblocks there, so reaching them needs a cross-provider replay where a client runs an Anthropic-native search and later replays that history against Bedrock. Confirmed live for completeness:/chat/completionsreturns400"tool_use ids were found without tool_result blocks immediately after", and Bedrock Converse silently drops the search turn instead. Both want their own ticketsFinal Attestation
Note
Medium Risk
Changes conversation shaping on every
/v1/messagesrequest with replayed search history; incorrect flattening could drop citations or alter multi-turn behavior, though real Anthropic encrypted blocks are explicitly preserved.Overview
Fixes multi-turn failures when native Anthropic clients replay intercepted web-search assistant turns against Bedrock-backed
/v1/messages.Websearch interception now emits a spec-correct
server_tool_use+web_search_tool_resultpair per search (sharedsrvtoolu_id) instead of a lone result block tied to the model’stoolu_id. Eachweb_search_resultalso carries an additivesnippetfield so clients and follow-ups have page text without relying onencrypted_content.Before dispatch,
flatten_unencrypted_web_search_results_in_anthropic_messagesrewrites LiteLLM-synthesized blocks (no realencrypted_content) into plain text with title/URL/snippet, drops the pairedserver_tool_use, and handles empty result lists the same way—so Bedrock no longer 400s on unsupportedserver_tool_use/web_search_tool_resulttags while preserving evidence. Genuine Anthropic blocks withencrypted_contentare left unchanged.Wired into the Anthropic messages handler (async + sync paths) alongside existing message sanitizers; tests cover snippets, native block pairs, flattening edge cases, and Bedrock outbound bodies.
Reviewed by Cursor Bugbot for commit f3958ab. Bugbot is set up for automated code reviews on this repo. Configure here.