Skip to content

Commit 8e96ea8

Browse files
authored
fix(bedrock): normalize empty toolResult content arrays in _format_bedrock_messages (#2123)
1 parent 6697d12 commit 8e96ea8

File tree

2 files changed

+89
-1
lines changed

2 files changed

+89
-1
lines changed

src/strands/models/bedrock.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,8 +601,15 @@ def _format_request_message_content(self, content: ContentBlock) -> dict[str, An
601601
# https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolResultBlock.html
602602
if "toolResult" in content:
603603
tool_result = content["toolResult"]
604+
# Normalize empty toolResult content arrays.
605+
# Some model providers (e.g., Nemotron) reject toolResult blocks with
606+
# content: [] via the Converse API, while others (e.g., Claude) accept
607+
# them. Replace empty content with a minimal text block to ensure
608+
# cross-model compatibility. This follows the same pattern as the
609+
# TypeScript SDK's _formatMessages in bedrock.ts.
610+
tool_result_content_list = tool_result.get("content") or [{"text": ""}]
604611
formatted_content: list[dict[str, Any]] = []
605-
for tool_result_content in tool_result["content"]:
612+
for tool_result_content in tool_result_content_list:
606613
if "json" in tool_result_content:
607614
# Handle json field since not in ContentBlock but valid in ToolResultContent
608615
formatted_content.append({"json": tool_result_content["json"]})

tests/strands/models/test_bedrock.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1607,6 +1607,87 @@ def test_format_request_cleans_tool_result_content_blocks(model, model_id):
16071607
assert "status" not in tool_result
16081608

16091609

1610+
def test_format_request_message_content_normalizes_empty_tool_result_content(model, model_id):
1611+
"""Test that _format_request_message_content replaces empty toolResult content with a minimal text block.
1612+
1613+
Some model providers (e.g., Nemotron) reject toolResult blocks with content: [] via the
1614+
Converse API, while others (e.g., Claude) accept them. The SDK should normalize empty
1615+
content arrays to ensure cross-model compatibility.
1616+
1617+
See: https://github.com/strands-agents/sdk-python/issues/2122
1618+
"""
1619+
messages = [
1620+
{"role": "user", "content": [{"text": "List tables"}]},
1621+
{
1622+
"role": "assistant",
1623+
"content": [
1624+
{"text": "Querying...\n"},
1625+
{"toolUse": {"toolUseId": "tool_001", "name": "run_query", "input": {"sql": "SELECT 1"}}},
1626+
],
1627+
},
1628+
{
1629+
"role": "user",
1630+
"content": [
1631+
{"toolResult": {"toolUseId": "tool_001", "content": []}},
1632+
],
1633+
},
1634+
]
1635+
1636+
formatted_request = model._format_request(messages)
1637+
1638+
tool_result = formatted_request["messages"][2]["content"][0]["toolResult"]
1639+
assert tool_result["content"] == [{"text": ""}], "Empty toolResult content should be normalized to [{'text': ''}]"
1640+
1641+
1642+
def test_format_request_message_content_does_not_mutate_empty_tool_result(model, model_id):
1643+
"""Test that normalizing empty toolResult content does not mutate the original messages."""
1644+
messages = [
1645+
{"role": "user", "content": [{"text": "List tables"}]},
1646+
{
1647+
"role": "assistant",
1648+
"content": [
1649+
{"toolUse": {"toolUseId": "tool_001", "name": "run_query", "input": {"sql": "SELECT 1"}}},
1650+
],
1651+
},
1652+
{
1653+
"role": "user",
1654+
"content": [
1655+
{"toolResult": {"toolUseId": "tool_001", "content": []}},
1656+
],
1657+
},
1658+
]
1659+
1660+
original_content = messages[2]["content"][0]["toolResult"]["content"]
1661+
model._format_request(messages)
1662+
1663+
assert original_content == [], "Original empty content list should not be mutated"
1664+
1665+
1666+
def test_format_request_message_content_preserves_nonempty_tool_result_content(model, model_id):
1667+
"""Test that _format_request_message_content does not modify non-empty toolResult content."""
1668+
messages = [
1669+
{"role": "user", "content": [{"text": "List tables"}]},
1670+
{
1671+
"role": "assistant",
1672+
"content": [
1673+
{"text": "Querying...\n"},
1674+
{"toolUse": {"toolUseId": "tool_001", "name": "run_query", "input": {"sql": "SELECT 1"}}},
1675+
],
1676+
},
1677+
{
1678+
"role": "user",
1679+
"content": [
1680+
{"toolResult": {"toolUseId": "tool_001", "content": [{"text": "some result"}]}},
1681+
],
1682+
},
1683+
]
1684+
1685+
formatted_request = model._format_request(messages)
1686+
1687+
tool_result = formatted_request["messages"][2]["content"][0]["toolResult"]
1688+
assert tool_result["content"] == [{"text": "some result"}]
1689+
1690+
16101691
def test_format_request_removes_status_field_when_configured(model, model_id):
16111692
model.update_config(include_tool_result_status=False)
16121693

0 commit comments

Comments
 (0)