Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions src/strands/models/bedrock.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
"too many total text bytes",
]

# Models that should include tool result status (include_tool_result_status = True)
_MODELS_INCLUDE_STATUS = [
"anthropic.claude",
]

T = TypeVar("T", bound=BaseModel)


Expand Down Expand Up @@ -68,6 +73,8 @@ class BedrockConfig(TypedDict, total=False):
guardrail_redact_output_message: If a Bedrock Output guardrail triggers, replace output with this message.
max_tokens: Maximum number of tokens to generate in the response
model_id: The Bedrock model ID (e.g., "us.anthropic.claude-sonnet-4-20250514-v1:0")
include_tool_result_status: Flag to include status field in tool results.
True includes status, False removes status, "auto" determines based on model_id. Defaults to "auto".
stop_sequences: List of sequences that will stop generation when encountered
streaming: Flag to enable/disable streaming. Defaults to True.
temperature: Controls randomness in generation (higher = more random)
Expand All @@ -89,6 +96,7 @@ class BedrockConfig(TypedDict, total=False):
guardrail_redact_output_message: Optional[str]
max_tokens: Optional[int]
model_id: str
include_tool_result_status: Optional[Literal["auto"] | bool]
stop_sequences: Optional[list[str]]
streaming: Optional[bool]
temperature: Optional[float]
Expand All @@ -114,7 +122,7 @@ def __init__(
if region_name and boto_session:
raise ValueError("Cannot specify both `region_name` and `boto_session`.")

self.config = BedrockModel.BedrockConfig(model_id=DEFAULT_BEDROCK_MODEL_ID)
self.config = BedrockModel.BedrockConfig(model_id=DEFAULT_BEDROCK_MODEL_ID, include_tool_result_status="auto")
self.update_config(**model_config)

logger.debug("config=<%s> | initializing", self.config)
Expand Down Expand Up @@ -163,6 +171,17 @@ def get_config(self) -> BedrockConfig:
"""
return self.config

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:
return False
else: # "auto"
return any(model in self.config["model_id"] for model in _MODELS_INCLUDE_STATUS)

def format_request(
self,
messages: Messages,
Expand Down Expand Up @@ -276,10 +295,18 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages:
# Create a new content block with only the cleaned toolResult
tool_result: ToolResult = content_block["toolResult"]

# Keep only the required fields for Bedrock
cleaned_tool_result = ToolResult(
content=tool_result["content"], toolUseId=tool_result["toolUseId"], status=tool_result["status"]
)
if self._should_include_tool_result_status():
# Include status field
cleaned_tool_result = ToolResult(
content=tool_result["content"],
toolUseId=tool_result["toolUseId"],
status=tool_result["status"],
)
else:
# Remove status field
cleaned_tool_result = ToolResult( # type: ignore[typeddict-item]
toolUseId=tool_result["toolUseId"], content=tool_result["content"]
)

cleaned_block: ContentBlock = {"toolResult": cleaned_tool_result}
cleaned_content.append(cleaned_block)
Expand All @@ -290,7 +317,6 @@ 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)

return cleaned_messages

def _has_blocked_guardrail(self, guardrail_data: dict[str, Any]) -> bool:
Expand Down
73 changes: 70 additions & 3 deletions tests/strands/models/test_bedrock.py
Original file line number Diff line number Diff line change
Expand Up @@ -1211,7 +1211,6 @@ async def test_stream_logging(bedrock_client, model, messages, caplog, alist):


def test_format_request_cleans_tool_result_content_blocks(model, model_id):
"""Test that format_request cleans toolResult blocks by removing extra fields."""
messages = [
{
"role": "user",
Expand All @@ -1231,9 +1230,77 @@ def test_format_request_cleans_tool_result_content_blocks(model, model_id):

formatted_request = model.format_request(messages)

# Verify toolResult only contains allowed fields in the formatted request
tool_result = formatted_request["messages"][0]["content"][0]["toolResult"]
expected = {"content": [{"text": "Tool output"}], "toolUseId": "tool123", "status": "success"}
expected = {"toolUseId": "tool123", "content": [{"text": "Tool output"}]}
assert tool_result == expected
assert "extraField" not in tool_result
assert "mcpMetadata" not in tool_result
assert "status" not in tool_result


def test_format_request_removes_status_field_when_configured(model, model_id):
model.update_config(include_tool_result_status=False)

messages = [
{
"role": "user",
"content": [
{
"toolResult": {
"content": [{"text": "Tool output"}],
"toolUseId": "tool123",
"status": "success",
}
},
],
}
]

formatted_request = model.format_request(messages)

tool_result = formatted_request["messages"][0]["content"][0]["toolResult"]
expected = {"toolUseId": "tool123", "content": [{"text": "Tool output"}]}
assert tool_result == expected
assert "status" not in tool_result


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"


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."""
# Default model is anthropic.claude, so should keep status
model = BedrockModel()

messages = [
{
"role": "user",
"content": [
{
"toolResult": {
"content": [{"text": "Tool output"}],
"toolUseId": "tool123",
"status": "success",
}
},
],
}
]

formatted_request = model.format_request(messages)

# Verify toolResult contains status field by default
tool_result = formatted_request["messages"][0]["content"][0]["toolResult"]
expected = {"content": [{"text": "Tool output"}], "toolUseId": "tool123", "status": "success"}
assert tool_result == expected
assert "status" in tool_result
Loading