mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-06 08:16:43 +00:00
Revert "fix: strip empty text content blocks in /v1/messages endpoint (#23097)"
This reverts commit 2c738cc939.
This commit is contained in:
parent
2cb47727b6
commit
7542845e8d
2 changed files with 0 additions and 307 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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": ""}
|
||||
Loading…
Add table
Reference in a new issue