Merge pull request #20390 from BerriAI/litellm_fix_empty_message

Fix: empty assistant message for converse API
This commit is contained in:
Sameer Kankute
2026-02-04 18:05:37 +05:30
committed by GitHub
3 changed files with 137 additions and 19 deletions
@@ -3987,10 +3987,12 @@ class BedrockConverseMessagesProcessor:
assistant_parts=assistants_parts,
)
elif element["type"] == "text":
assistants_part = BedrockContentBlock(
text=element["text"]
)
assistants_parts.append(assistants_part)
# Skip completely empty strings to avoid blank content blocks
if element.get("text", "").strip():
assistants_part = BedrockContentBlock(
text=element["text"]
)
assistants_parts.append(assistants_part)
elif element["type"] == "image_url":
if isinstance(element["image_url"], dict):
image_url = element["image_url"]["url"]
@@ -4015,9 +4017,12 @@ class BedrockConverseMessagesProcessor:
elif _assistant_content is not None and isinstance(
_assistant_content, str
):
assistant_content.append(
BedrockContentBlock(text=_assistant_content)
)
# Skip completely empty strings to avoid blank content blocks
if _assistant_content.strip():
assistant_content.append(
BedrockContentBlock(text=_assistant_content)
)
# If content is empty/whitespace, skip it (don't add a placeholder)
# Add cache point block for assistant string content
_cache_point_block = (
litellm.AmazonConverseConfig()._get_cache_point_block(
@@ -4348,12 +4353,11 @@ def _bedrock_converse_messages_pt( # noqa: PLR0915
assistant_parts=assistants_parts,
)
elif element["type"] == "text":
# AWS Bedrock doesn't allow empty or whitespace-only text content, so use placeholder for empty strings
text_content = (
element["text"] if element["text"].strip() else "."
)
assistants_part = BedrockContentBlock(text=text_content)
assistants_parts.append(assistants_part)
# AWS Bedrock doesn't allow empty or whitespace-only text content
# Skip completely empty strings to avoid blank content blocks
if element.get("text", "").strip():
assistants_part = BedrockContentBlock(text=element["text"])
assistants_parts.append(assistants_part)
elif element["type"] == "image_url":
if isinstance(element["image_url"], dict):
image_url = element["image_url"]["url"]
@@ -4376,9 +4380,9 @@ def _bedrock_converse_messages_pt( # noqa: PLR0915
assistants_parts.append(_cache_point_block)
assistant_content.extend(assistants_parts)
elif _assistant_content is not None and isinstance(_assistant_content, str):
# AWS Bedrock doesn't allow empty or whitespace-only text content, so use placeholder for empty strings
text_content = _assistant_content if _assistant_content.strip() else "."
assistant_content.append(BedrockContentBlock(text=text_content))
# Skip completely empty strings to avoid blank content blocks
if _assistant_content.strip():
assistant_content.append(BedrockContentBlock(text=_assistant_content))
# Add cache point block for assistant string content
_cache_point_block = (
litellm.AmazonConverseConfig()._get_cache_point_block(
@@ -330,3 +330,118 @@ async def test_bedrock_converse_sync_async_parity_with_duplicates():
)
assert sync_result == async_result
# ---------------------------------------------------------------------------
# Empty content filtering tests
# ---------------------------------------------------------------------------
def test_bedrock_converse_filters_empty_assistant_content():
"""Verify that empty assistant content blocks are filtered out to avoid
Bedrock API errors about blank text fields."""
messages = [
{"role": "user", "content": "Say hello"},
{"role": "assistant", "content": "Hello"},
{"role": "assistant", "content": " there"},
{"role": "assistant", "content": "!"},
{"role": "assistant", "content": ""}, # Empty content
{"role": "assistant", "content": ""}, # Empty content
{"role": "user", "content": "How are you?"},
]
result = _bedrock_converse_messages_pt(messages, MODEL, PROVIDER)
# Should have 3 messages: user, assistant (with merged non-empty content), user
assert len(result) == 3
assert result[0]["role"] == "user"
assert result[1]["role"] == "assistant"
assert result[2]["role"] == "user"
# Assistant message should only contain non-empty text blocks
assistant_content = result[1]["content"]
text_blocks = [block for block in assistant_content if "text" in block]
assert len(text_blocks) == 3 # "Hello", " there", "!"
assert text_blocks[0]["text"] == "Hello"
assert text_blocks[1]["text"] == " there"
assert text_blocks[2]["text"] == "!"
@pytest.mark.asyncio
async def test_bedrock_converse_filters_empty_assistant_content_async():
"""Verify that the async path also filters empty assistant content blocks."""
messages = [
{"role": "user", "content": "Say hello"},
{"role": "assistant", "content": "Hello"},
{"role": "assistant", "content": " there"},
{"role": "assistant", "content": "!"},
{"role": "assistant", "content": ""}, # Empty content
{"role": "assistant", "content": ""}, # Empty content
{"role": "user", "content": "How are you?"},
]
result = await BedrockConverseMessagesProcessor._bedrock_converse_messages_pt_async(
messages, MODEL, PROVIDER
)
# Should have 3 messages: user, assistant (with merged non-empty content), user
assert len(result) == 3
assert result[0]["role"] == "user"
assert result[1]["role"] == "assistant"
assert result[2]["role"] == "user"
# Assistant message should only contain non-empty text blocks
assistant_content = result[1]["content"]
text_blocks = [block for block in assistant_content if "text" in block]
assert len(text_blocks) == 3 # "Hello", " there", "!"
assert text_blocks[0]["text"] == "Hello"
assert text_blocks[1]["text"] == " there"
assert text_blocks[2]["text"] == "!"
def test_bedrock_converse_filters_whitespace_only_content():
"""Verify that whitespace-only content is also filtered out."""
messages = [
{"role": "user", "content": "Test"},
{"role": "assistant", "content": "Response"},
{"role": "assistant", "content": " "}, # Whitespace only
{"role": "assistant", "content": "\n\t"}, # Whitespace only
{"role": "assistant", "content": ""}, # Empty
]
result = _bedrock_converse_messages_pt(messages, MODEL, PROVIDER)
# Should have 2 messages: user and assistant
assert len(result) == 2
assistant_content = result[1]["content"]
text_blocks = [block for block in assistant_content if "text" in block]
# Only "Response" should be present
assert len(text_blocks) == 1
assert text_blocks[0]["text"] == "Response"
def test_bedrock_converse_filters_empty_list_content():
"""Verify that empty text elements in list content are filtered out."""
messages = [
{"role": "user", "content": "Test"},
{
"role": "assistant",
"content": [
{"type": "text", "text": "Hello"},
{"type": "text", "text": ""}, # Empty
{"type": "text", "text": "World"},
{"type": "text", "text": " "}, # Whitespace only
],
},
]
result = _bedrock_converse_messages_pt(messages, MODEL, PROVIDER)
# Should have 2 messages: user and assistant
assert len(result) == 2
assistant_content = result[1]["content"]
text_blocks = [block for block in assistant_content if "text" in block]
# Only "Hello" and "World" should be present
assert len(text_blocks) == 2
assert text_blocks[0]["text"] == "Hello"
assert text_blocks[1]["text"] == "World"
@@ -2356,9 +2356,8 @@ def test_bedrock_no_default_message():
assistant_messages = [
msg for msg in formatted_messages if msg["role"] == "assistant"
]
assert len(assistant_messages) == 2
assert assistant_messages[0]["content"][0]["text"] == "."
assert assistant_messages[1]["content"][0]["text"] == "Valid response"
assert len(assistant_messages) == 1
assert assistant_messages[0]["content"][0]["text"] == "Valid response"
@pytest.mark.parametrize("top_k_param", ["top_k", "topK"])