BerriAI/litellm · error · ValueError

WebSearchInterception: missing follow-up messages

Error message

WebSearchInterception: missing follow-up messages

What it means

In the legacy (non-streaming) Anthropic web-search interception path, the handler builds a request patch (search results + follow-up messages) and then requires patch.messages to be non-None before re-calling the model. If _build_anthropic_request_patch produced no follow-up message list — e.g. the tool call had no usable search query or the patch builder hit a shape it could not extend — it raises this ValueError.

Source

Thrown at litellm/integrations/websearch_interception/handler.py:1188

        tool_calls: list[dict],
        thinking_blocks: list[dict],
        anthropic_messages_optional_request_params: dict,
        logging_obj: "LiteLLMLoggingObj | None",
        stream: bool,
        kwargs: dict,
    ) -> "AnthropicMessagesResponse | AsyncIterator[object]":
        """Legacy path: execute search + build patch + run follow-up call."""
        request_patch, structured_results = await self._build_anthropic_request_patch(
            model=model,
            messages=messages,
            tool_calls=tool_calls,
            thinking_blocks=thinking_blocks,
            anthropic_messages_optional_request_params=anthropic_messages_optional_request_params,
            logging_obj=logging_obj,
            kwargs=kwargs,
        )
        if request_patch.messages is None:
            raise ValueError("WebSearchInterception: missing follow-up messages")

        optional_params: Final = dict(anthropic_messages_optional_request_params)
        optional_params.update(request_patch.optional_params)
        max_tokens = request_patch.max_tokens
        if max_tokens is None:
            max_tokens = cast(int | None, optional_params.pop("max_tokens", None))
        else:
            optional_params.pop("max_tokens", None)
        if max_tokens is None:
            max_tokens = cast(int, kwargs.get("max_tokens", 1024))

        response: AnthropicMessagesResponse | AsyncIterator[object] = await anthropic_messages.acreate(
            max_tokens=max_tokens,
            messages=request_patch.messages,
            model=request_patch.model or model,
            **optional_params,
            **request_patch.kwargs,
        )

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Inspect the tool_calls/thinking_blocks passed in — the web_search tool call must carry a valid query input
  2. Upgrade litellm; the interception patch builder is actively fixed for edge-case message shapes
  3. If you control the caller, validate that each web_search tool_use has non-empty input before enabling interception

Example fix

# before — assistant tool call with empty input
messages = [
    {"role": "assistant", "content": [
        {"type": "tool_use", "id": "tu_1", "name": "web_search", "input": {}}
    ]},
]
# -> ValueError: WebSearchInterception: missing follow-up messages

# after
messages = [
    {"role": "assistant", "content": [
        {"type": "tool_use", "id": "tu_1", "name": "web_search",
         "input": {"query": "latest litellm release notes"}}
    ]},
]
Defensive patterns

Strategy: try-catch

Validate before calling

def has_valid_web_search_tool_use(messages: list[dict]) -> bool:
    for msg in messages:
        for block in (msg.get("content") or [] if isinstance(msg.get("content"), list) else []):
            if isinstance(block, dict) and block.get("type") == "tool_use" and block.get("name") == "web_search":
                if not block.get("input", {}).get("query"):
                    return False
    return True

assert has_valid_web_search_tool_use(messages), "web_search tool_use blocks need a non-empty query"

Type guard

from typing import Any, TypeGuard

def is_web_search_tool_use(block: Any) -> TypeGuard[dict]:
    return (
        isinstance(block, dict)
        and block.get("type") == "tool_use"
        and block.get("name") == "web_search"
        and isinstance(block.get("input"), dict)
        and bool(block["input"].get("query"))
    )

Try / catch

try:
    result = await handler._legacy_search_and_follow_up(
        model, messages, tool_calls, thinking_blocks, optional_params, logging_obj, kwargs
    )
except ValueError as e:
    if "missing follow-up messages" in str(e):
        # fall back to a direct anthropic call without interception
        result = await anthropic_messages.acreate(**original_kwargs)
    else:
        raise

Prevention

When it happens

Trigger: An assistant message contains a web_search tool_use block whose input is empty/unparsable, so no search results and no follow-up user message are generated; a client supplies a hand-crafted message list with malformed server_tool_use/web_search_tool_result blocks; version drift between the handler and the anthropic SDK message shapes.

Common situations: Replaying captured Anthropic conversations through the interception handler; prompts where the model emitted a tool call missing the 'query' field; partial writes/truncation of the messages array in middleware.

Related errors


AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15). Data as JSON: /api/errors/2a73d80ac6a98457. Report an issue: GitHub.