@@ -1446,3 +1446,270 @@ def test_system_prompt_with_complex_structure(sentry_init, capture_events):
14461446 assert stored_messages [0 ]["content" ][1 ]["text" ] == "Be concise and clear."
14471447 assert stored_messages [1 ]["role" ] == "user"
14481448 assert stored_messages [1 ]["content" ] == "Hello"
1449+
1450+
1451+ def test_nonstreaming_create_message_with_cache_tokens (sentry_init , capture_events ):
1452+ """Test that cache tokens (read and write) are captured from Anthropic responses."""
1453+
1454+ # Create a mock usage object with cache tokens
1455+ class MockUsageWithCache :
1456+ def __init__ (self ):
1457+ self .input_tokens = 100
1458+ self .output_tokens = 50
1459+ self .cache_read_input_tokens = 25
1460+ self .cache_write_input_tokens = 30
1461+
1462+ # Create a mock message with cache tokens
1463+ example_message_with_cache = Message (
1464+ id = "id" ,
1465+ model = "model" ,
1466+ role = "assistant" ,
1467+ content = [TextBlock (type = "text" , text = "Hi, I'm Claude." )],
1468+ type = "message" ,
1469+ usage = Usage (input_tokens = 100 , output_tokens = 50 ),
1470+ )
1471+ # Add cache token attributes to the usage object
1472+ example_message_with_cache .usage .cache_read_input_tokens = 25
1473+ example_message_with_cache .usage .cache_write_input_tokens = 30
1474+
1475+ sentry_init (
1476+ integrations = [AnthropicIntegration ()],
1477+ traces_sample_rate = 1.0 ,
1478+ )
1479+ events = capture_events ()
1480+ client = Anthropic (api_key = "z" )
1481+ client .messages ._post = mock .Mock (return_value = example_message_with_cache )
1482+
1483+ messages = [{"role" : "user" , "content" : "Hello, Claude" }]
1484+
1485+ with start_transaction (name = "anthropic" ):
1486+ response = client .messages .create (
1487+ max_tokens = 1024 , messages = messages , model = "model"
1488+ )
1489+
1490+ assert response == example_message_with_cache
1491+
1492+ assert len (events ) == 1
1493+ (event ,) = events
1494+
1495+ assert len (event ["spans" ]) == 1
1496+ (span ,) = event ["spans" ]
1497+
1498+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS ] == 100
1499+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_OUTPUT_TOKENS ] == 50
1500+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_TOTAL_TOKENS ] == 150
1501+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS_CACHED ] == 25
1502+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE ] == 30
1503+
1504+
1505+ @pytest .mark .asyncio
1506+ async def test_nonstreaming_create_message_with_cache_tokens_async (
1507+ sentry_init , capture_events
1508+ ):
1509+ """Test that cache tokens are captured from async Anthropic responses."""
1510+ example_message_with_cache = Message (
1511+ id = "id" ,
1512+ model = "model" ,
1513+ role = "assistant" ,
1514+ content = [TextBlock (type = "text" , text = "Hi, I'm Claude." )],
1515+ type = "message" ,
1516+ usage = Usage (input_tokens = 100 , output_tokens = 50 ),
1517+ )
1518+ example_message_with_cache .usage .cache_read_input_tokens = 25
1519+ example_message_with_cache .usage .cache_write_input_tokens = 30
1520+
1521+ sentry_init (
1522+ integrations = [AnthropicIntegration ()],
1523+ traces_sample_rate = 1.0 ,
1524+ )
1525+ events = capture_events ()
1526+ client = AsyncAnthropic (api_key = "z" )
1527+ client .messages ._post = AsyncMock (return_value = example_message_with_cache )
1528+
1529+ messages = [{"role" : "user" , "content" : "Hello, Claude" }]
1530+
1531+ with start_transaction (name = "anthropic" ):
1532+ response = await client .messages .create (
1533+ max_tokens = 1024 , messages = messages , model = "model"
1534+ )
1535+
1536+ assert response == example_message_with_cache
1537+
1538+ assert len (events ) == 1
1539+ (event ,) = events
1540+
1541+ (span ,) = event ["spans" ]
1542+
1543+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS ] == 100
1544+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_OUTPUT_TOKENS ] == 50
1545+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS_CACHED ] == 25
1546+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE ] == 30
1547+
1548+
1549+ def test_streaming_create_message_with_cache_tokens (sentry_init , capture_events ):
1550+ """Test that cache tokens are captured from streaming Anthropic responses."""
1551+ # Create a mock usage object with cache tokens for the message start event
1552+ example_message_with_cache = Message (
1553+ id = "id" ,
1554+ model = "model" ,
1555+ role = "assistant" ,
1556+ content = [TextBlock (type = "text" , text = "Hi, I'm Claude." )],
1557+ type = "message" ,
1558+ usage = Usage (input_tokens = 100 , output_tokens = 20 ),
1559+ )
1560+ example_message_with_cache .usage .cache_read_input_tokens = 25
1561+ example_message_with_cache .usage .cache_write_input_tokens = 30
1562+
1563+ client = Anthropic (api_key = "z" )
1564+ returned_stream = Stream (cast_to = None , response = None , client = client )
1565+ returned_stream ._iterator = [
1566+ MessageStartEvent (
1567+ message = example_message_with_cache ,
1568+ type = "message_start" ,
1569+ ),
1570+ ContentBlockStartEvent (
1571+ type = "content_block_start" ,
1572+ index = 0 ,
1573+ content_block = TextBlock (type = "text" , text = "" ),
1574+ ),
1575+ ContentBlockDeltaEvent (
1576+ delta = TextDelta (text = "Hi!" , type = "text_delta" ),
1577+ index = 0 ,
1578+ type = "content_block_delta" ,
1579+ ),
1580+ ContentBlockStopEvent (type = "content_block_stop" , index = 0 ),
1581+ MessageDeltaEvent (
1582+ delta = Delta (),
1583+ usage = MessageDeltaUsage (output_tokens = 10 ),
1584+ type = "message_delta" ,
1585+ ),
1586+ ]
1587+
1588+ sentry_init (
1589+ integrations = [AnthropicIntegration ()],
1590+ traces_sample_rate = 1.0 ,
1591+ )
1592+ events = capture_events ()
1593+ client .messages ._post = mock .Mock (return_value = returned_stream )
1594+
1595+ messages = [{"role" : "user" , "content" : "Hello, Claude" }]
1596+
1597+ with start_transaction (name = "anthropic" ):
1598+ message = client .messages .create (
1599+ max_tokens = 1024 , messages = messages , model = "model" , stream = True
1600+ )
1601+ for _ in message :
1602+ pass
1603+
1604+ assert len (events ) == 1
1605+ (event ,) = events
1606+
1607+ (span ,) = event ["spans" ]
1608+
1609+ # For streaming, tokens accumulate from message_start and message_delta
1610+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS ] == 100
1611+ # Output tokens: 20 from message_start + 10 from message_delta = 30
1612+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_OUTPUT_TOKENS ] == 30
1613+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS_CACHED ] == 25
1614+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE ] == 30
1615+
1616+
1617+ @pytest .mark .asyncio
1618+ async def test_streaming_create_message_with_cache_tokens_async (
1619+ sentry_init , capture_events
1620+ ):
1621+ """Test that cache tokens are captured from async streaming Anthropic responses."""
1622+ example_message_with_cache = Message (
1623+ id = "id" ,
1624+ model = "model" ,
1625+ role = "assistant" ,
1626+ content = [TextBlock (type = "text" , text = "Hi, I'm Claude." )],
1627+ type = "message" ,
1628+ usage = Usage (input_tokens = 100 , output_tokens = 20 ),
1629+ )
1630+ example_message_with_cache .usage .cache_read_input_tokens = 25
1631+ example_message_with_cache .usage .cache_write_input_tokens = 30
1632+
1633+ client = AsyncAnthropic (api_key = "z" )
1634+ returned_stream = AsyncStream (cast_to = None , response = None , client = client )
1635+ returned_stream ._iterator = async_iterator (
1636+ [
1637+ MessageStartEvent (
1638+ message = example_message_with_cache ,
1639+ type = "message_start" ,
1640+ ),
1641+ ContentBlockStartEvent (
1642+ type = "content_block_start" ,
1643+ index = 0 ,
1644+ content_block = TextBlock (type = "text" , text = "" ),
1645+ ),
1646+ ContentBlockDeltaEvent (
1647+ delta = TextDelta (text = "Hi!" , type = "text_delta" ),
1648+ index = 0 ,
1649+ type = "content_block_delta" ,
1650+ ),
1651+ ContentBlockStopEvent (type = "content_block_stop" , index = 0 ),
1652+ MessageDeltaEvent (
1653+ delta = Delta (),
1654+ usage = MessageDeltaUsage (output_tokens = 10 ),
1655+ type = "message_delta" ,
1656+ ),
1657+ ]
1658+ )
1659+
1660+ sentry_init (
1661+ integrations = [AnthropicIntegration ()],
1662+ traces_sample_rate = 1.0 ,
1663+ )
1664+ events = capture_events ()
1665+ client .messages ._post = AsyncMock (return_value = returned_stream )
1666+
1667+ messages = [{"role" : "user" , "content" : "Hello, Claude" }]
1668+
1669+ with start_transaction (name = "anthropic" ):
1670+ message = await client .messages .create (
1671+ max_tokens = 1024 , messages = messages , model = "model" , stream = True
1672+ )
1673+ async for _ in message :
1674+ pass
1675+
1676+ assert len (events ) == 1
1677+ (event ,) = events
1678+
1679+ (span ,) = event ["spans" ]
1680+
1681+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS ] == 100
1682+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_OUTPUT_TOKENS ] == 30
1683+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS_CACHED ] == 25
1684+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE ] == 30
1685+
1686+
1687+ def test_nonstreaming_without_cache_tokens (sentry_init , capture_events ):
1688+ """Test that missing or zero cache tokens don't cause errors (backwards compatibility)."""
1689+ sentry_init (
1690+ integrations = [AnthropicIntegration ()],
1691+ traces_sample_rate = 1.0 ,
1692+ )
1693+ events = capture_events ()
1694+ client = Anthropic (api_key = "z" )
1695+ # Using EXAMPLE_MESSAGE which doesn't have explicit cache tokens set
1696+ client .messages ._post = mock .Mock (return_value = EXAMPLE_MESSAGE )
1697+
1698+ messages = [{"role" : "user" , "content" : "Hello, Claude" }]
1699+
1700+ with start_transaction (name = "anthropic" ):
1701+ client .messages .create (max_tokens = 1024 , messages = messages , model = "model" )
1702+
1703+ assert len (events ) == 1
1704+ (event ,) = events
1705+
1706+ (span ,) = event ["spans" ]
1707+
1708+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS ] == 10
1709+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_OUTPUT_TOKENS ] == 20
1710+ # Cache tokens may be present with 0 values (newer SDK versions)
1711+ # or not present at all (older SDK versions) - both are acceptable
1712+ cached = span ["data" ].get (SPANDATA .GEN_AI_USAGE_INPUT_TOKENS_CACHED )
1713+ cache_write = span ["data" ].get (SPANDATA .GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE )
1714+ assert cached is None or cached == 0
1715+ assert cache_write is None or cache_write == 0
0 commit comments