diff --git a/py/src/braintrust/integrations/anthropic/cassettes/0.48.0/test_anthropic_sync_create_stream_logs_error_and_ends_span.yaml b/py/src/braintrust/integrations/anthropic/cassettes/0.48.0/test_anthropic_sync_create_stream_logs_error_and_ends_span.yaml new file mode 100644 index 00000000..5bc01a93 --- /dev/null +++ b/py/src/braintrust/integrations/anthropic/cassettes/0.48.0/test_anthropic_sync_create_stream_logs_error_and_ends_span.yaml @@ -0,0 +1,75 @@ +interactions: +- request: + body: '{"max_tokens":16,"messages":[{"role":"user","content":"hi"}],"model":"claude-haiku-4-5-20251001","stream":true}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + anthropic-version: + - '2023-06-01' + connection: + - keep-alive + content-length: + - '111' + content-type: + - application/json + host: + - api.anthropic.com + user-agent: + - Anthropic/Python 1.16.0 + x-stainless-arch: + - x64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - Linux + x-stainless-package-version: + - 1.16.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.15 + x-stainless-timeout: + - NOT_GIVEN + method: POST + uri: https://api.anthropic.com/v1/messages + response: + body: + string: '{"type":"error","error":{"type":"authentication_error","message":"invalid + x-api-key"},"request_id":"req_011CgEDPUuC2rTGyj3oNjLGF"}' + headers: + cache-control: + - no-store + cf-cache-status: + - DYNAMIC + cf-ray: + - a4db2dc05a155c70-MIA + connection: + - keep-alive + content-length: + - '130' + content-security-policy: + - default-src 'none'; frame-ancestors 'none' + content-type: + - application/json + date: + - Tue, 20 Oct 2026 21:40:03 GMT + request-id: + - req_011CgEDPUuC2rTGyj3oNjLGF + server: + - cloudflare + x-robots-tag: + - none + x-should-retry: + - 'false' + status: + code: 401 + message: Unauthorized +version: 1 diff --git a/py/src/braintrust/integrations/anthropic/cassettes/latest/test_anthropic_sync_create_stream_logs_error_and_ends_span.yaml b/py/src/braintrust/integrations/anthropic/cassettes/latest/test_anthropic_sync_create_stream_logs_error_and_ends_span.yaml new file mode 100644 index 00000000..5bc01a93 --- /dev/null +++ b/py/src/braintrust/integrations/anthropic/cassettes/latest/test_anthropic_sync_create_stream_logs_error_and_ends_span.yaml @@ -0,0 +1,75 @@ +interactions: +- request: + body: '{"max_tokens":16,"messages":[{"role":"user","content":"hi"}],"model":"claude-haiku-4-5-20251001","stream":true}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + anthropic-version: + - '2023-06-01' + connection: + - keep-alive + content-length: + - '111' + content-type: + - application/json + host: + - api.anthropic.com + user-agent: + - Anthropic/Python 1.16.0 + x-stainless-arch: + - x64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - Linux + x-stainless-package-version: + - 1.16.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.15 + x-stainless-timeout: + - NOT_GIVEN + method: POST + uri: https://api.anthropic.com/v1/messages + response: + body: + string: '{"type":"error","error":{"type":"authentication_error","message":"invalid + x-api-key"},"request_id":"req_011CgEDPUuC2rTGyj3oNjLGF"}' + headers: + cache-control: + - no-store + cf-cache-status: + - DYNAMIC + cf-ray: + - a4db2dc05a155c70-MIA + connection: + - keep-alive + content-length: + - '130' + content-security-policy: + - default-src 'none'; frame-ancestors 'none' + content-type: + - application/json + date: + - Tue, 20 Oct 2026 21:40:03 GMT + request-id: + - req_011CgEDPUuC2rTGyj3oNjLGF + server: + - cloudflare + x-robots-tag: + - none + x-should-retry: + - 'false' + status: + code: 401 + message: Unauthorized +version: 1 diff --git a/py/src/braintrust/integrations/anthropic/test_anthropic.py b/py/src/braintrust/integrations/anthropic/test_anthropic.py index 588a5c5e..ecef4680 100644 --- a/py/src/braintrust/integrations/anthropic/test_anthropic.py +++ b/py/src/braintrust/integrations/anthropic/test_anthropic.py @@ -1740,3 +1740,24 @@ def test_sync_batches_results_logs_error_on_failure(self, memory_logger): span = spans[0] assert span["span_attributes"]["name"] == "anthropic.messages.batches.results" assert "results fetch failed" in span["error"] + + +@pytest.mark.vcr +def test_anthropic_sync_create_stream_logs_error_and_ends_span(memory_logger): + # A request that the API rejects (here: an invalid API key, a 401) must end the span with + # the error, as it does for the non-streaming call and for the async client. + assert not memory_logger.pop() + + client = wrap_anthropic(anthropic.Anthropic(api_key="invalid-key-for-error-cassette")) + with pytest.raises(anthropic.AuthenticationError): + client.messages.create( + model=MODEL, + max_tokens=16, + messages=[{"role": "user", "content": "hi"}], + stream=True, + ) + + spans = memory_logger.pop() + assert len(spans) == 1 + assert spans[0]["span_attributes"]["name"] == "anthropic.messages.stream" + assert spans[0]["error"] diff --git a/py/src/braintrust/integrations/anthropic/tracing.py b/py/src/braintrust/integrations/anthropic/tracing.py index 27ea2220..a9dae240 100644 --- a/py/src/braintrust/integrations/anthropic/tracing.py +++ b/py/src/braintrust/integrations/anthropic/tracing.py @@ -241,7 +241,12 @@ def create(self, *args, **kwargs): def __trace_stream(self, stream_func, *args, **kwargs): span = _start_span("anthropic.messages.stream", kwargs) request_start_time = time.time() - s = stream_func(*args, **kwargs) + try: + s = stream_func(*args, **kwargs) + except Exception as e: + span.log(error=e) + span.end() + raise return TracedMessageStreamManager(s, span, request_start_time)