diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index b1628d817..8a6d5116f 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -180,7 +180,7 @@ def get_config(self) -> BedrockConfig: def _should_include_tool_result_status(self) -> bool: """Determine whether to include tool result status based on current config.""" include_status = self.config.get("include_tool_result_status", "auto") - + if include_status is True: return True elif include_status is False: @@ -275,6 +275,7 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: """Format messages for Bedrock API compatibility. This function ensures messages conform to Bedrock's expected format by: + - Filtering out SDK_UNKNOWN_MEMBER content blocks - Cleaning tool result content blocks by removing additional fields that may be useful for retaining information in hooks but would cause Bedrock validation exceptions when presented with unexpected fields @@ -292,11 +293,17 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolResultBlock.html """ cleaned_messages = [] + filtered_unknown_members = False for message in messages: cleaned_content: list[ContentBlock] = [] for content_block in message["content"]: + # Filter out SDK_UNKNOWN_MEMBER content blocks + if "SDK_UNKNOWN_MEMBER" in content_block: + filtered_unknown_members = True + continue + if "toolResult" in content_block: # Create a new content block with only the cleaned toolResult tool_result: ToolResult = content_block["toolResult"] @@ -323,6 +330,12 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: # Create new message with cleaned content cleaned_message: Message = Message(content=cleaned_content, role=message["role"]) cleaned_messages.append(cleaned_message) + + if filtered_unknown_members: + logger.warning( + "Filtered out SDK_UNKNOWN_MEMBER content blocks from messages, consider upgrading boto3 version" + ) + return cleaned_messages def _has_blocked_guardrail(self, guardrail_data: dict[str, Any]) -> bool: diff --git a/tests/strands/models/test_bedrock.py b/tests/strands/models/test_bedrock.py index e0f7879c0..13918b6ea 100644 --- a/tests/strands/models/test_bedrock.py +++ b/tests/strands/models/test_bedrock.py @@ -1331,7 +1331,7 @@ def test_format_request_removes_status_field_when_configured(model, model_id): def test_auto_behavior_anthropic_vs_non_anthropic(bedrock_client): model_anthropic = BedrockModel(model_id="us.anthropic.claude-sonnet-4-20250514-v1:0") assert model_anthropic.get_config()["include_tool_result_status"] == "auto" - + model_non_anthropic = BedrockModel(model_id="amazon.titan-text-v1") assert model_non_anthropic.get_config()["include_tool_result_status"] == "auto" @@ -1339,7 +1339,7 @@ def test_auto_behavior_anthropic_vs_non_anthropic(bedrock_client): def test_explicit_boolean_values_preserved(bedrock_client): model = BedrockModel(model_id="us.anthropic.claude-sonnet-4-20250514-v1:0", include_tool_result_status=True) assert model.get_config()["include_tool_result_status"] is True - + model2 = BedrockModel(model_id="amazon.titan-text-v1", include_tool_result_status=False) assert model2.get_config()["include_tool_result_status"] is False """Test that format_request keeps status field by default for anthropic.claude models.""" @@ -1368,3 +1368,27 @@ def test_explicit_boolean_values_preserved(bedrock_client): expected = {"content": [{"text": "Tool output"}], "toolUseId": "tool123", "status": "success"} assert tool_result == expected assert "status" in tool_result + + +def test_format_request_filters_sdk_unknown_member_content_blocks(model, model_id, caplog): + """Test that format_request filters out SDK_UNKNOWN_MEMBER content blocks.""" + messages = [ + { + "role": "assistant", + "content": [ + {"text": "Hello"}, + {"SDK_UNKNOWN_MEMBER": {"name": "reasoningContent"}}, + {"text": "World"}, + ], + } + ] + + formatted_request = model.format_request(messages) + + content = formatted_request["messages"][0]["content"] + assert len(content) == 2 + assert content[0] == {"text": "Hello"} + assert content[1] == {"text": "World"} + + for block in content: + assert "SDK_UNKNOWN_MEMBER" not in block