Skip to content

Commit 5a2015e

Browse files
fix(boto3): Skip malformed Bedrock stream chunks and gate stream outputs once
Malformed InvokeModelWithResponseStream chunks are skipped quietly instead of reporting an SDK internal error, like the non-streamed response path. The buffered stream content is already gated on recording outputs, so serialize it directly rather than checking the gate a second time. That makes the buffering gate the only gate, so tests cover it. Co-Authored-By: Sergiy Dybskiy <3760543+sergical@users.noreply.github.com>
1 parent 68389c6 commit 5a2015e

2 files changed

Lines changed: 47 additions & 2 deletions

File tree

‎sentry_sdk/integrations/boto3/_services/bedrock_runtime.py‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -810,8 +810,12 @@ def get_attributes(self) -> "Attributes":
810810
tool_call["arguments"] = content
811811
parts.append(tool_call)
812812

813+
# `record_outputs` already gated the buffered content blocks.
813814
if parts:
814-
attributes.update(_get_output_attributes([_to_output_message(parts)]))
815+
with capture_internal_exceptions():
816+
attributes[SPANDATA.GEN_AI_OUTPUT_MESSAGES] = json.dumps(
817+
[_to_output_message(parts)]
818+
)
815819

816820
return attributes
817821

@@ -882,7 +886,11 @@ def _record_invoke_model_stream_event(
882886
if not payload:
883887
return
884888

885-
chunk = json.loads(payload)
889+
try:
890+
chunk = json.loads(payload)
891+
except ValueError:
892+
return
893+
886894
if not isinstance(chunk, Mapping):
887895
return
888896

‎tests/integrations/boto3/test_bedrock_runtime.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2150,6 +2150,43 @@ def call():
21502150
)
21512151

21522152

2153+
@pytest.mark.parametrize("trace_mode", TRACE_MODES)
2154+
def test_invoke_model_with_response_stream_skips_malformed_chunks(
2155+
init_bedrock, capture_items, trace_mode
2156+
):
2157+
client = init_bedrock(trace_mode, send_default_pii=True)
2158+
malformed_chunk = ("chunk", {"bytes": base64.b64encode(b"not json").decode()})
2159+
events = [malformed_chunk] + _invoke_model_stream_chunks(
2160+
{
2161+
"outputs": [{"text": "Hi!", "stop_reason": "stop"}],
2162+
"amazon-bedrock-invocationMetrics": {
2163+
"inputTokenCount": 7,
2164+
"outputTokenCount": 3,
2165+
},
2166+
}
2167+
)
2168+
received_chunks = []
2169+
2170+
def call():
2171+
response = client.invoke_model_with_response_stream(
2172+
modelId="mistral.mistral-7b-instruct-v0:2",
2173+
body=json.dumps({"prompt": "Hello!"}),
2174+
)
2175+
received_chunks.extend(event["chunk"]["bytes"] for event in response["body"])
2176+
2177+
with _event_stream_response(client, events):
2178+
spans = _capture_spans(capture_items, trace_mode, call)
2179+
2180+
assert received_chunks[0] == b"not json"
2181+
assert len(received_chunks) == 2
2182+
2183+
attributes = _get_attributes(_get_bedrock_span(spans))
2184+
assert attributes[SPANDATA.GEN_AI_OUTPUT_MESSAGES] == [
2185+
{"role": "assistant", "parts": [{"type": "text", "content": "Hi!"}]}
2186+
]
2187+
assert attributes[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 3
2188+
2189+
21532190
@pytest.mark.parametrize("trace_mode", TRACE_MODES)
21542191
def test_invoke_model_with_response_stream_error_is_unchanged(
21552192
init_bedrock, capture_items, trace_mode

0 commit comments

Comments
 (0)