diff --git a/src/strands/models/bedrock.py b/src/strands/models/bedrock.py index a5ffb539d..5448f96e0 100644 --- a/src/strands/models/bedrock.py +++ b/src/strands/models/bedrock.py @@ -380,6 +380,32 @@ def stream(self, request: dict[str, Any]) -> Iterable[StreamEvent]: logger.warning("bedrock threw context window overflow error") raise ContextWindowOverflowException(e) from e + region = self.client.meta.region_name + + # add_note added in Python 3.11 + if hasattr(e, "add_note"): + # Aid in debugging by adding more information + e.add_note(f"└ Bedrock region: {region}") + e.add_note(f"└ Model id: {self.config.get('model_id')}") + + if ( + e.response["Error"]["Code"] == "AccessDeniedException" + and "You don't have access to the model" in error_message + ): + e.add_note( + "└ For more information see " + "https://strandsagents.com/user-guide/concepts/model-providers/amazon-bedrock/#model-access-issue" + ) + + if ( + e.response["Error"]["Code"] == "ValidationException" + and "with on-demand throughput isn’t supported" in error_message + ): + e.add_note( + "└ For more information see " + "https://strandsagents.com/latest/user-guide/concepts/model-providers/amazon-bedrock/#on-demand-throughput-isnt-supported" + ) + # Otherwise raise the error raise e diff --git a/tests/strands/models/test_bedrock.py b/tests/strands/models/test_bedrock.py index 137b57c8c..3ed72973b 100644 --- a/tests/strands/models/test_bedrock.py +++ b/tests/strands/models/test_bedrock.py @@ -1,4 +1,5 @@ import os +import sys import unittest.mock import boto3 @@ -15,7 +16,10 @@ @pytest.fixture def bedrock_client(): with unittest.mock.patch.object(strands.models.bedrock.boto3, "Session") as mock_session_cls: - yield mock_session_cls.return_value.client.return_value + mock_client = mock_session_cls.return_value.client.return_value + mock_client.meta = unittest.mock.MagicMock() + mock_client.meta.region_name = "us-west-2" + yield mock_client @pytest.fixture @@ -1029,3 +1033,80 @@ def test_converse_output_guardrails_redacts_output(bedrock_client): bedrock_client.converse.assert_called_once() bedrock_client.converse_stream.assert_not_called() + + +@pytest.mark.skipif(sys.version_info < (3, 11), reason="This test requires Python 3.11 or higher (need add_note)") +def test_add_note_on_client_error(bedrock_client, model): + """Test that add_note is called on ClientError with region and model ID information.""" + # Mock the client error response + error_response = {"Error": {"Code": "ValidationException", "Message": "Some error message"}} + bedrock_client.converse_stream.side_effect = ClientError(error_response, "ConversationStream") + + # Call the stream method which should catch and add notes to the exception + with pytest.raises(ClientError) as err: + list(model.stream({"modelId": "test-model"})) + + assert err.value.__notes__ == ["└ Bedrock region: us-west-2", "└ Model id: m1"] + + +def test_no_add_note_when_not_available(bedrock_client, model): + """Verify that on any python version (even < 3.11 where add_note is not available, we get the right exception).""" + # Mock the client error response + error_response = {"Error": {"Code": "ValidationException", "Message": "Some error message"}} + bedrock_client.converse_stream.side_effect = ClientError(error_response, "ConversationStream") + + # Call the stream method which should catch and add notes to the exception + with pytest.raises(ClientError): + list(model.stream({"modelId": "test-model"})) + + +@pytest.mark.skipif(sys.version_info < (3, 11), reason="This test requires Python 3.11 or higher (need add_note)") +def test_add_note_on_access_denied_exception(bedrock_client, model): + """Test that add_note adds documentation link for AccessDeniedException.""" + # Mock the client error response for access denied + error_response = { + "Error": { + "Code": "AccessDeniedException", + "Message": "An error occurred (AccessDeniedException) when calling the ConverseStream operation: " + "You don't have access to the model with the specified model ID.", + } + } + bedrock_client.converse_stream.side_effect = ClientError(error_response, "ConversationStream") + + # Call the stream method which should catch and add notes to the exception + with pytest.raises(ClientError) as err: + list(model.stream({"modelId": "test-model"})) + + assert err.value.__notes__ == [ + "└ Bedrock region: us-west-2", + "└ Model id: m1", + "└ For more information see " + "https://strandsagents.com/user-guide/concepts/model-providers/amazon-bedrock/#model-access-issue", + ] + + +@pytest.mark.skipif(sys.version_info < (3, 11), reason="This test requires Python 3.11 or higher (need add_note)") +def test_add_note_on_validation_exception_throughput(bedrock_client, model): + """Test that add_note adds documentation link for ValidationException about on-demand throughput.""" + # Mock the client error response for validation exception + error_response = { + "Error": { + "Code": "ValidationException", + "Message": "An error occurred (ValidationException) when calling the ConverseStream operation: " + "Invocation of model ID anthropic.claude-3-7-sonnet-20250219-v1:0 with on-demand throughput " + "isn’t supported. Retry your request with the ID or ARN of an inference profile that contains " + "this model.", + } + } + bedrock_client.converse_stream.side_effect = ClientError(error_response, "ConversationStream") + + # Call the stream method which should catch and add notes to the exception + with pytest.raises(ClientError) as err: + list(model.stream({"modelId": "test-model"})) + + assert err.value.__notes__ == [ + "└ Bedrock region: us-west-2", + "└ Model id: m1", + "└ For more information see " + "https://strandsagents.com/latest/user-guide/concepts/model-providers/amazon-bedrock/#on-demand-throughput-isnt-supported", + ]