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
6 changes: 6 additions & 0 deletions src/anthropic/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT:
return model.model_copy(deep=deep)


def model_fields_set(model: pydantic.BaseModel) -> set[str]:
if PYDANTIC_V1:
return model.__fields_set__ # type: ignore
return model.model_fields_set


def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str:
if PYDANTIC_V1:
return model.json(indent=indent) # type: ignore
Expand Down
4 changes: 3 additions & 1 deletion src/anthropic/lib/streaming/_beta_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from ..._types import NotGiven, not_given
from ..._utils import consume_sync_iterator, consume_async_iterator
from ..._compat import model_fields_set
from ..._models import build, construct_type, construct_type_unchecked
from ._beta_types import (
BetaCitationEvent,
Expand Down Expand Up @@ -536,7 +537,8 @@ def accumulate_event(
elif event.delta.type == "compaction_delta":
if content.type == "compaction":
content.content = event.delta.content
content.encrypted_content = event.delta.encrypted_content
if "encrypted_content" in model_fields_set(event.delta):
content.encrypted_content = event.delta.encrypted_content
else:
# we only want exhaustive checking for linters, not at runtime
if TYPE_CHECKING: # type: ignore[unreachable]
Expand Down
37 changes: 37 additions & 0 deletions tests/lib/streaming/test_beta_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,43 @@ def test_compaction(self, respx_mock: MockRouter) -> None:

assert_compaction_response([event for event in stream], stream.get_final_message())

@pytest.mark.respx(base_url=base_url)
def test_compaction_delta_without_encrypted_content_retains_metadata(self, respx_mock: MockRouter) -> None:
sse_lines = [
"event: message_start\n",
'data: {"type":"message_start","message":{"id":"msg_01","type":"message","role":"assistant","content":[],"model":"claude-opus-4-7","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":30,"output_tokens":1}}}\n\n',
"event: content_block_start\n",
'data: {"type":"content_block_start","index":0,"content_block":{"type":"compaction","content":null,"encrypted_content":null}}\n\n',
"event: content_block_delta\n",
'data: {"type":"content_block_delta","index":0,"delta":{"type":"compaction_delta","content":"Initial summary.","encrypted_content":"opaque_meta"}}\n\n',
"event: content_block_delta\n",
'data: {"type":"content_block_delta","index":0,"delta":{"type":"compaction_delta","content":"Updated summary."}}\n\n',
"event: content_block_stop\n",
'data: {"type":"content_block_stop","index":0}\n\n',
"event: message_delta\n",
'data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":10}}\n\n',
"event: message_stop\n",
'data: {"type":"message_stop"}\n\n',
]
respx_mock.post("/v1/messages").mock(
return_value=httpx2.Response(200, content="".join(sse_lines).encode("utf-8"))
)

with sync_client.beta.messages.stream(
max_tokens=1024,
messages=[{"role": "user", "content": "Say hello there!"}],
model="claude-opus-4-7",
) as stream:
events = list(stream)
final_message = stream.get_final_message()

compaction_events = [e for e in events if isinstance(e, BetaCompactionEvent)]
assert len(compaction_events) == 2
assert compaction_events[0].encrypted_content == "opaque_meta"
assert compaction_events[1].encrypted_content == "opaque_meta"
assert final_message.content[0].type == "compaction"
assert final_message.content[0].encrypted_content == "opaque_meta"

@pytest.mark.respx(base_url=base_url)
def test_fallback_relabels_model(self, respx_mock: MockRouter) -> None:
respx_mock.post("/v1/messages").mock(
Expand Down