Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions src/anthropic/lib/bedrock/_stream_decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,16 @@ def _chunk_bytes_to_sse(raw: bytes) -> ServerSentEvent | None:

payload = cast("Dict[str, Any]", data)
event_type = payload.get("type")
if not isinstance(event_type, str):
event_type = "completion"
if isinstance(event_type, str):
return ServerSentEvent(data=decoded, event=event_type)

# No typed discriminator. Two untyped payload shapes reach this point. Legacy
# text-completion chunks carry a "completion" field and belong on the completion
# path. Bedrock also appends an amazon-bedrock-invocationMetrics trailer that
# carries neither a type nor a completion field. Forwarding that trailer to the
# stream-event union makes it construct as a contract-violating
# RawMessageStartEvent(message=None), so drop it instead.
if "completion" in payload:
return ServerSentEvent(data=decoded, event="completion")

return ServerSentEvent(data=decoded, event=event_type)
return None
20 changes: 20 additions & 0 deletions tests/lib/test_bedrock.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,26 @@ def test_chunk_bytes_to_sse_legacy_completion_with_metrics() -> None:
assert sse.event == "completion"


def test_chunk_bytes_to_sse_drops_metrics_only_trailer() -> None:
# The amazon-bedrock-invocationMetrics trailer carries no type and no
# completion field. Forwarding it to the stream-event union constructs a
# contract-violating RawMessageStartEvent(message=None), so it must be dropped.
raw = b'{"amazon-bedrock-invocationMetrics":{"inputTokenCount":10,"outputTokenCount":5}}'
assert _chunk_bytes_to_sse(raw) is None


def test_metrics_trailer_would_violate_stream_event_contract() -> None:
from anthropic.types import RawMessageStreamEvent
from anthropic._models import construct_type

# Documents why the trailer must be dropped. Were it forwarded to the
# stream-event union, the union has no discriminator to match it and falls
# back to its first member, RawMessageStartEvent, with a null message.
trailer = {"amazon-bedrock-invocationMetrics": {"inputTokenCount": 10, "outputTokenCount": 5}}
event = construct_type(value=trailer, type_=cast(t.Any, RawMessageStreamEvent))
assert getattr(event, "message", None) is None


def test_copy_x_stainless_helper_header_appends() -> None:
# `x-stainless-helper` accumulates across copies instead of being clobbered
client = sync_client.with_options(default_headers={"x-stainless-helper": "parent"})
Expand Down