diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cbbe7aa3b..bfef4b9cff 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/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 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..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 @@ -528,7 +528,9 @@ def __init__( def from_converse( cls, response: dict[str, Any], capture_content: bool ) -> _Choice: - orig_message = response["output"]["message"] + # 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"): message = {"role": role} else: @@ -537,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 da8194c6a8..a8a0afe112 100644 --- a/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py +++ b/instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py @@ -27,6 +27,7 @@ from opentelemetry.instrumentation.botocore.extensions.bedrock_utils import ( InvokeModelWithResponseStreamWrapper, + _Choice, ) from opentelemetry.semconv._incubating.attributes.error_attributes import ( ERROR_TYPE, @@ -3051,6 +3052,16 @@ def stream_error_callback(exc, ended): assert "input" not in tool_block +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=True) + + assert choice.finish_reason == "end_turn" + assert choice.message == {} + assert choice.index == 0 + + def amazon_nova_messages(): return [ {"role": "user", "content": [{"text": "Say this is a test"}]},