Skip to content

fix: WebSearch interception returns native Anthropic format for Claude Code compatibility - #25242

Closed
0xxmemo wants to merge 8 commits into
BerriAI:mainfrom
0xxmemo:fix/websearch-claude-code-native-format
Closed

fix: WebSearch interception returns native Anthropic format for Claude Code compatibility#25242
0xxmemo wants to merge 8 commits into
BerriAI:mainfrom
0xxmemo:fix/websearch-claude-code-native-format

Conversation

@0xxmemo

@0xxmemo 0xxmemo commented Apr 6, 2026

Copy link
Copy Markdown

Relevant issues

Fixes web search interception not working with Claude Code CLI (#24143 follow-up)

Pre-Submission checklist

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Type

🐛 Bug Fix

Changes

WebSearch interception does not work with Claude Code because the response format, tool naming, and streaming do not match what Claude Code expects. Three bugs, one root cause: the interception pipeline was built for programmatic litellm.messages.acreate() callers, not for Claude Code's streaming WebSearchTool.

Problem

When Claude Code sends a web_search_20250305 server tool request through a LiteLLM proxy:

  1. Wrong tool name — Interception registers the tool as litellm_web_search. Claude Code checks for WebSearch. The tool shows up with the wrong name, so Claude Code never recognizes it as its native web search.

  2. Wrong response formattry_short_circuit_search returns {type: "text"}. Claude Code's makeOutputFromSearchResponse expects native Anthropic format: server_tool_use + web_search_tool_result content blocks with structured search hits.

  3. Missing streaming supportFakeAnthropicMessagesStreamIterator only handles text, thinking, redacted_thinking, and tool_use blocks. It silently drops server_tool_use and web_search_tool_result. Since Claude Code uses queryModelWithStreaming, the search results never arrive — producing "Did 0 searches in Xs".

Fix

constants.py — Rename LITELLM_WEB_SEARCH_TOOL_NAME from "litellm_web_search" to "WebSearch" to match Claude Code's expected tool name.

handler.pytry_short_circuit_search returns native Anthropic format (server_tool_use + web_search_tool_result with structured hits). Also passes api_base from search tool config to litellm.asearch().

tools.py / transformation.py — Detection functions recognize both "WebSearch" and "litellm_web_search" for backwards compatibility.

fake_stream_iterator.py — Handle server_tool_use and web_search_tool_result block types so they stream as proper SSE events.

How it works now

  1. Claude Code sends web_search_20250305 to LiteLLM proxy
  2. try_short_circuit_search detects search-only request, executes via configured provider (SearXNG/Tavily/Perplexity)
  3. Returns synthetic response in native Anthropic format
  4. FakeAnthropicMessagesStreamIterator streams it as SSE
  5. Claude Code's WebSearchTool parses correctly

All providers (Anthropic, Bedrock, Vertex, third-party) go through the same single funnel.

Tested with

  • Claude Code v2.1.92 via claude -p with web search queries
  • Direct curl /v1/messages with web_search_20250305 tool
  • Multiple providers (Anthropic, Alibaba/Qwen) returning correct native format
  • Backwards compatibility: litellm_web_search name still detected

0xxmemo added 4 commits April 6, 2026 16:03
…l parameters

- Updated the WebSearchInterceptionLogger to extract and utilize the api_base parameter from the search tool's litellm_params.
- Modified the search execution logic to pass the api_base along with the search_provider to the asearch function, improving flexibility in API interactions.
- Ensured backward compatibility by maintaining existing functionality while adding the new parameter handling.
…se format

- Updated the WebSearchInterceptionLogger to execute web searches through the configured provider and return responses in a native Anthropic format.
- Implemented structured parsing of search results into hits for the web_search_tool_result, ensuring compatibility with Claude Code's WebSearchTool parser.
- Removed legacy checks for providers with native Anthropic Messages support, streamlining the short-circuiting logic for web search requests.
- Changed the standard web search tool name from "litellm_web_search" to "WebSearch" for consistency.
- Updated the documentation and descriptions in the web search tool functions to reflect the new naming and improve clarity.
- Enhanced the logic for identifying web search tools to accommodate both the new and legacy names, ensuring compatibility across different formats.
…hropicMessagesStreamIterator

- Implemented handling for "server_tool_use" and "web_search_tool_result" block types in the FakeAnthropicMessagesStreamIterator.
- Added logic to emit content_block_start and content_block_stop events for both block types, aligning with Anthropic's native streaming format.
- Enhanced the iterator's functionality to support new message types, improving integration with external tools.
@vercel

vercel Bot commented Apr 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Apr 6, 2026 9:36pm

Request Review

@CLAassistant

CLAassistant commented Apr 6, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@codspeed-hq

codspeed-hq Bot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing 0xxmemo:fix/websearch-claude-code-native-format (573b02a) with main (39c1042)

Open in CodSpeed

…ormat

- Update test_websearch_short_circuit.py to validate native Anthropic response
  format (server_tool_use + web_search_tool_result) instead of plain text
- Add tests for structured search hits, tool_use_id linking, usage tracking,
  and uniform provider handling
- Fix server_tool_use/web_search_tool_result block handling in
  _create_content_block_chunks method (was orphaned from cherry-pick)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
0xxmemo and others added 2 commits April 6, 2026 16:12
Providers with native Anthropic Messages support (anthropic, bedrock,
vertex_ai) skip the short-circuit and let their API handle
web_search_20250305 natively. Only non-native providers (github_copilot,
etc.) get the synthetic native-format response.

This preserves upstream behavior: Anthropic's API handles web search
server-side for providers that support it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes web search interception for Claude Code compatibility by making three targeted changes: (1) renaming LITELLM_WEB_SEARCH_TOOL_NAME from "litellm_web_search" to "WebSearch" to match Claude Code's expected tool name, (2) rewriting try_short_circuit_search to return native Anthropic format (server_tool_use + web_search_tool_result with structured hits instead of a plain {type: "text"} block), and (3) extending FakeAnthropicMessagesStreamIterator to handle server_tool_use and web_search_tool_result block types in SSE streaming.

  • Backwards-compat concern (rule b48b7341): The LITELLM_WEB_SEARCH_TOOL_NAME rename changes the injected tool wire format for all deployments. Detection functions now accept both "WebSearch" and "litellm_web_search" for incoming detection, but the tool name the proxy injects into model requests is now "WebSearch" for everyone. Existing users inspecting or matching tool names in model responses for "litellm_web_search" will silently get "WebSearch" instead.
  • Message ID format: Fixed in this PR — msg_ IDs now strip UUID hyphens (replace('-', '')[:20]), consistent with Anthropic's compact alphanumeric format.
  • Snippet in encrypted_content: The snippet text is placed in encrypted_content, which is the best available option for surfacing page content since LiteLLM cannot replicate Anthropic's server-side encryption.
  • Test coverage: All tests use AsyncMock with no real network calls; new assertions correctly verify the 3-block content structure and streaming SSE output."

Confidence Score: 4/5

Safe for new Claude Code deployments; existing users relying on 'litellm_web_search' as the injected tool name in model responses face a silent wire-format break.

The core logic is sound and well-tested with mocked unit tests. Score is 4 rather than 5 because of the backwards-incompatible tool name rename (rule b48b7341) — existing proxy deployments inspecting or matching 'litellm_web_search' in responses will silently receive 'WebSearch' instead. All other findings are P2 minor style/observation issues that don't block merge.

litellm/constants.py (wire-format rename) and litellm/integrations/websearch_interception/handler.py (trailing text block in short-circuit response)

Important Files Changed

Filename Overview
litellm/integrations/websearch_interception/handler.py Core short-circuit rewritten to return native Anthropic format (server_tool_use + web_search_tool_result); a trailing text block in all responses may interfere with Claude Code's synthesis step
litellm/llms/anthropic/experimental_pass_through/messages/fake_stream_iterator.py Adds server_tool_use and web_search_tool_result streaming via content_block_start events, matching Anthropic's SSE protocol for server-side tool blocks
litellm/integrations/websearch_interception/tools.py Detection set updated to recognize both WebSearch and litellm_web_search for backwards compat; minor redundancy in _WEB_SEARCH_NAMES with LITELLM_WEB_SEARCH_TOOL_NAME == 'WebSearch'
litellm/integrations/websearch_interception/transformation.py No functional changes; detection already accepts both old and new tool names via hardcoded list
litellm/constants.py LITELLM_WEB_SEARCH_TOOL_NAME renamed from 'litellm_web_search' to 'WebSearch' — potentially breaks existing deployments inspecting tool names in responses
tests/test_litellm/integrations/websearch_interception/test_websearch_short_circuit.py Tests correctly updated for new 3-block content structure; all use AsyncMock with no real network calls; good coverage of native format, streaming, and error paths

Sequence Diagram

sequenceDiagram
    participant CC as Claude Code
    participant LLP as LiteLLM Proxy
    participant SC as try_short_circuit_search
    participant SE as _execute_search
    participant FS as FakeStreamIterator

    CC->>LLP: POST /v1/messages {tools: [web_search_20250305], stream: true}
    LLP->>SC: try_short_circuit_search(model, messages, tools)
    Note over SC: Skips native providers (anthropic, bedrock, vertex_ai)
    SC->>SE: _execute_search(query)
    SE-->>SC: "Title: ...\nURL: ...\nSnippet: ..."
    SC->>SC: Parse into search_hits[]
    SC-->>LLP: {server_tool_use + web_search_tool_result + text}
    LLP->>FS: FakeAnthropicMessagesStreamIterator(response)
    FS-->>CC: SSE: message_start
    FS-->>CC: SSE: content_block_start (server_tool_use)
    FS-->>CC: SSE: content_block_stop
    FS-->>CC: SSE: content_block_start (web_search_tool_result)
    FS-->>CC: SSE: content_block_stop
    FS-->>CC: SSE: content_block_start (text)
    FS-->>CC: SSE: content_block_delta
    FS-->>CC: SSE: content_block_stop
    FS-->>CC: SSE: message_delta + message_stop
    CC->>CC: WebSearchTool.makeOutputFromSearchResponse()
    Note over CC: Parses server_tool_use + web_search_tool_result correctly
Loading

Greploops — Automatically fix all review issues by running /greploops in Claude Code. It iterates: fix, push, re-review, repeat until 5/5 confidence.
Use the Greptile plugin for Claude Code to query reviews, search comments, and manage custom context directly from your terminal.

Reviews (2): Last reviewed commit: "Fix review feedback: encrypted_content a..." | Re-trigger Greptile

Comment thread litellm/constants.py
# LiteLLM standard web search tool name
# Used for web search interception across providers
LITELLM_WEB_SEARCH_TOOL_NAME = "litellm_web_search"
LITELLM_WEB_SEARCH_TOOL_NAME = "WebSearch"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Backwards-incompatible tool name change

Renaming LITELLM_WEB_SEARCH_TOOL_NAME from "litellm_web_search" to "WebSearch" is a breaking change for existing users. This constant controls the name of the tool injected into model requests via get_litellm_web_search_tool() and get_litellm_web_search_tool_openai(). Any user who:

  • Has code checking tool names in model responses for "litellm_web_search"
  • Has hardcoded the tool name in downstream tooling
  • Is testing that the model uses the "litellm_web_search" tool

...will silently break because model responses will now contain "WebSearch" instead.

While is_web_search_tool() and the detection logic in transformation.py cover both names for detection, the change in the injected tool name changes the wire format for all existing deployments. Per the project style guide, backwards-incompatible changes should use a user-controlled flag.

Suggested approach: keep LITELLM_WEB_SEARCH_TOOL_NAME = "litellm_web_search" as the default and add a separate constant or config flag for Claude Code compatibility. If the rename is intentional, a release note warning users is strongly recommended.

Rule Used: What: avoid backwards-incompatible changes without... (source)

Comment on lines +173 to +181
hit: Dict[str, Any] = {
"type": "web_search_result",
"url": url,
"title": title or url,
"encrypted_content": "",
"page_age": None,
}
if snippet:
hit["snippet"] = snippet

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 encrypted_content always empty — model may not see search content

The web_search_result objects are built with "encrypted_content": "". In Anthropic's native format, encrypted_content is an opaque, server-encrypted payload containing the page content that the model reads when processing search results. Setting it to an empty string means the language model won't have access to the actual page content through this field — it relies only on the separate text block at the top level.

Claude Code's WebSearchTool parser reads encrypted_content to surface page content to the LLM in the follow-up turn. With an empty value, search results will show titles and URLs but the model won't process page text, potentially producing low-quality answers even when searches succeed.

Since LiteLLM can't replicate Anthropic's encryption, consider putting the snippet text directly in the encrypted_content field (unencrypted) or verifying that the downstream Claude Code version handles an empty value gracefully.

]

response: Dict[str, Any] = {
"id": f"msg_{str(uuid.uuid4())}",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Message ID includes UUID hyphens — non-standard format

f"msg_{str(uuid.uuid4())}" generates IDs with hyphens (e.g., msg_<hex>-<hex>-<hex>-<hex>-<hex>). Anthropic's real message IDs use a compact alphanumeric format without hyphens (e.g., msg_01XFDUDYJgAACTJCRiKey6u9). Clients that strictly validate the message ID format may reject this synthetic response.

The tool_use_id on line 184 correctly strips hyphens: str(uuid.uuid4()).replace('-', '')[:24]. Apply the same treatment here:

Suggested change
"id": f"msg_{str(uuid.uuid4())}",
"id": f"msg_{str(uuid.uuid4()).replace('-', '')[:20]}",

@codecov

codecov Bot commented Apr 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.33333% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...llm/integrations/websearch_interception/handler.py 65.21% 8 Missing ⚠️

📢 Thoughts on this report? Let us know!

- Put snippet text in encrypted_content so the model can read page
  content from search results (empty string meant the model only saw
  titles/URLs)
- Strip hyphens from synthetic message ID to match Anthropic's compact
  alphanumeric format (msg_<hex> not msg_<hex>-<hex>-...)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@0xxmemo

0xxmemo commented Apr 6, 2026

Copy link
Copy Markdown
Author

Addressed the review feedback:

P1 (tool name rename): This is intentional — "WebSearch" matches what Claude Code expects as its native tool name. The detection layer (is_web_search_tool, is_web_search_tool_chat_completion, and transformation.py) all retain "litellm_web_search" in their detection sets, so any existing code checking responses for that name will still work. The change only affects what name is injected into model requests. If maintainers prefer to keep the old default and use a config flag, happy to adjust.

P2 (encrypted_content): Fixed in 573b02a — snippet text is now placed in encrypted_content so the model can read page content from search results.

P2 (message ID format): Fixed in 573b02a — hyphens stripped to match Anthropic's compact msg_01XFDUDYJgAACTJCRiKey6u9 format.

@0xxmemo

0xxmemo commented Apr 6, 2026

Copy link
Copy Markdown
Author

@greptileai

dannycochran added a commit to dannycochran/litellm that referenced this pull request May 13, 2026
…t blocks) with bedrock short-circuit enabled

Combines PR BerriAI#25242 (native server_tool_use/web_search_tool_result format
for the websearch_interception short-circuit) with a fix to not skip
short-circuit for bedrock — bedrock speaks Anthropic Messages but does
not support web_search_20250305 natively.

Reference build for testing; not for upstream as-is (PR BerriAI#25242 work by
its original author).
@0xxmemo 0xxmemo closed this by deleting the head repository Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants