Skip to content

fix(bedrock): preserve stream event type and drop invocation-metrics trailer#1682

Merged
craigie-ant merged 2 commits into
nextfrom
fix-bedrock-stream-decoder-1647
Jun 17, 2026
Merged

fix(bedrock): preserve stream event type and drop invocation-metrics trailer#1682
craigie-ant merged 2 commits into
nextfrom
fix-bedrock-stream-decoder-1647

Conversation

@qing-ant

Copy link
Copy Markdown
Contributor

Problem

The Bedrock stream decoder wraps every chunk as ServerSentEvent(event="completion"), discarding the payload's actual event type. Downstream, the "completion" branch in _streaming.py::__stream__ does not backfill data["type"] (unlike the typed-event branch), so chunks without a type field reach construct_type against the RawMessageStreamEvent union with no discriminator. That falls back to the first union member and yields RawMessageStartEvent(message=None) — an object that violates the SDK's own type contract.

The amazon-bedrock-invocationMetrics trailer that Bedrock appends to streams has no type field, so it hits exactly this. Any consumer reading event.message.usage crashes with AttributeError: 'NoneType' object has no attribute 'usage'.

Fixes #1647.

Fix

In lib/bedrock/_stream_decoder.py (hand-maintained, not Stainless-generated):

  • Parse each chunk's JSON and use its type field as the SSE event name, so typed Messages events route through the branch in __stream__ that backfills data["type"].
  • Drop the metrics-only trailer (no type, no completion key) instead of forwarding it to the stream-event union.
  • Fall back to event="completion" for legacy untyped payloads (and any non-JSON payload), preserving the existing Completions path. A legacy completion chunk that also carries amazon-bedrock-invocationMetrics is kept, not dropped.

Relation to #1651

#1651 preserves the event type but still forwards the metrics trailer (its .get("type", "completion") returns "completion" for the type-less trailer), so the reported crash still occurs. This PR adds the missing drop and a regression test for it.

Behavior notes

  • Bedrock streams now route by the same event names as direct SSE. A chunk whose type is not in __stream__'s recognized list is dropped rather than yielded as a mis-constructed object — this matches non-Bedrock behavior.
  • A {"type": "error", ...} chunk inside a 200 stream now raises (same as direct SSE) instead of being yielded as data.

Tests

Four unit tests on _chunk_bytes_to_sse: typed event preserved, metrics-only trailer dropped, legacy completion kept, legacy completion with merged metrics kept.

…trailer

The Bedrock stream decoder previously wrapped every chunk as
ServerSentEvent(event="completion"), discarding the payload's actual
event type. Chunks without a "type" field — notably the
amazon-bedrock-invocationMetrics trailer that Bedrock appends to every
stream — then routed through the completion branch in _streaming.py,
which does not backfill data["type"]. construct_type fell back to the
first member of the RawMessageStreamEvent union and yielded
RawMessageStartEvent(message=None), violating the type contract and
crashing any consumer that read event.message.

The decoder now reads "type" from the chunk payload and uses it as the
SSE event name, drops the invocation-metrics trailer (it is not a model
stream event), and only falls back to "completion" for legacy untyped
payloads.

Fixes #1647
@qing-ant qing-ant requested a review from a team as a code owner June 16, 2026 23:36
Comment on lines +82 to +86
# Bedrock appends a trailing chunk that only carries invocation
# metrics; it is not a model stream event, so drop it rather than
# let it be mis-constructed against the stream-event union.
if "amazon-bedrock-invocationMetrics" in payload and "completion" not in payload:
return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is something we should fix but it is a breaking change, as a user may be using this value, so I'm removing the logic here for now

@craigie-ant craigie-ant left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@craigie-ant craigie-ant changed the base branch from main to next June 17, 2026 00:29
@craigie-ant craigie-ant merged commit b27e343 into next Jun 17, 2026
14 checks passed
@stainless-app stainless-app Bot mentioned this pull request Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bedrock stream decoder drops SSE event types; type-less chunks construct as RawMessageStartEvent(message=None)

2 participants