From 208d081988b982b70ad781f9c8e1396a75c1f8d7 Mon Sep 17 00:00:00 2001 From: Rachit Mehta Date: Sun, 17 Aug 2025 16:51:40 -0700 Subject: [PATCH 01/14] fix: Remove status field from toolResult for non-claude 3 models in Bedrock model provider --- src/strands/models/bedrock.py | 17 ++++++-- src/strands/types/tools.py | 4 +- tests/strands/models/test_bedrock.py | 58 ++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index ace35640a..bc191e4f8 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -276,10 +276,19 @@ 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"] - ) + model_id = self.config.get("model_id") + if "claude-3" in model_id: + # Keep the status field for Claude models + cleaned_tool_result = ToolResult( + content=tool_result["content"], + toolUseId=tool_result["toolUseId"], + status=tool_result["status"], + ) + else: + # For non-Claude models, use ToolResult without status + cleaned_tool_result = ToolResult( + toolUseId=tool_result["toolUseId"], content=tool_result["content"] + ) cleaned_block: ContentBlock = {"toolResult": cleaned_tool_result} cleaned_content.append(cleaned_block) diff --git a/src/strands/types/tools.py b/src/strands/types/tools.py index bb7c874f6..918b9c0b8 100644 --- a/src/strands/types/tools.py +++ b/src/strands/types/tools.py @@ -7,7 +7,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, AsyncGenerator, Awaitable, Callable, Literal, Protocol, Union +from typing import TYPE_CHECKING, Any, AsyncGenerator, Awaitable, Callable, Literal, Optional, Protocol, Union from typing_extensions import TypedDict @@ -91,7 +91,7 @@ class ToolResult(TypedDict): """ content: list[ToolResultContent] - status: ToolResultStatus + status: Optional[ToolResultStatus] toolUseId: str diff --git a/tests/strands/models/test_bedrock.py b/tests/strands/models/test_bedrock.py index 09e508845..83449848d 100644 --- a/tests/strands/models/test_bedrock.py +++ b/tests/strands/models/test_bedrock.py @@ -1237,3 +1237,61 @@ def test_format_request_cleans_tool_result_content_blocks(model, model_id): assert tool_result == expected assert "extraField" not in tool_result assert "mcpMetadata" not in tool_result + + +def test_format_request_removes_status_field_for_non_claude_models(model, model_id): + """Test that format_request removes status field for non-Claude models.""" + # Update model to use a non-Claude model + model.update_config(model_id="us.writer.palmyra-x4-v1:0") + + messages = [ + { + "role": "user", + "content": [ + { + "toolResult": { + "content": [{"text": "Tool output"}], + "toolUseId": "tool123", + "status": "success", + } + }, + ], + } + ] + + formatted_request = model.format_request(messages) + + # Verify toolResult does not contain status field for non-Claude models + 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_format_request_keeps_status_field_for_claude_models(model, model_id): + """Test that format_request keeps status field for Claude models.""" + # Update model to use a Claude model + model.update_config(model_id="anthropic.claude-3-sonnet-20240229-v1:0") + + messages = [ + { + "role": "user", + "content": [ + { + "toolResult": { + "content": [{"text": "Tool output"}], + "toolUseId": "tool123", + "status": "success", + } + }, + ], + } + ] + + formatted_request = model.format_request(messages) + + # Verify toolResult contains status field for Claude models + 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 From 552737623c3906c7bd18b6c26c646594cf8c3577 Mon Sep 17 00:00:00 2001 From: Rachit Mehta Date: Sun, 17 Aug 2025 17:02:24 -0700 Subject: [PATCH 02/14] fix: Remove status field from toolResult for non-claude 3 models in Bedrock model provider --- tests/strands/models/test_bedrock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/strands/models/test_bedrock.py b/tests/strands/models/test_bedrock.py index 83449848d..25a5557d9 100644 --- a/tests/strands/models/test_bedrock.py +++ b/tests/strands/models/test_bedrock.py @@ -1233,7 +1233,7 @@ def test_format_request_cleans_tool_result_content_blocks(model, model_id): # 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 = {"content": [{"text": "Tool output"}], "toolUseId": "tool123"} assert tool_result == expected assert "extraField" not in tool_result assert "mcpMetadata" not in tool_result From a19d4f8df5851d0270244303e79c1ae04a41b116 Mon Sep 17 00:00:00 2001 From: Rachit Mehta Date: Tue, 19 Aug 2025 09:05:56 -0400 Subject: [PATCH 03/14] fix: Remove status field from toolResult for non-claude 3 models in Bedrock model provider --- src/strands/models/bedrock.py | 2 +- src/strands/types/tools.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index bc191e4f8..e20daf26c 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -277,7 +277,7 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: tool_result: ToolResult = content_block["toolResult"] model_id = self.config.get("model_id") - if "claude-3" in model_id: + if model_id and "claude-3" in model_id: # Keep the status field for Claude models cleaned_tool_result = ToolResult( content=tool_result["content"], diff --git a/src/strands/types/tools.py b/src/strands/types/tools.py index 918b9c0b8..86b5160d5 100644 --- a/src/strands/types/tools.py +++ b/src/strands/types/tools.py @@ -9,7 +9,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any, AsyncGenerator, Awaitable, Callable, Literal, Optional, Protocol, Union -from typing_extensions import TypedDict +from typing_extensions import NotRequired, TypedDict from .media import DocumentContent, ImageContent @@ -91,7 +91,7 @@ class ToolResult(TypedDict): """ content: list[ToolResultContent] - status: Optional[ToolResultStatus] + status: NotRequired[Optional[ToolResultStatus]] toolUseId: str From f2489f4177241f7a243b9a3f0952e7cd5d8f28b4 Mon Sep 17 00:00:00 2001 From: Rachit Mehta Date: Tue, 19 Aug 2025 09:14:27 -0400 Subject: [PATCH 04/14] fix: Remove status field from toolResult for non-claude 3 models in Bedrock model provider --- src/strands/models/bedrock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index e20daf26c..dae9ed1fc 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -277,7 +277,7 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: tool_result: ToolResult = content_block["toolResult"] model_id = self.config.get("model_id") - if model_id and "claude-3" in model_id: + if model_id and "claude" in model_id: # Keep the status field for Claude models cleaned_tool_result = ToolResult( content=tool_result["content"], From 88ec21474403eef3843a428dd8246af7efab2a33 Mon Sep 17 00:00:00 2001 From: Rachit Mehta Date: Wed, 20 Aug 2025 08:54:23 -0400 Subject: [PATCH 05/14] fix: Remove status field from toolResult for non-claude 3 models in Bedrock model provider --- src/strands/models/bedrock.py | 2 +- src/strands/types/tools.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index dae9ed1fc..08da61af2 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -277,7 +277,7 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: tool_result: ToolResult = content_block["toolResult"] model_id = self.config.get("model_id") - if model_id and "claude" in model_id: + if model_id and "anthropic.claude" in model_id: # Keep the status field for Claude models cleaned_tool_result = ToolResult( content=tool_result["content"], diff --git a/src/strands/types/tools.py b/src/strands/types/tools.py index 86b5160d5..d6fef6bab 100644 --- a/src/strands/types/tools.py +++ b/src/strands/types/tools.py @@ -91,7 +91,7 @@ class ToolResult(TypedDict): """ content: list[ToolResultContent] - status: NotRequired[Optional[ToolResultStatus]] + status: NotRequired[ToolResultStatus] toolUseId: str From b0dda32a82c517a5ee529b0134adcd1bb7afcf32 Mon Sep 17 00:00:00 2001 From: Rachit Mehta Date: Wed, 20 Aug 2025 08:57:28 -0400 Subject: [PATCH 06/14] fix: Remove status field from toolResult for non-claude 3 models in Bedrock model provider --- src/strands/types/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strands/types/tools.py b/src/strands/types/tools.py index d6fef6bab..459e9cea5 100644 --- a/src/strands/types/tools.py +++ b/src/strands/types/tools.py @@ -7,7 +7,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, AsyncGenerator, Awaitable, Callable, Literal, Optional, Protocol, Union +from typing import TYPE_CHECKING, Any, AsyncGenerator, Awaitable, Callable, Literal, Protocol, Union from typing_extensions import NotRequired, TypedDict From a919bb6e5a036d66c8617ec23084d26f6830b4d8 Mon Sep 17 00:00:00 2001 From: Rachit Mehta Date: Fri, 22 Aug 2025 10:20:11 -0400 Subject: [PATCH 07/14] fix: Remove status field from toolResult for non-claude 3 models in Bedrock model provider --- src/strands/models/bedrock.py | 17 +++++++++-------- tests/strands/models/test_bedrock.py | 21 +++++++++------------ 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index 08da61af2..2160896d3 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -68,6 +68,7 @@ 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") + remove_tool_result_status: Flag to remove status field from tool results. Defaults to auto-detect based on model. 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) @@ -89,6 +90,7 @@ class BedrockConfig(TypedDict, total=False): guardrail_redact_output_message: Optional[str] max_tokens: Optional[int] model_id: str + remove_tool_result_status: Optional[bool] stop_sequences: Optional[list[str]] streaming: Optional[bool] temperature: Optional[float] @@ -276,18 +278,17 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: # Create a new content block with only the cleaned toolResult tool_result: ToolResult = content_block["toolResult"] - model_id = self.config.get("model_id") - if model_id and "anthropic.claude" in model_id: - # Keep the status field for Claude models + if self.config.get("remove_tool_result_status", False): + # Remove status field when explicitly configured cleaned_tool_result = ToolResult( - content=tool_result["content"], - toolUseId=tool_result["toolUseId"], - status=tool_result["status"], + toolUseId=tool_result["toolUseId"], content=tool_result["content"] ) else: - # For non-Claude models, use ToolResult without status + # Keep status field by default cleaned_tool_result = ToolResult( - toolUseId=tool_result["toolUseId"], content=tool_result["content"] + content=tool_result["content"], + toolUseId=tool_result["toolUseId"], + status=tool_result["status"], ) cleaned_block: ContentBlock = {"toolResult": cleaned_tool_result} diff --git a/tests/strands/models/test_bedrock.py b/tests/strands/models/test_bedrock.py index 25a5557d9..1515cc317 100644 --- a/tests/strands/models/test_bedrock.py +++ b/tests/strands/models/test_bedrock.py @@ -1233,16 +1233,16 @@ def test_format_request_cleans_tool_result_content_blocks(model, model_id): # 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"} + expected = {"content": [{"text": "Tool output"}], "toolUseId": "tool123", "status": "success"} assert tool_result == expected assert "extraField" not in tool_result assert "mcpMetadata" not in tool_result -def test_format_request_removes_status_field_for_non_claude_models(model, model_id): - """Test that format_request removes status field for non-Claude models.""" - # Update model to use a non-Claude model - model.update_config(model_id="us.writer.palmyra-x4-v1:0") +def test_format_request_removes_status_field_when_configured(model, model_id): + """Test that format_request removes status field when remove_tool_result_status=True.""" + # Configure model to remove status field + model.update_config(remove_tool_result_status=True) messages = [ { @@ -1261,18 +1261,15 @@ def test_format_request_removes_status_field_for_non_claude_models(model, model_ formatted_request = model.format_request(messages) - # Verify toolResult does not contain status field for non-Claude models + # Verify toolResult does not contain status field when configured to remove 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_format_request_keeps_status_field_for_claude_models(model, model_id): - """Test that format_request keeps status field for Claude models.""" - # Update model to use a Claude model - model.update_config(model_id="anthropic.claude-3-sonnet-20240229-v1:0") - +def test_format_request_keeps_status_field_by_default(model, model_id): + """Test that format_request keeps status field by default.""" messages = [ { "role": "user", @@ -1290,7 +1287,7 @@ def test_format_request_keeps_status_field_for_claude_models(model, model_id): formatted_request = model.format_request(messages) - # Verify toolResult contains status field for Claude models + # 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 From c314ad853615bc3db74a958495a5ee56225c979d Mon Sep 17 00:00:00 2001 From: Rachit Mehta Date: Fri, 22 Aug 2025 10:22:13 -0400 Subject: [PATCH 08/14] fix: Remove status field from toolResult for non-claude 3 models in Bedrock model provider --- src/strands/models/bedrock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index 2160896d3..82fe4aae0 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -68,7 +68,7 @@ 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") - remove_tool_result_status: Flag to remove status field from tool results. Defaults to auto-detect based on model. + remove_tool_result_status: Flag to remove status field from tool results. Defaults to False. 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) From 004eaa601ecbdee07ed113719b82a52162f02ceb Mon Sep 17 00:00:00 2001 From: Rachit Mehta Date: Thu, 28 Aug 2025 10:40:18 -0400 Subject: [PATCH 09/14] fix: Remove status field from toolResult for non-claude 3 models in Bedrock model provider --- src/strands/models/bedrock.py | 6 +++--- src/strands/types/tools.py | 4 ++-- tests/strands/models/test_bedrock.py | 30 +++++++++++++++++++++++++++- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index 82fe4aae0..c8e6f4f8b 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -68,7 +68,7 @@ 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") - remove_tool_result_status: Flag to remove status field from tool results. Defaults to False. + remove_tool_result_status: Flag to remove status field from tool results. True removes status, False keeps status, "auto" keeps status. Defaults to None. 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) @@ -90,7 +90,7 @@ class BedrockConfig(TypedDict, total=False): guardrail_redact_output_message: Optional[str] max_tokens: Optional[int] model_id: str - remove_tool_result_status: Optional[bool] + remove_tool_result_status: Optional[Literal["auto"] | bool] stop_sequences: Optional[list[str]] streaming: Optional[bool] temperature: Optional[float] @@ -278,7 +278,7 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: # Create a new content block with only the cleaned toolResult tool_result: ToolResult = content_block["toolResult"] - if self.config.get("remove_tool_result_status", False): + if self.config.get("remove_tool_result_status") is True: # Remove status field when explicitly configured cleaned_tool_result = ToolResult( toolUseId=tool_result["toolUseId"], content=tool_result["content"] diff --git a/src/strands/types/tools.py b/src/strands/types/tools.py index 459e9cea5..bb7c874f6 100644 --- a/src/strands/types/tools.py +++ b/src/strands/types/tools.py @@ -9,7 +9,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Any, AsyncGenerator, Awaitable, Callable, Literal, Protocol, Union -from typing_extensions import NotRequired, TypedDict +from typing_extensions import TypedDict from .media import DocumentContent, ImageContent @@ -91,7 +91,7 @@ class ToolResult(TypedDict): """ content: list[ToolResultContent] - status: NotRequired[ToolResultStatus] + status: ToolResultStatus toolUseId: str diff --git a/tests/strands/models/test_bedrock.py b/tests/strands/models/test_bedrock.py index 1515cc317..c9a463e93 100644 --- a/tests/strands/models/test_bedrock.py +++ b/tests/strands/models/test_bedrock.py @@ -1268,7 +1268,35 @@ def test_format_request_removes_status_field_when_configured(model, model_id): assert "status" not in tool_result -def test_format_request_keeps_status_field_by_default(model, model_id): +def test_format_request_keeps_status_field_with_auto(model, model_id): + """Test that format_request keeps status field when remove_tool_result_status="auto".""" + # Configure model with auto setting + model.update_config(remove_tool_result_status="auto") + + messages = [ + { + "role": "user", + "content": [ + { + "toolResult": { + "content": [{"text": "Tool output"}], + "toolUseId": "tool123", + "status": "success", + } + }, + ], + } + ] + + formatted_request = model.format_request(messages) + + # Verify toolResult contains status field with auto setting + 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 + + """Test that format_request keeps status field by default.""" messages = [ { From d9d7c12241362cdea3ddff861c10feb31e194e10 Mon Sep 17 00:00:00 2001 From: Rachit Mehta Date: Thu, 28 Aug 2025 10:41:44 -0400 Subject: [PATCH 10/14] fix: Remove status field from toolResult for non-claude 3 models in Bedrock model provider --- src/strands/models/bedrock.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index c8e6f4f8b..5cf2195c1 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -68,7 +68,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") - remove_tool_result_status: Flag to remove status field from tool results. True removes status, False keeps status, "auto" keeps status. Defaults to None. + remove_tool_result_status: Flag to remove status field from tool results. + True removes status, False keeps status, "auto" keeps status. Defaults to None. 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) From 9139015f03d6a43dd793f9fa37f38a5c71415d86 Mon Sep 17 00:00:00 2001 From: Rachit Mehta Date: Thu, 28 Aug 2025 10:46:14 -0400 Subject: [PATCH 11/14] fix: Remove status field from toolResult for non-claude 3 models in Bedrock model provider --- src/strands/models/bedrock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index 5cf2195c1..2a0ac903e 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -281,7 +281,7 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: if self.config.get("remove_tool_result_status") is True: # Remove status field when explicitly configured - cleaned_tool_result = ToolResult( + cleaned_tool_result = ToolResult( # type: ignore[typeddict-item] toolUseId=tool_result["toolUseId"], content=tool_result["content"] ) else: From eadcff6276580db1528f867c894c47e0e2900077 Mon Sep 17 00:00:00 2001 From: Rachit Mehta Date: Wed, 3 Sep 2025 11:55:15 -0400 Subject: [PATCH 12/14] fix: Remove status field from toolResult for non-claude 3 models in Bedrock model provider --- src/strands/models/bedrock.py | 26 ++++++++++++--- tests/strands/models/test_bedrock.py | 50 ++++++++++------------------ 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index 2a0ac903e..c8905e344 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -34,6 +34,11 @@ "too many total text bytes", ] +# Models that should keep tool result status (remove_tool_result_status = False) +MODELS_KEEP_STATUS = [ + "anthropic.claude", +] + T = TypeVar("T", bound=BaseModel) @@ -69,7 +74,7 @@ class BedrockConfig(TypedDict, total=False): 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") remove_tool_result_status: Flag to remove status field from tool results. - True removes status, False keeps status, "auto" keeps status. Defaults to None. + True removes status, False keeps 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) @@ -117,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, remove_tool_result_status="auto") self.update_config(**model_config) logger.debug("config=<%s> | initializing", self.config) @@ -147,6 +152,12 @@ def __init__( ) logger.debug("region=<%s> | bedrock client created", self.client.meta.region_name) + + # Resolve "auto" value for remove_tool_result_status + if self.config.get("remove_tool_result_status") == "auto": + self.config["remove_tool_result_status"] = not any( + model in self.config["model_id"] for model in MODELS_KEEP_STATUS + ) @override def update_config(self, **model_config: Unpack[BedrockConfig]) -> None: # type: ignore @@ -156,6 +167,12 @@ def update_config(self, **model_config: Unpack[BedrockConfig]) -> None: # type: **model_config: Configuration overrides. """ self.config.update(model_config) + + # Resolve "auto" value for remove_tool_result_status if needed + if self.config.get("remove_tool_result_status") == "auto": + self.config["remove_tool_result_status"] = not any( + model in self.config["model_id"] for model in MODELS_KEEP_STATUS + ) @override def get_config(self) -> BedrockConfig: @@ -280,12 +297,12 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: tool_result: ToolResult = content_block["toolResult"] if self.config.get("remove_tool_result_status") is True: - # Remove status field when explicitly configured + # Remove status field cleaned_tool_result = ToolResult( # type: ignore[typeddict-item] toolUseId=tool_result["toolUseId"], content=tool_result["content"] ) else: - # Keep status field by default + # Keep status field cleaned_tool_result = ToolResult( content=tool_result["content"], toolUseId=tool_result["toolUseId"], @@ -301,7 +318,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: diff --git a/tests/strands/models/test_bedrock.py b/tests/strands/models/test_bedrock.py index c9a463e93..af604d858 100644 --- a/tests/strands/models/test_bedrock.py +++ b/tests/strands/models/test_bedrock.py @@ -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", @@ -1231,17 +1230,15 @@ 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): - """Test that format_request removes status field when remove_tool_result_status=True.""" - # Configure model to remove status field model.update_config(remove_tool_result_status=True) messages = [ @@ -1261,43 +1258,30 @@ def test_format_request_removes_status_field_when_configured(model, model_id): formatted_request = model.format_request(messages) - # Verify toolResult does not contain status field when configured to remove 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_format_request_keeps_status_field_with_auto(model, model_id): - """Test that format_request keeps status field when remove_tool_result_status="auto".""" - # Configure model with auto setting - model.update_config(remove_tool_result_status="auto") +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()["remove_tool_result_status"] is False + + model_non_anthropic = BedrockModel(model_id="amazon.titan-text-v1") + assert model_non_anthropic.get_config()["remove_tool_result_status"] is True - messages = [ - { - "role": "user", - "content": [ - { - "toolResult": { - "content": [{"text": "Tool output"}], - "toolUseId": "tool123", - "status": "success", - } - }, - ], - } - ] - - formatted_request = model.format_request(messages) - - # Verify toolResult contains status field with auto setting - 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 +def test_explicit_boolean_values_preserved(bedrock_client): + model = BedrockModel(model_id="us.anthropic.claude-sonnet-4-20250514-v1:0", remove_tool_result_status=True) + assert model.get_config()["remove_tool_result_status"] is True + + model2 = BedrockModel(model_id="amazon.titan-text-v1", remove_tool_result_status=False) + assert model2.get_config()["remove_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() - """Test that format_request keeps status field by default.""" messages = [ { "role": "user", From 0fe5e1b2568e4a651eb3064d0a77d586b4ca62da Mon Sep 17 00:00:00 2001 From: Rachit Mehta Date: Wed, 3 Sep 2025 13:45:52 -0400 Subject: [PATCH 13/14] fix: Remove status field from toolResult for non-claude 3 models in Bedrock model provider --- src/strands/models/bedrock.py | 27 +++++++++++++-------------- tests/strands/models/test_bedrock.py | 4 ++-- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index c8905e344..74ba064fb 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -35,7 +35,7 @@ ] # Models that should keep tool result status (remove_tool_result_status = False) -MODELS_KEEP_STATUS = [ +_MODELS_KEEP_STATUS = [ "anthropic.claude", ] @@ -152,12 +152,6 @@ def __init__( ) logger.debug("region=<%s> | bedrock client created", self.client.meta.region_name) - - # Resolve "auto" value for remove_tool_result_status - if self.config.get("remove_tool_result_status") == "auto": - self.config["remove_tool_result_status"] = not any( - model in self.config["model_id"] for model in MODELS_KEEP_STATUS - ) @override def update_config(self, **model_config: Unpack[BedrockConfig]) -> None: # type: ignore @@ -167,12 +161,6 @@ def update_config(self, **model_config: Unpack[BedrockConfig]) -> None: # type: **model_config: Configuration overrides. """ self.config.update(model_config) - - # Resolve "auto" value for remove_tool_result_status if needed - if self.config.get("remove_tool_result_status") == "auto": - self.config["remove_tool_result_status"] = not any( - model in self.config["model_id"] for model in MODELS_KEEP_STATUS - ) @override def get_config(self) -> BedrockConfig: @@ -183,6 +171,17 @@ def get_config(self) -> BedrockConfig: """ return self.config + def _should_remove_tool_result_status(self) -> bool: + """Determine whether to remove tool result status based on current config.""" + remove_status = self.config.get("remove_tool_result_status", "auto") + + if remove_status is True: + return True + elif remove_status is False: + return False + else: # "auto" + return not any(model in self.config["model_id"] for model in _MODELS_KEEP_STATUS) + def format_request( self, messages: Messages, @@ -296,7 +295,7 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages: # Create a new content block with only the cleaned toolResult tool_result: ToolResult = content_block["toolResult"] - if self.config.get("remove_tool_result_status") is True: + if self._should_remove_tool_result_status(): # Remove status field cleaned_tool_result = ToolResult( # type: ignore[typeddict-item] toolUseId=tool_result["toolUseId"], content=tool_result["content"] diff --git a/tests/strands/models/test_bedrock.py b/tests/strands/models/test_bedrock.py index af604d858..f40a3bfda 100644 --- a/tests/strands/models/test_bedrock.py +++ b/tests/strands/models/test_bedrock.py @@ -1266,10 +1266,10 @@ 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()["remove_tool_result_status"] is False + assert model_anthropic.get_config()["remove_tool_result_status"] == "auto" model_non_anthropic = BedrockModel(model_id="amazon.titan-text-v1") - assert model_non_anthropic.get_config()["remove_tool_result_status"] is True + assert model_non_anthropic.get_config()["remove_tool_result_status"] == "auto" def test_explicit_boolean_values_preserved(bedrock_client): From 1e6e5d19ebaa7fa5f80db9b1060370ad82a8fa1f Mon Sep 17 00:00:00 2001 From: Rachit Mehta Date: Thu, 4 Sep 2025 10:55:34 -0400 Subject: [PATCH 14/14] fix: Remove status field from toolResult for non-claude 3 models in Bedrock model provider --- src/strands/models/bedrock.py | 38 ++++++++++++++-------------- tests/strands/models/test_bedrock.py | 14 +++++----- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index 74ba064fb..b86b7ee6c 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -34,8 +34,8 @@ "too many total text bytes", ] -# Models that should keep tool result status (remove_tool_result_status = False) -_MODELS_KEEP_STATUS = [ +# Models that should include tool result status (include_tool_result_status = True) +_MODELS_INCLUDE_STATUS = [ "anthropic.claude", ] @@ -73,8 +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") - remove_tool_result_status: Flag to remove status field from tool results. - True removes status, False keeps status, "auto" determines based on model_id. Defaults to "auto". + 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) @@ -96,7 +96,7 @@ class BedrockConfig(TypedDict, total=False): guardrail_redact_output_message: Optional[str] max_tokens: Optional[int] model_id: str - remove_tool_result_status: Optional[Literal["auto"] | bool] + include_tool_result_status: Optional[Literal["auto"] | bool] stop_sequences: Optional[list[str]] streaming: Optional[bool] temperature: Optional[float] @@ -122,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, remove_tool_result_status="auto") + 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) @@ -171,16 +171,16 @@ def get_config(self) -> BedrockConfig: """ return self.config - def _should_remove_tool_result_status(self) -> bool: - """Determine whether to remove tool result status based on current config.""" - remove_status = self.config.get("remove_tool_result_status", "auto") + 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 remove_status is True: + if include_status is True: return True - elif remove_status is False: + elif include_status is False: return False else: # "auto" - return not any(model in self.config["model_id"] for model in _MODELS_KEEP_STATUS) + return any(model in self.config["model_id"] for model in _MODELS_INCLUDE_STATUS) def format_request( self, @@ -295,18 +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"] - if self._should_remove_tool_result_status(): - # Remove status field - cleaned_tool_result = ToolResult( # type: ignore[typeddict-item] - toolUseId=tool_result["toolUseId"], content=tool_result["content"] - ) - else: - # Keep status field + 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) diff --git a/tests/strands/models/test_bedrock.py b/tests/strands/models/test_bedrock.py index f40a3bfda..ff150925f 100644 --- a/tests/strands/models/test_bedrock.py +++ b/tests/strands/models/test_bedrock.py @@ -1239,7 +1239,7 @@ def test_format_request_cleans_tool_result_content_blocks(model, model_id): def test_format_request_removes_status_field_when_configured(model, model_id): - model.update_config(remove_tool_result_status=True) + model.update_config(include_tool_result_status=False) messages = [ { @@ -1266,18 +1266,18 @@ 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()["remove_tool_result_status"] == "auto" + 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()["remove_tool_result_status"] == "auto" + 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", remove_tool_result_status=True) - assert model.get_config()["remove_tool_result_status"] is True + 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", remove_tool_result_status=False) - assert model2.get_config()["remove_tool_result_status"] is False + 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()