From 429f6245d8a2ec5a5f2d7bd42f079ddb4efbd7bc Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Mon, 1 Dec 2025 14:43:40 -0800 Subject: [PATCH 01/12] add defensive check for from_converse response --- .../botocore/extensions/bedrock_utils.py | 3 ++- .../tests/test_botocore_bedrock.py | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py index 2260d8ac73..8741321753 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py @@ -528,7 +528,8 @@ def __init__( def from_converse( cls, response: dict[str, Any], capture_content: bool ) -> _Choice: - orig_message = response["output"]["message"] + output = response.get("output", {}) + orig_message = output.get("message", {}) if role := orig_message.get("role"): message = {"role": role} else: diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py index da8194c6a8..4774a0450d 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py @@ -26,6 +26,7 @@ from botocore.response import StreamingBody from opentelemetry.instrumentation.botocore.extensions.bedrock_utils import ( + ConverseStreamWrapper, InvokeModelWithResponseStreamWrapper, ) from opentelemetry.semconv._incubating.attributes.error_attributes import ( @@ -3049,7 +3050,21 @@ def stream_error_callback(exc, ended): assert isinstance(tool_block["input"], dict) else: assert "input" not in tool_block - +def test_converse_stream_with_malformed_response(): + """Test that converse stream handles malformed response missing output key.""" + def stream_done_callback(response, ended): + pass + + wrapper = ConverseStreamWrapper( + stream=mock.MagicMock(), + stream_done_callback=stream_done_callback, + model_id="amazon.nova-micro-v1:0", + ) + + malformed_response = {"stopReason": "end_turn"} + result = wrapper._complete_stream(malformed_response) + assert result is None + def amazon_nova_messages(): return [ From 0861f9f95aa0a2712affd13dfd5f32091cd8e83c Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Mon, 1 Dec 2025 15:03:26 -0800 Subject: [PATCH 02/12] add defensive check for from_converse response --- CHANGELOG.md | 2 ++ .../tests/test_botocore_bedrock.py | 14 ++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cbbe7aa3b..2a8fbb2f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- `opentelemetry-instrumentation-botocore`: bedrock: Add safety check for bedrock ConverseStream responses + ([#3990](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/TBD)) - `opentelemetry-instrumentation-botocore`: bedrock: only decode JSON input buffer in Anthropic Claude streaming ([#3875](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3875)) - `opentelemetry-instrumentation-aiohttp-client`, `opentelemetry-instrumentation-aiohttp-server`: Fix readme links and text diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py index 4774a0450d..a3e40ccdf2 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py @@ -3050,21 +3050,23 @@ def stream_error_callback(exc, ended): assert isinstance(tool_block["input"], dict) else: assert "input" not in tool_block -def test_converse_stream_with_malformed_response(): + + +def test_converse_stream_with_missing_output_in_response(): """Test that converse stream handles malformed response missing output key.""" + def stream_done_callback(response, ended): pass - + wrapper = ConverseStreamWrapper( stream=mock.MagicMock(), stream_done_callback=stream_done_callback, model_id="amazon.nova-micro-v1:0", ) - - malformed_response = {"stopReason": "end_turn"} - result = wrapper._complete_stream(malformed_response) + + bedrock_response = {"stopReason": "end_turn"} + result = wrapper._complete_stream(bedrock_response) assert result is None - def amazon_nova_messages(): return [ From 24ed6f720d66650d4b1f71f2da9c857ea57a547f Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Tue, 2 Dec 2025 10:06:14 +0100 Subject: [PATCH 03/12] Update instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py --- .../instrumentation/botocore/extensions/bedrock_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py index 8741321753..d1cd1e680a 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py @@ -528,6 +528,7 @@ def __init__( def from_converse( cls, response: dict[str, Any], capture_content: bool ) -> _Choice: + # be defensive about malformed responses, refer to #3958 for more context output = response.get("output", {}) orig_message = output.get("message", {}) if role := orig_message.get("role"): From 753b370545217dc6d4bbff46e6838a9267a444e2 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Tue, 2 Dec 2025 10:08:29 +0100 Subject: [PATCH 04/12] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a8fbb2f9e..bfef4b9cff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,7 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - `opentelemetry-instrumentation-botocore`: bedrock: Add safety check for bedrock ConverseStream responses - ([#3990](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/TBD)) + ([#3990](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3990)) - `opentelemetry-instrumentation-botocore`: bedrock: only decode JSON input buffer in Anthropic Claude streaming ([#3875](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3875)) - `opentelemetry-instrumentation-aiohttp-client`, `opentelemetry-instrumentation-aiohttp-server`: Fix readme links and text From c348072742bda30423e1d2e15fb9b7af590b13bb Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Tue, 2 Dec 2025 08:47:42 -0800 Subject: [PATCH 05/12] add comment for malformed repsonses in from_converse --- .../instrumentation/botocore/extensions/bedrock_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py index 8741321753..68e4a0cef7 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py @@ -529,6 +529,8 @@ def from_converse( cls, response: dict[str, Any], capture_content: bool ) -> _Choice: output = response.get("output", {}) + # be defensive about malformed responses + # refer to https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3958 for more context orig_message = output.get("message", {}) if role := orig_message.get("role"): message = {"role": role} From cf6012a039e2ea84a9e8ca04f18b0a355ee192fe Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Tue, 2 Dec 2025 08:49:32 -0800 Subject: [PATCH 06/12] merge suggested changes --- .../instrumentation/botocore/extensions/bedrock_utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py index 227e3799e5..d1cd1e680a 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py @@ -530,8 +530,6 @@ def from_converse( ) -> _Choice: # be defensive about malformed responses, refer to #3958 for more context output = response.get("output", {}) - # be defensive about malformed responses - # refer to https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3958 for more context orig_message = output.get("message", {}) if role := orig_message.get("role"): message = {"role": role} From ed477de1e04dae575094330664ed3f5857e2b15c Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Tue, 2 Dec 2025 09:14:53 -0800 Subject: [PATCH 07/12] fix botocore converse stream test --- .../tests/test_botocore_bedrock.py | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py index a3e40ccdf2..78acbbd47c 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py @@ -26,8 +26,8 @@ from botocore.response import StreamingBody from opentelemetry.instrumentation.botocore.extensions.bedrock_utils import ( - ConverseStreamWrapper, InvokeModelWithResponseStreamWrapper, + _Choice, ) from opentelemetry.semconv._incubating.attributes.error_attributes import ( ERROR_TYPE, @@ -3053,20 +3053,14 @@ def stream_error_callback(exc, ended): def test_converse_stream_with_missing_output_in_response(): - """Test that converse stream handles malformed response missing output key.""" + # Test malformed response missing "output" key + malformed_response = {"stopReason": "end_turn"} + choice = _Choice.from_converse(malformed_response, capture_content=False) - def stream_done_callback(response, ended): - pass - - wrapper = ConverseStreamWrapper( - stream=mock.MagicMock(), - stream_done_callback=stream_done_callback, - model_id="amazon.nova-micro-v1:0", - ) + assert choice.finish_reason == "end_turn" + assert choice.message == {} + assert choice.index == 0 - bedrock_response = {"stopReason": "end_turn"} - result = wrapper._complete_stream(bedrock_response) - assert result is None def amazon_nova_messages(): return [ From 568b6bc0bbd177de7a2279a15612cf72bfcd9953 Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Tue, 2 Dec 2025 09:16:30 -0800 Subject: [PATCH 08/12] lint fix --- .../tests/test_botocore_bedrock.py | 1 - 1 file changed, 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py index 78acbbd47c..7fd74f131e 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py @@ -3061,7 +3061,6 @@ def test_converse_stream_with_missing_output_in_response(): assert choice.message == {} assert choice.index == 0 - def amazon_nova_messages(): return [ {"role": "user", "content": [{"text": "Say this is a test"}]}, From ace58d206f94bb176dca1a037262956439b1c974 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Tue, 2 Dec 2025 19:29:43 +0100 Subject: [PATCH 09/12] Update instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py --- .../tests/test_botocore_bedrock.py | 1 + 1 file changed, 1 insertion(+) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py index 7fd74f131e..78acbbd47c 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py @@ -3061,6 +3061,7 @@ def test_converse_stream_with_missing_output_in_response(): assert choice.message == {} assert choice.index == 0 + def amazon_nova_messages(): return [ {"role": "user", "content": [{"text": "Say this is a test"}]}, From b019d66fcba4f64dfaa1a3dbb69533462b0488a7 Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Tue, 2 Dec 2025 14:40:02 -0800 Subject: [PATCH 10/12] add defensive check just in case content doesn't exist in orig_message --- .../instrumentation/botocore/extensions/bedrock_utils.py | 4 ++-- .../tests/test_botocore_bedrock.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py index d1cd1e680a..0b164b1f92 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py @@ -539,8 +539,8 @@ def from_converse( if tool_calls := extract_tool_calls(orig_message, capture_content): message["tool_calls"] = tool_calls - elif capture_content: - message["content"] = orig_message["content"] + elif capture_content and (content := orig_message.get("content")): + message["content"] = content return cls(message, response["stopReason"], index=0) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py index 7fd74f131e..c460629033 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py @@ -3055,7 +3055,7 @@ def stream_error_callback(exc, ended): def test_converse_stream_with_missing_output_in_response(): # Test malformed response missing "output" key malformed_response = {"stopReason": "end_turn"} - choice = _Choice.from_converse(malformed_response, capture_content=False) + choice = _Choice.from_converse(malformed_response, capture_content=True) assert choice.finish_reason == "end_turn" assert choice.message == {} From 331e4e94ef68ace0735b29b34defafc6dac9a186 Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Tue, 2 Dec 2025 14:41:26 -0800 Subject: [PATCH 11/12] lint fix --- .../tests/test_botocore_bedrock.py | 1 - 1 file changed, 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py index a8a0afe112..c460629033 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py @@ -3061,7 +3061,6 @@ def test_converse_stream_with_missing_output_in_response(): assert choice.message == {} assert choice.index == 0 - def amazon_nova_messages(): return [ {"role": "user", "content": [{"text": "Say this is a test"}]}, From 663c016bf4db1bf2d4fb5db56da44b77a8a5a650 Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Tue, 2 Dec 2025 14:43:00 -0800 Subject: [PATCH 12/12] new line --- .../tests/test_botocore_bedrock.py | 1 + 1 file changed, 1 insertion(+) diff --git a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py index c460629033..a8a0afe112 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py @@ -3061,6 +3061,7 @@ def test_converse_stream_with_missing_output_in_response(): assert choice.message == {} assert choice.index == 0 + def amazon_nova_messages(): return [ {"role": "user", "content": [{"text": "Say this is a test"}]},