From a8dfdde2ba85ceca3fd2b5891a0275c51406f7d8 Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Tue, 20 Oct 2026 17:38:30 -0400 Subject: [PATCH] fix(anthropic): end the span when a streaming messages.create call fails The sync Messages.create(stream=True) path started the span and then called the SDK with no error handling. When the request itself failed (for example a 4xx or a connection error), the span was never ended and the error was never logged, so the failed call showed up as an unfinished span without an error. The async client already logs the error and ends the span in that case. The sync path now does the same. Co-Authored-By: Claude Sonnet 5.5 --- ...reate_stream_logs_error_and_ends_span.yaml | 75 +++++++++++++++++++ ...reate_stream_logs_error_and_ends_span.yaml | 75 +++++++++++++++++++ .../integrations/anthropic/test_anthropic.py | 21 ++++++ .../integrations/anthropic/tracing.py | 7 +- 4 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 py/src/braintrust/integrations/anthropic/cassettes/0.48.0/test_anthropic_sync_create_stream_logs_error_and_ends_span.yaml create mode 100644 py/src/braintrust/integrations/anthropic/cassettes/latest/test_anthropic_sync_create_stream_logs_error_and_ends_span.yaml 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)