Skip to content

Commit 72625db

Browse files
authored
[PATCH] Add safety check for bedrock ConverseStream responses (#547)
*Description of changes:* Ports the changes from open-telemetry/opentelemetry-python-contrib#3990 Add defensive check for Bedrock responses missing expected structure. When no messageStart event with assistant role is received, the response may lack the standard output.message format, causing the occasional KeyError exceptions. For testing, I was unable to reproduce the exact scenario that would trigger this bug, however, this safety check would fix this issue without affecting normal instrumentation behavior. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 54c5f11 commit 72625db

File tree

3 files changed

+32
-1
lines changed

3 files changed

+32
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,6 @@ If your change does not need a CHANGELOG entry, add the "skip changelog" label t
2323
([#522](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/522))
2424
- Support credentials provider name for BedrockAgentCore Identity
2525
([#534](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/534))
26+
- [PATCH] Add safety check for bedrock ConverseStream responses
27+
([#547](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/547))
2628

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,9 +334,10 @@ def patched_extract_tool_calls(
334334
tool_calls.append(tool_call)
335335
return tool_calls
336336

337-
# TODO: The following code is to patch a bedrock bug that was fixed in
337+
# TODO: The following code is to patch bedrock bugs that were fixed in
338338
# opentelemetry-instrumentation-botocore==0.60b0 in:
339339
# https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3875
340+
# https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3990
340341
# Remove this code once we've bumped opentelemetry-instrumentation-botocore dependency to 0.60b0
341342
def patched_process_anthropic_claude_chunk(self, chunk):
342343
# pylint: disable=too-many-return-statements,too-many-branches
@@ -412,12 +413,30 @@ def patched_process_anthropic_claude_chunk(self, chunk):
412413
self._stream_done_callback(self._response)
413414
return
414415

416+
def patched_from_converse(cls, response: dict[str, Any], capture_content: bool) -> bedrock_utils._Choice:
417+
# be defensive about malformed responses, refer to #3958 for more context
418+
output = response.get("output", {})
419+
orig_message = output.get("message", {})
420+
if role := orig_message.get("role"):
421+
message = {"role": role}
422+
else:
423+
# amazon.titan does not serialize the role
424+
message = {}
425+
426+
if tool_calls := bedrock_utils.extract_tool_calls(orig_message, capture_content):
427+
message["tool_calls"] = tool_calls
428+
elif capture_content and (content := orig_message.get("content")):
429+
message["content"] = content
430+
431+
return cls(message, response["stopReason"], index=0)
432+
415433
bedrock_utils.ConverseStreamWrapper.__init__ = patched_init
416434
bedrock_utils.ConverseStreamWrapper._process_event = patched_process_event
417435
bedrock_utils.InvokeModelWithResponseStreamWrapper._process_anthropic_claude_chunk = (
418436
patched_process_anthropic_claude_chunk
419437
)
420438
bedrock_utils.extract_tool_calls = patched_extract_tool_calls
439+
bedrock_utils._Choice.from_converse = classmethod(patched_from_converse)
421440

422441
# END The OpenTelemetry Authors code
423442

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/patches/test_instrumentation_patch.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ def _test_patched_botocore_instrumentation(self):
250250
self._test_patched_process_anthropic_claude_chunk({"location": "Seattle"}, {"location": "Seattle"})
251251
self._test_patched_process_anthropic_claude_chunk(None, None)
252252
self._test_patched_process_anthropic_claude_chunk({}, {})
253+
self._test_patched_from_converse_with_malformed_response()
253254

254255
# Bedrock Agent Runtime
255256
self.assertTrue("bedrock-agent-runtime" in _KNOWN_EXTENSIONS)
@@ -645,6 +646,15 @@ def _test_patched_extract_tool_calls(self):
645646
result = bedrock_utils.extract_tool_calls(message_with_type_tool_use, True)
646647
self.assertEqual(len(result), 1)
647648

649+
def _test_patched_from_converse_with_malformed_response(self):
650+
"""Test patched from_converse handles malformed response missing output key"""
651+
malformed_response = {"stopReason": "end_turn"}
652+
choice = bedrock_utils._Choice.from_converse(malformed_response, capture_content=False)
653+
654+
self.assertEqual(choice.finish_reason, "end_turn")
655+
self.assertEqual(choice.message, {})
656+
self.assertEqual(choice.index, 0)
657+
648658
def _test_patched_process_anthropic_claude_chunk(
649659
self, input_value: Dict[str, str], expected_output: Dict[str, str]
650660
):

0 commit comments

Comments
 (0)