Merge pull request #24368 from Chesars/fix/bedrock-converse-content-block-ordering

fix(bedrock): sort assistant content blocks so text precedes toolUse
This commit is contained in:
Cesar Garcia
2026-03-22 18:45:22 -03:00
committed by GitHub
2 changed files with 167 additions and 0 deletions
@@ -4051,6 +4051,36 @@ def _deduplicate_bedrock_tool_content(
return _deduplicate_bedrock_content_blocks(tool_content, "toolResult")
def _sort_bedrock_assistant_content_blocks(
blocks: List[BedrockContentBlock],
) -> List[BedrockContentBlock]:
"""
Sort assistant content blocks so that ``text`` blocks appear before
``toolUse`` blocks.
Bedrock requires all ``text`` blocks to precede any ``toolUse`` blocks
within an assistant message. When the Responses API converts
function_call items before message items, the resulting ``toolUse``
blocks can end up before ``text`` blocks, causing Bedrock to reject
the request with a 400 error because the ``toolUse`` → ``toolResult``
pairing is broken by the intervening ``text`` block.
Sort order (stable):
0 - reasoningContent
1 - text / image / document / video / other non-tool blocks
2 - toolUse
"""
def _sort_key(block: BedrockContentBlock) -> int:
if "reasoningContent" in block:
return 0
if "toolUse" in block:
return 2
return 1
return sorted(blocks, key=_sort_key)
def _insert_assistant_continue_message(
messages: List[BedrockMessageBlock],
assistant_continue_message: Optional[
@@ -4642,6 +4672,9 @@ class BedrockConverseMessagesProcessor:
assistant_content = _deduplicate_bedrock_content_blocks(
assistant_content, "toolUse"
)
assistant_content = _sort_bedrock_assistant_content_blocks(
assistant_content
)
if assistant_content:
contents.append(
@@ -5007,6 +5040,9 @@ def _bedrock_converse_messages_pt( # noqa: PLR0915
assistant_content = _deduplicate_bedrock_content_blocks(
assistant_content, "toolUse"
)
assistant_content = _sort_bedrock_assistant_content_blocks(
assistant_content
)
if assistant_content:
contents.append(
@@ -9,6 +9,7 @@ from litellm.litellm_core_utils.prompt_templates.factory import (
_bedrock_converse_messages_pt,
_deduplicate_bedrock_content_blocks,
_deduplicate_bedrock_tool_content,
_sort_bedrock_assistant_content_blocks,
BedrockConverseMessagesProcessor,
)
@@ -445,3 +446,133 @@ def test_bedrock_converse_filters_empty_list_content():
assert len(text_blocks) == 2
assert text_blocks[0]["text"] == "Hello"
assert text_blocks[1]["text"] == "World"
# ---------------------------------------------------------------------------
# Content block ordering tests (text before toolUse)
# ---------------------------------------------------------------------------
def _make_tooluse_before_text_messages():
"""Return messages where the assistant message has a tool_call followed by
a separate assistant message with text content. When merged, the toolUse
block would end up before the text block without sorting."""
return [
{"role": "user", "content": "What's the weather?"},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "tooluse_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": '{"location": "Paris"}',
},
}
],
},
{
"role": "assistant",
"content": "Let me check the weather for you.",
},
{
"role": "tool",
"tool_call_id": "tooluse_abc123",
"content": '{"temp": 22}',
},
]
def test_sort_bedrock_assistant_content_blocks_text_before_tooluse():
"""Direct unit test: text blocks should come before toolUse blocks."""
blocks = [
{"toolUse": {"toolUseId": "id_1", "name": "fn_a", "input": {}}},
{"text": "thinking..."},
]
result = _sort_bedrock_assistant_content_blocks(blocks)
assert len(result) == 2
assert "text" in result[0]
assert "toolUse" in result[1]
def test_sort_bedrock_assistant_content_blocks_reasoning_first():
"""reasoningContent blocks should come before text and toolUse."""
blocks = [
{"toolUse": {"toolUseId": "id_1", "name": "fn_a", "input": {}}},
{"text": "thinking..."},
{"reasoningContent": {"reasoningText": {"text": "reasoning"}}},
]
result = _sort_bedrock_assistant_content_blocks(blocks)
assert "reasoningContent" in result[0]
assert "text" in result[1]
assert "toolUse" in result[2]
def test_sort_bedrock_assistant_content_blocks_preserves_order_when_correct():
"""If blocks are already in the correct order, sorting should not change them."""
blocks = [
{"text": "hello"},
{"toolUse": {"toolUseId": "id_1", "name": "fn_a", "input": {}}},
{"toolUse": {"toolUseId": "id_2", "name": "fn_b", "input": {}}},
]
result = _sort_bedrock_assistant_content_blocks(blocks)
assert result == blocks
def test_bedrock_converse_sorts_text_before_tooluse_sync():
"""Verify the sync path sorts text blocks before toolUse blocks in
assistant messages."""
messages = _make_tooluse_before_text_messages()
result = _bedrock_converse_messages_pt(messages, MODEL, PROVIDER)
assistant_msgs = [msg for msg in result if msg["role"] == "assistant"]
assert len(assistant_msgs) == 1
content = assistant_msgs[0]["content"]
text_indices = [i for i, b in enumerate(content) if "text" in b]
tool_indices = [i for i, b in enumerate(content) if "toolUse" in b]
# All text blocks must come before all toolUse blocks
assert max(text_indices) < min(tool_indices), (
f"text blocks at {text_indices} should all precede toolUse blocks at {tool_indices}"
)
@pytest.mark.asyncio
async def test_bedrock_converse_sorts_text_before_tooluse_async():
"""Verify the async path sorts text blocks before toolUse blocks in
assistant messages."""
messages = _make_tooluse_before_text_messages()
result = await BedrockConverseMessagesProcessor._bedrock_converse_messages_pt_async(
messages, MODEL, PROVIDER
)
assistant_msgs = [msg for msg in result if msg["role"] == "assistant"]
assert len(assistant_msgs) == 1
content = assistant_msgs[0]["content"]
text_indices = [i for i, b in enumerate(content) if "text" in b]
tool_indices = [i for i, b in enumerate(content) if "toolUse" in b]
assert max(text_indices) < min(tool_indices), (
f"text blocks at {text_indices} should all precede toolUse blocks at {tool_indices}"
)
@pytest.mark.asyncio
async def test_bedrock_converse_content_ordering_sync_async_parity():
"""Sync and async paths should produce identical content block ordering."""
messages = _make_tooluse_before_text_messages()
sync_result = _bedrock_converse_messages_pt(messages, MODEL, PROVIDER)
async_result = await BedrockConverseMessagesProcessor._bedrock_converse_messages_pt_async(
messages, MODEL, PROVIDER
)
assert sync_result == async_result