1313except ImportError :
1414 import httpx as _http_lib
1515from anthropic import APIConnectionError , AsyncAnthropic , NotFoundError
16- from anthropic ._response import AsyncAPIResponse
16+ from anthropic ._response import AsyncAPIResponse , AsyncResponseContextManager
1717
1818try :
1919 from anthropic ._legacy_response import LegacyAPIResponse
2323from anthropic .resources .messages import AsyncMessages as _AsyncMessages
2424from anthropic .types import Message
2525
26- from opentelemetry .instrumentation .genai .anthropic import _raw_response
26+ from opentelemetry .instrumentation .genai .anthropic import (
27+ AnthropicInstrumentor ,
28+ _raw_response ,
29+ )
2730from opentelemetry .instrumentation .genai .anthropic ._raw_response import (
2831 RawResponseProxy ,
2932)
@@ -1542,14 +1545,6 @@ async def test_async_messages_raw_response_parse_after_exit(
15421545 assert message .model == model
15431546
15441547
1545- @pytest .mark .skip (
1546- reason = "Known gap, tracked in #389: the SDK's "
1547- "AsyncResponseContextManager.__aexit__ discards the caller's exception "
1548- "before closing the response, so the proxy never sees it and the span is "
1549- "finalized as a success. Fixing it means instrumenting "
1550- "AsyncMessagesWithStreamingResponse.create and wrapping the context "
1551- "manager itself, which is a new patch target and out of scope here."
1552- )
15531548@pytest .mark .cassette ("test_async_messages_create_streaming_with_raw_response" )
15541549@pytest .mark .asyncio
15551550@pytest .mark .vcr ()
@@ -1585,6 +1580,108 @@ async def test_async_messages_with_streaming_response_user_exception(
15851580 assert span .attributes [ErrorAttributes .ERROR_TYPE ] == "ValueError"
15861581
15871582
1583+ @pytest .mark .cassette ("test_async_messages_create_streaming_with_raw_response" )
1584+ @pytest .mark .asyncio
1585+ @pytest .mark .vcr ()
1586+ async def test_async_messages_with_streaming_response_user_exception_before_parse (
1587+ span_exporter , async_anthropic_client , instrument_no_content
1588+ ):
1589+ """A caller error before parsing still fails the raw-response span once."""
1590+ with pytest .raises (ValueError , match = "User raised exception" ):
1591+ async with (
1592+ async_anthropic_client .messages .with_streaming_response .create (
1593+ model = "claude-sonnet-4-20250514" ,
1594+ max_tokens = 100 ,
1595+ messages = [
1596+ {"role" : "user" , "content" : "Say hello in one word." }
1597+ ],
1598+ stream = True ,
1599+ )
1600+ ):
1601+ raise ValueError ("User raised exception" )
1602+
1603+ spans = span_exporter .get_finished_spans ()
1604+ assert len (spans ) == 1
1605+ assert spans [0 ].attributes [ErrorAttributes .ERROR_TYPE ] == "ValueError"
1606+
1607+
1608+ @pytest .mark .cassette ("test_async_messages_create_streaming_with_raw_response" )
1609+ @pytest .mark .asyncio
1610+ @pytest .mark .vcr ()
1611+ async def test_async_messages_with_streaming_response_user_exception_after_drain (
1612+ span_exporter , async_anthropic_client , instrument_no_content
1613+ ):
1614+ """A caller error after draining does not finalize the span twice."""
1615+ with pytest .raises (ValueError , match = "User raised exception" ):
1616+ async with (
1617+ async_anthropic_client .messages .with_streaming_response .create (
1618+ model = "claude-sonnet-4-20250514" ,
1619+ max_tokens = 100 ,
1620+ messages = [
1621+ {"role" : "user" , "content" : "Say hello in one word." }
1622+ ],
1623+ stream = True ,
1624+ )
1625+ ) as raw_response :
1626+ async for _ in await raw_response .parse ():
1627+ pass
1628+ raise ValueError ("User raised exception" )
1629+
1630+ spans = span_exporter .get_finished_spans ()
1631+ assert len (spans ) == 1
1632+ assert ErrorAttributes .ERROR_TYPE not in spans [0 ].attributes
1633+
1634+
1635+ @pytest .mark .cassette ("test_async_messages_create_with_raw_response" )
1636+ @pytest .mark .asyncio
1637+ @pytest .mark .vcr ()
1638+ async def test_async_messages_with_streaming_response_nonstreaming_user_exception (
1639+ span_exporter , async_anthropic_client , instrument_no_content
1640+ ):
1641+ """A caller error in a non-streaming async response context fails the span."""
1642+ model = "claude-sonnet-4-20250514"
1643+
1644+ with pytest .raises (ValueError , match = "User raised exception" ):
1645+ async with (
1646+ async_anthropic_client .messages .with_streaming_response .create (
1647+ model = model ,
1648+ max_tokens = 100 ,
1649+ messages = [
1650+ {"role" : "user" , "content" : "Say hello in one word." }
1651+ ],
1652+ )
1653+ ):
1654+ raise ValueError ("User raised exception" )
1655+
1656+ spans = span_exporter .get_finished_spans ()
1657+ assert len (spans ) == 1
1658+ assert spans [0 ].attributes [ErrorAttributes .ERROR_TYPE ] == "ValueError"
1659+
1660+
1661+ @pytest .mark .cassette ("test_async_messages_create_with_raw_response" )
1662+ @pytest .mark .asyncio
1663+ @pytest .mark .vcr ()
1664+ async def test_async_messages_with_raw_response_caller_exception (
1665+ span_exporter , async_anthropic_client , instrument_no_content
1666+ ):
1667+ """A caller error while handling a raw response is separate from the call."""
1668+ raw_response = (
1669+ await async_anthropic_client .messages .with_raw_response .create (
1670+ model = "claude-sonnet-4-20250514" ,
1671+ max_tokens = 100 ,
1672+ messages = [{"role" : "user" , "content" : "Say hello in one word." }],
1673+ )
1674+ )
1675+
1676+ with pytest .raises (ValueError , match = "User raised exception" ):
1677+ raise ValueError ("User raised exception" )
1678+
1679+ spans = span_exporter .get_finished_spans ()
1680+ assert len (spans ) == 1
1681+ assert ErrorAttributes .ERROR_TYPE not in spans [0 ].attributes
1682+ assert raw_response .headers is not None
1683+
1684+
15881685@pytest .mark .cassette ("test_async_messages_create_api_error" )
15891686@pytest .mark .asyncio
15901687@pytest .mark .vcr ()
@@ -1608,6 +1705,52 @@ async def test_async_messages_with_raw_response_api_error(
16081705 assert "NotFoundError" in span .attributes [ErrorAttributes .ERROR_TYPE ]
16091706
16101707
1708+ @pytest .mark .asyncio
1709+ async def test_async_streaming_response_type_survives_instrumentation_round_trip (
1710+ tracer_provider , logger_provider , meter_provider
1711+ ):
1712+ """Async instrumentation round-trips keep the SDK manager type intact."""
1713+ instrumentor = AnthropicInstrumentor ()
1714+ client = AsyncAnthropic ()
1715+
1716+ def create_context_manager ():
1717+ context_manager = client .messages .with_streaming_response .create (
1718+ model = "claude-sonnet-4-20250514" ,
1719+ max_tokens = 100 ,
1720+ messages = [{"role" : "user" , "content" : "Hello" }],
1721+ stream = True ,
1722+ )
1723+ request = getattr (context_manager , "_api_request" , None )
1724+ if inspect .iscoroutine (request ):
1725+ request .close ()
1726+ return context_manager
1727+
1728+ instrumentor .instrument (
1729+ tracer_provider = tracer_provider ,
1730+ logger_provider = logger_provider ,
1731+ meter_provider = meter_provider ,
1732+ )
1733+ try :
1734+ assert (
1735+ create_context_manager ().__class__ is AsyncResponseContextManager
1736+ )
1737+ instrumentor .uninstrument ()
1738+ assert (
1739+ create_context_manager ().__class__ is AsyncResponseContextManager
1740+ )
1741+ instrumentor .instrument (
1742+ tracer_provider = tracer_provider ,
1743+ logger_provider = logger_provider ,
1744+ meter_provider = meter_provider ,
1745+ )
1746+ assert (
1747+ create_context_manager ().__class__ is AsyncResponseContextManager
1748+ )
1749+ finally :
1750+ instrumentor .uninstrument ()
1751+ await client .close ()
1752+
1753+
16111754@pytest .mark .cassette ("test_async_messages_create_with_raw_response" )
16121755@pytest .mark .asyncio
16131756@pytest .mark .vcr ()
@@ -1634,12 +1777,17 @@ async def test_async_messages_streaming_response_is_transparent(
16341777 span_exporter , async_anthropic_client , instrument_no_content
16351778):
16361779 """The streaming proxy must also keep the SDK's type and its parsed stream."""
1637- async with async_anthropic_client .messages .with_streaming_response .create (
1638- model = "claude-sonnet-4-20250514" ,
1639- max_tokens = 100 ,
1640- messages = [{"role" : "user" , "content" : "Say hello in one word." }],
1641- stream = True ,
1642- ) as raw_response :
1780+ context_manager = (
1781+ async_anthropic_client .messages .with_streaming_response .create (
1782+ model = "claude-sonnet-4-20250514" ,
1783+ max_tokens = 100 ,
1784+ messages = [{"role" : "user" , "content" : "Say hello in one word." }],
1785+ stream = True ,
1786+ )
1787+ )
1788+ assert isinstance (context_manager , AsyncResponseContextManager )
1789+ assert context_manager .__class__ is AsyncResponseContextManager
1790+ async with context_manager as raw_response :
16431791 assert isinstance (raw_response , AsyncAPIResponse )
16441792 assert raw_response .__class__ is AsyncAPIResponse
16451793 stream = await raw_response .parse ()
0 commit comments