Skip to content

fix(bedrock): drop metrics-only invocation trailer from stream decoder#1702

Open
camposvinicius wants to merge 1 commit into
anthropics:mainfrom
camposvinicius:fix/bedrock-drop-metrics-trailer
Open

fix(bedrock): drop metrics-only invocation trailer from stream decoder#1702
camposvinicius wants to merge 1 commit into
anthropics:mainfrom
camposvinicius:fix/bedrock-drop-metrics-trailer

Conversation

@camposvinicius

Copy link
Copy Markdown

Problem

Follow-up to #1682. That PR fixed typed-event routing by preserving each chunk's real event type, but the amazon-bedrock-invocationMetrics trailer is still forwarded as event="completion". The trailer carries neither a type nor a completion field, so on a Messages stream the "completion" branch in _streaming.py::__stream__ constructs it against the RawMessageStreamEvent union with no discriminator. The union falls back to its first member and yields a contract-violating RawMessageStartEvent(message=None), which leaks to the consumer as an extra stream event.

Any consumer that trusts the type annotations (for example event.message.usage, as in the original report via pydantic-ai's streaming path on AsyncAnthropicBedrock) then crashes with AttributeError: 'NoneType' object has no attribute 'usage'. This is the same failure reported in #1647, still reproducible on main.

Reproduction (end to end, offline)

Feeding a complete Messages stream plus the metrics trailer through the real AnthropicBedrock client (binary eventstream frames, decoder, raw messages.create(stream=True)):

BEFORE  total events: 8 | last = RawMessageStartEvent  type=None  message=None
        event.message.usage -> AttributeError: 'NoneType' object has no attribute 'usage'

AFTER   total events: 7 | stream is clean, no leaked event

Fix

In lib/bedrock/_stream_decoder.py (hand-maintained, not Stainless-generated), _chunk_bytes_to_sse now drops the metrics-only trailer instead of forwarding it:

  • typed Messages events keep their real event type (unchanged)
  • legacy text-completion chunks (which carry a completion field) still route as event="completion" (unchanged)
  • a legacy completion chunk that also carries the metrics trailer is kept (unchanged)
  • a chunk with no type and no completion field, the metrics-only trailer, is dropped so it never reaches the stream-event union

Tests

Adds two regression tests to tests/lib/test_bedrock.py:

  • test_chunk_bytes_to_sse_drops_metrics_only_trailer asserts the decoder returns None for the trailer
  • test_metrics_trailer_would_violate_stream_event_contract documents the union fallback the drop protects against

Full tests/lib/test_bedrock.py passes, and ruff and pyright are clean on the changed files.

Fixes #1647.

Follow-up to anthropics#1682, which preserved the real event type but still forwarded
the amazon-bedrock-invocationMetrics trailer as event="completion". That
trailer carries no type and no completion field, so on a Messages stream it
constructs against the RawMessageStreamEvent union with no discriminator,
falls back to the first union member, and yields a contract-violating
RawMessageStartEvent(message=None). Consumers that trust the type
annotations (for example event.message.usage) then crash with AttributeError.

The decoder now drops the metrics-only trailer instead of forwarding it.
Typed Messages events, legacy text completions, and legacy completions that
also carry the metrics trailer are all unchanged.

Adds regression tests covering the drop and documenting the union fallback
the drop protects against.

Fixes anthropics#1647.
@camposvinicius camposvinicius requested a review from a team as a code owner June 24, 2026 05:44
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)

1 participant