Skip to content
Closed
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
29 changes: 28 additions & 1 deletion src/strands/models/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from typing import Any, TypedDict, TypeVar, cast

import anthropic
from anthropic.types import TextBlock
from pydantic import BaseModel
from typing_extensions import Required, Unpack, override

Expand Down Expand Up @@ -254,6 +255,32 @@ def _format_tool_choice(tool_choice: ToolChoice | None) -> dict:
else:
return {}

@staticmethod
def _normalize_event(event: Any) -> Any:
"""Normalize Anthropic stream events to avoid Pydantic serialization warnings.

The Anthropic SDK returns ParsedTextBlock (a TextBlock subclass with an extra
parsed_output field) in content_block_start events. Calling model_dump() on those
events triggers PydanticSerializationUnexpectedValue warnings because ParsedTextBlock
is not in the discriminated union on Message.content.

This converts any ParsedTextBlock back to a plain TextBlock so the discriminated
union matches cleanly and no warnings are emitted.
"""
try:
from anthropic.types.parsed_message import ParsedTextBlock
except ImportError:
return event

if event.type == "content_block_start" and isinstance(event.content_block, ParsedTextBlock):
event.content_block = TextBlock(
text=event.content_block.text,
type=event.content_block.type,
citations=event.content_block.citations,
)

return event

def format_chunk(self, event: dict[str, Any]) -> StreamEvent:
"""Format the Anthropic response events into standardized message chunks.

Expand Down Expand Up @@ -407,7 +434,7 @@ async def stream(
logger.debug("got response from model")
async for event in stream:
if event.type in AnthropicModel.EVENT_TYPES:
yield self.format_chunk(event.model_dump())
yield self.format_chunk(self._normalize_event(event).model_dump())

usage = event.message.usage # type: ignore
yield self.format_chunk({"type": "metadata", "usage": usage.model_dump()})
Expand Down
47 changes: 47 additions & 0 deletions tests/strands/models/test_anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -933,3 +933,50 @@ def test_format_request_filters_location_source_document(model, model_id, max_to
]
assert tru_request["messages"] == exp_messages
assert "Location sources are not supported by Anthropic" in caplog.text


def test_normalize_event_converts_parsed_text_block(model):
"""_normalize_event replaces ParsedTextBlock with TextBlock to avoid Pydantic warnings."""
try:
from anthropic.types.parsed_message import ParsedTextBlock
except ImportError:
pytest.skip("ParsedTextBlock not available in this SDK version")

from anthropic.types import TextBlock

parsed_block = ParsedTextBlock(type="text", text="hello", citations=None)
mock_event = unittest.mock.Mock()
mock_event.type = "content_block_start"
mock_event.content_block = parsed_block

result = AnthropicModel._normalize_event(mock_event)

assert result is mock_event
assert isinstance(result.content_block, TextBlock)
assert not isinstance(result.content_block, ParsedTextBlock)
assert result.content_block.text == "hello"


def test_normalize_event_passes_through_text_block(model):
"""_normalize_event leaves events with a plain TextBlock unchanged."""
from anthropic.types import TextBlock

plain_block = TextBlock(type="text", text="world")
mock_event = unittest.mock.Mock()
mock_event.type = "content_block_start"
mock_event.content_block = plain_block

result = AnthropicModel._normalize_event(mock_event)

assert result is mock_event
assert result.content_block is plain_block


def test_normalize_event_passes_through_non_content_block_start(model):
"""_normalize_event leaves events with types other than content_block_start unchanged."""
mock_event = unittest.mock.Mock()
mock_event.type = "message_start"

result = AnthropicModel._normalize_event(mock_event)

assert result is mock_event