Revert "fix: strip empty text content blocks in /v1/messages endpoint (#23097)"

This reverts commit 2c738cc939.
This commit is contained in:
Sameer Kankute 2026-03-10 09:53:19 +05:30
parent 2cb47727b6
commit 7542845e8d
2 changed files with 0 additions and 307 deletions

View file

@ -152,59 +152,6 @@ else:
LiteLLMLoggingObj = Any
def _sanitize_anthropic_messages_empty_text_blocks(
messages: List[Dict],
) -> List[Dict]:
"""
Strip empty text content blocks from Anthropic-format messages.
Claude's API returns assistant messages with ``{"type": "text", "text": ""}``
alongside ``tool_use`` blocks, but rejects them when sent back in subsequent
requests. This helper removes those empty text blocks so the /v1/messages
native path doesn't forward them as-is.
- If a content list contains a mix of empty text blocks and other blocks
(e.g. tool_use), the empty text blocks are removed.
- If *all* blocks in a content list are empty text, the content is replaced
with a single non-empty placeholder to avoid sending an empty array.
Ref: https://github.com/BerriAI/litellm/issues/22930
"""
sanitized: List[Dict] = []
for message in messages:
content = message.get("content")
if not isinstance(content, list):
sanitized.append(message)
continue
filtered = [
block
for block in content
if not (
isinstance(block, dict)
and block.get("type") == "text"
and not block.get("text", "").strip()
)
]
if filtered == content:
# Nothing was removed — keep original message as-is.
sanitized.append(message)
elif filtered:
# Some empty text blocks removed, but other content remains.
new_message = message.copy()
new_message["content"] = filtered
sanitized.append(new_message)
else:
# All blocks were empty text blocks. Replace with a placeholder
# so we don't send an empty content array.
new_message = message.copy()
new_message["content"] = [{"type": "text", "text": "..."}]
sanitized.append(new_message)
return sanitized
class BaseLLMHTTPHandler:
async def _make_common_async_call(
self,
@ -1958,13 +1905,6 @@ class BaseLLMHTTPHandler:
anthropic_messages_optional_request_params, path
)
# Sanitize empty text content blocks from messages before forwarding.
# Claude's API returns assistant messages with empty text blocks
# ({"type": "text", "text": ""}) alongside tool_use blocks, but rejects
# them when sent back. Strip these to prevent 400 errors.
# Ref: https://github.com/BerriAI/litellm/issues/22930
messages = _sanitize_anthropic_messages_empty_text_blocks(messages)
# Prepare request body
request_body = anthropic_messages_provider_config.transform_anthropic_messages_request(
model=model,

View file

@ -1,247 +0,0 @@
"""
Test empty text content block sanitization for the /v1/messages native path.
The Anthropic API returns assistant messages with empty text blocks
({"type": "text", "text": ""}) alongside tool_use blocks, but rejects
them when sent back. The /v1/messages endpoint must strip these before
forwarding to providers.
Ref: https://github.com/BerriAI/litellm/issues/22930
"""
import pytest
from litellm.llms.custom_httpx.llm_http_handler import (
_sanitize_anthropic_messages_empty_text_blocks,
)
class TestSanitizeAnthropicMessagesEmptyTextBlocks:
"""Unit tests for _sanitize_anthropic_messages_empty_text_blocks."""
def test_strips_empty_text_alongside_tool_use(self):
"""
The most common case from the bug report: an assistant message
containing an empty text block next to a tool_use block.
"""
messages = [
{"role": "user", "content": "Run the command."},
{
"role": "assistant",
"content": [
{"type": "text", "text": ""},
{
"type": "tool_use",
"id": "toolu_xxx",
"name": "Bash",
"input": {"command": "ls"},
},
],
},
]
result = _sanitize_anthropic_messages_empty_text_blocks(messages)
assert len(result) == 2
assert result[0] == messages[0] # user message unchanged
# assistant content should only have the tool_use block
assert len(result[1]["content"]) == 1
assert result[1]["content"][0]["type"] == "tool_use"
def test_preserves_nonempty_text_blocks(self):
"""Non-empty text blocks must not be removed."""
messages = [
{
"role": "assistant",
"content": [
{"type": "text", "text": "Let me check that."},
{
"type": "tool_use",
"id": "toolu_yyy",
"name": "Bash",
"input": {"command": "pwd"},
},
],
},
]
result = _sanitize_anthropic_messages_empty_text_blocks(messages)
assert len(result[0]["content"]) == 2
assert result[0]["content"][0] == {"type": "text", "text": "Let me check that."}
def test_whitespace_only_text_block_stripped(self):
"""Whitespace-only text blocks should also be stripped."""
messages = [
{
"role": "assistant",
"content": [
{"type": "text", "text": " \n\t "},
{
"type": "tool_use",
"id": "toolu_zzz",
"name": "Bash",
"input": {},
},
],
},
]
result = _sanitize_anthropic_messages_empty_text_blocks(messages)
assert len(result[0]["content"]) == 1
assert result[0]["content"][0]["type"] == "tool_use"
def test_all_empty_text_blocks_replaced_with_placeholder(self):
"""
If all content blocks are empty text, replace with a placeholder
to avoid sending an empty content array.
"""
messages = [
{
"role": "assistant",
"content": [
{"type": "text", "text": ""},
],
},
]
result = _sanitize_anthropic_messages_empty_text_blocks(messages)
assert len(result[0]["content"]) == 1
assert result[0]["content"][0]["type"] == "text"
assert result[0]["content"][0]["text"].strip() # must be non-empty
def test_string_content_untouched(self):
"""Messages with string content should pass through unchanged."""
messages = [
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi there!"},
]
result = _sanitize_anthropic_messages_empty_text_blocks(messages)
assert result == messages
def test_no_content_key_untouched(self):
"""Messages without a content key should pass through."""
messages = [
{"role": "user", "content": "Hello"},
{"role": "assistant"},
]
result = _sanitize_anthropic_messages_empty_text_blocks(messages)
assert result == messages
def test_user_message_content_list_also_sanitized(self):
"""
Empty text blocks should be stripped from user messages too,
not just assistant messages.
"""
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": ""},
{"type": "text", "text": "actual question"},
],
},
]
result = _sanitize_anthropic_messages_empty_text_blocks(messages)
assert len(result[0]["content"]) == 1
assert result[0]["content"][0]["text"] == "actual question"
def test_tool_result_content_blocks_untouched(self):
"""
tool_result content blocks should not be affected only
{"type": "text", "text": ""} blocks are stripped.
"""
messages = [
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_xxx",
"content": "",
},
],
},
]
result = _sanitize_anthropic_messages_empty_text_blocks(messages)
assert result == messages
def test_multiple_messages_mixed(self):
"""End-to-end scenario with multiple messages, some needing sanitization."""
messages = [
{"role": "user", "content": "Run ls"},
{
"role": "assistant",
"content": [
{"type": "text", "text": ""},
{
"type": "tool_use",
"id": "toolu_1",
"name": "Bash",
"input": {"command": "ls"},
},
],
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_1",
"content": "file1.txt\nfile2.txt",
},
],
},
{
"role": "assistant",
"content": [
{"type": "text", "text": "Here are the files:"},
],
},
]
result = _sanitize_anthropic_messages_empty_text_blocks(messages)
# First message: string content, unchanged
assert result[0] == messages[0]
# Second message: empty text stripped, only tool_use remains
assert len(result[1]["content"]) == 1
assert result[1]["content"][0]["type"] == "tool_use"
# Third message: tool_result, unchanged
assert result[2] == messages[2]
# Fourth message: non-empty text, unchanged
assert result[3] == messages[3]
def test_does_not_mutate_original_messages(self):
"""The function should not modify the input list or its dicts."""
original_content = [
{"type": "text", "text": ""},
{
"type": "tool_use",
"id": "toolu_1",
"name": "Bash",
"input": {},
},
]
messages = [
{
"role": "assistant",
"content": original_content,
},
]
_sanitize_anthropic_messages_empty_text_blocks(messages)
# Original message content should be unchanged
assert len(messages[0]["content"]) == 2
assert messages[0]["content"][0] == {"type": "text", "text": ""}