Skip to content

fix: sanitize empty text content blocks in /v1/messages endpoint - #22979

Closed
atian8179 wants to merge 1 commit into
BerriAI:mainfrom
atian8179:fix/anthropic-messages-empty-text-blocks
Closed

fix: sanitize empty text content blocks in /v1/messages endpoint#22979
atian8179 wants to merge 1 commit into
BerriAI:mainfrom
atian8179:fix/anthropic-messages-empty-text-blocks

Conversation

@atian8179

Copy link
Copy Markdown

Problem

Multi-turn tool-use conversations through /v1/messages fail with:

400: messages: text content blocks must be non-empty

Claude's API returns assistant messages with empty text blocks alongside tool_use blocks ({"type": "text", "text": ""}). These are valid in responses but rejected when sent back. The /v1/messages endpoint passes messages through without sanitizing.

Empty text sanitization already exists for /v1/chat/completions (Bedrock #7177, Anthropic #20370, Databricks #20384) but was missing from the /v1/messages code path.

Fix

Add _sanitize_anthropic_empty_text_blocks() in the /v1/messages handler to strip empty text content blocks before forwarding to the upstream API.

Fixes #22930

Claude's API returns assistant messages with empty text blocks
alongside tool_use blocks, but rejects them when sent back in
subsequent requests. The /v1/messages endpoint passes messages
through without sanitizing these blocks, causing 400 errors in
multi-turn tool-use conversations.

Add _sanitize_anthropic_empty_text_blocks() to strip empty text
content blocks from Anthropic-format messages before forwarding.

Fixes BerriAI#22930
@vercel

vercel Bot commented Mar 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 Mar 6, 2026 4:08pm

Request Review

@CLAassistant

CLAassistant commented Mar 6, 2026

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented Mar 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds empty-text-block sanitization to the /v1/messages pass-through handler, mirroring fixes that already existed for Bedrock, Anthropic chat, and Databricks code paths. The core fix is correct and addresses a real 400-error regression in multi-turn tool-use conversations.

  • Fix location_sanitize_anthropic_empty_text_blocks() is introduced as a standalone helper and called at the start of anthropic_messages_handler, before any provider routing.
  • Common case handled correctly – Messages mixing empty text blocks with tool_use blocks will have the empty text blocks stripped while preserving the tool_use blocks.
  • All-empty edge case is inconsistent – When every content block is an empty text block, the code injects [{"type": "text", "text": "."}] as a placeholder. Both the Databricks sanitizer (litellm/llms/databricks/chat/transformation.py:89) and the Anthropic chat transformation remove the content key entirely in this case; injecting fabricated content deviates from that convention.
  • No tests added – The PR lacks unit tests for _sanitize_anthropic_empty_text_blocks, making it impossible to verify correctness or protect against regression.

Confidence Score: 3/5

  • Safe to merge for the common case but carries a silent data-mutation risk in the all-empty-blocks edge case and has no accompanying tests.
  • The primary fix (stripping empty text blocks from mixed-content messages) is correct and consistent with prior implementations. The score is reduced because: (1) the fallback placeholder injection fabricates conversation content and diverges from every other sanitizer in the repo; (2) there are no unit tests to prove correctness or catch regressions.
  • litellm/llms/anthropic/experimental_pass_through/messages/handler.py — specifically the _sanitize_anthropic_empty_text_blocks fallback on line 231.

Important Files Changed

Filename Overview
litellm/llms/anthropic/experimental_pass_through/messages/handler.py Adds _sanitize_anthropic_empty_text_blocks() and calls it at the top of anthropic_messages_handler to strip empty text content blocks before forwarding to upstream APIs. The logic is mostly correct for the common case (mixed tool_use + empty text), but the all-empty-blocks fallback injects a fabricated "." placeholder rather than removing the content key (inconsistent with Databricks and Anthropic chat sanitizers). No unit tests are included.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Incoming /v1/messages request] --> B[anthropic_messages async wrapper]
    B --> C[_execute_pre_request_hooks\nreceives original messages]
    C --> D[anthropic_messages_handler]
    D --> E[validate_anthropic_api_metadata]
    E --> F[_sanitize_anthropic_empty_text_blocks\nNEW]

    F --> G{For each message\nwith list content}
    G -->|Has empty text blocks| H[Filter out empty text blocks]
    H --> I{Any blocks remaining?}
    I -->|Yes| J[deepcopy msg\nset content = filtered]
    I -->|No - edge case| K[deepcopy msg\nset content = dot placeholder ⚠️]
    G -->|No empty blocks| L[Pass through unchanged]
    J --> M[Continue to provider routing]
    K --> M
    L --> M

    M --> N{Provider config found?}
    N -->|No - OpenAI| O[Responses API]
    N -->|No - other| P[Chat Completions]
    N -->|Yes e.g. Anthropic/Bedrock| Q[base_llm_http_handler\nanthropic_messages_handler]
Loading

Last reviewed commit: fd12506

Comment on lines +228 to +231
if len(filtered) != len(content):
msg = deepcopy(msg)
# If all blocks were empty text, keep one with placeholder
msg["content"] = filtered if filtered else [{"type": "text", "text": "."}]

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.

Injecting fabricated placeholder content

When a message's content list consists entirely of empty text blocks (all filtered out), the code replaces the content with [{"type": "text", "text": "."}]. This silently injects a fabricated "." string into the conversation history, which could alter model behaviour or confuse downstream consumers inspecting the request/response.

The existing Databricks sanitizer (_sanitize_empty_content in litellm/llms/databricks/chat/transformation.py, lines 89–92) handles the same edge case by removing the content key entirely rather than substituting a placeholder:

if not filtered:
    message_dict.pop("content")

A message with no content key is structurally cleaner than one that contains a fabricated sentence fragment. The same approach should be applied here to stay consistent and avoid modifying conversation semantics:

Suggested change
if len(filtered) != len(content):
msg = deepcopy(msg)
# If all blocks were empty text, keep one with placeholder
msg["content"] = filtered if filtered else [{"type": "text", "text": "."}]
if len(filtered) != len(content):
msg = deepcopy(msg)
if filtered:
msg["content"] = filtered
else:
msg.pop("content", None)

Comment on lines +206 to +235
def _sanitize_anthropic_empty_text_blocks(messages: List[Dict]) -> List[Dict]:
"""Remove empty text content blocks from Anthropic-format messages.

Claude's API can return assistant messages with empty text blocks alongside
tool_use blocks (e.g., {"type": "text", "text": ""}). These are valid in
responses but rejected when sent back in subsequent requests. This function
strips empty text blocks from content arrays, preserving all other blocks.

See: https://github.com/BerriAI/litellm/issues/22930
"""
sanitized = []
for msg in messages:
content = msg.get("content")
if isinstance(content, list):
filtered = [
block for block in content
if not (
isinstance(block, dict)
and block.get("type") == "text"
and not block.get("text", "").strip()
)
]
if len(filtered) != len(content):
msg = deepcopy(msg)
# If all blocks were empty text, keep one with placeholder
msg["content"] = filtered if filtered else [{"type": "text", "text": "."}]
sanitized.append(msg)
else:
sanitized.append(msg)
return sanitized

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.

No unit tests added for new sanitization function

_sanitize_anthropic_empty_text_blocks is a pure, side-effect-free function that is straightforward to unit-test, yet no tests are included in this PR. The custom review policy requires evidence that the claimed fix actually resolves the issue (e.g. passing tests or a before/after summary). The existing test file at tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py is the natural home for these.

Suggested test cases to cover:

  • A message with a mix of an empty text block and a tool_use block → empty block is stripped, tool_use block remains.
  • A message where text is only whitespace ("\n", " ") → treated as empty and stripped.
  • A message whose content is a plain string → left unchanged.
  • A message with no empty blocks → returned unmodified (no unnecessary deepcopy).
  • The all-empty edge case → content handled consistently (e.g. key removed instead of injected placeholder).

Context Used: Rule from dashboard - What: Ensure that any PR claiming to fix an issue includes evidence that the issue is resolved, such... (source)

@Sameerlite

Copy link
Copy Markdown
Contributor

Fix for #22930 was mergd. closing this

@Sameerlite Sameerlite closed this Jun 3, 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.

[Bug]: /v1/messages endpoint does not sanitize empty text content blocks

3 participants