Repository navigation
Description
Which component is this bug for?
Anthropic instrumentation
📜 Description
When a streamed Anthropic response is not read to the end, the LLM span is never ended and never exported, even after the stream object is garbage-collected. This affects the most common ways of stopping a stream early:
- leaving a
withblock (sync and async) - the SDK's recommended helper:
with client.messages.stream(...) as s:and thenbreakout ofs.text_stream stream.close()/await stream.close()break, dropping the stream, or never iterating it
The model call, its tokens and its cost are silently missing from the trace.
Root cause: in streaming.py, AnthropicStream and AnthropicAsyncStream end the span only in _complete_instrumentation(), which runs on stream exhaustion or from get_final_message() / until_done(). The wrappers define no close, no cleanup in __exit__ / __aexit__, and no __del__ fallback, so every early exit leaves the span open permanently.
(The OpenAI instrumentation's ChatStream has __exit__ / __del__ cleanup since #3155, so the Anthropic wrappers are inconsistent with it.)
👟 Reproduction steps
Self-contained (mock HTTP transport, no API key). Run with python repro_anthropic.py:
import asyncio, gc, json, anthropic
try:
import httpx2 as httpx
except ImportError:
import httpx
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
def ev(name, data): return f"event: {name}\ndata: {json.dumps(data)}\n\n"
SSE = (ev("message_start", {"type": "message_start", "message": {"id": "msg_1", "type": "message",
"role": "assistant", "model": "m", "content": [], "stop_reason": None, "stop_sequence": None,
"usage": {"input_tokens": 3, "output_tokens": 0}}})
+ ev("content_block_start", {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}})
+ "".join(ev("content_block_delta", {"type": "content_block_delta", "index": 0,
"delta": {"type": "text_delta", "text": p}}) for p in ["Hel", "lo", "!"])
+ ev("content_block_stop", {"type": "content_block_stop", "index": 0})
+ ev("message_delta", {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": None},
"usage": {"output_tokens": 3}})
+ ev("message_stop", {"type": "message_stop"})).encode()
handler = lambda r: httpx.Response(200, headers={"content-type": "text/event-stream"}, content=SSE)
exporter = InMemorySpanExporter()
provider = TracerProvider(); provider.add_span_processor(SimpleSpanProcessor(exporter))
AnthropicInstrumentor().instrument(tracer_provider=provider)
KW = dict(model="m", max_tokens=10, messages=[{"role": "user", "content": "hi"}])
client = anthropic.Anthropic(api_key="x", base_url="http://mock",
http_client=httpx.Client(transport=httpx.MockTransport(handler)))
def spans(): gc.collect(); return len(exporter.get_finished_spans())
# 1. Recommended helper, stop after the first text chunk
exporter.clear()
with client.messages.stream(**KW) as s:
for text in s.text_stream:
break
del s
print("messages.stream() early exit:", spans()) # expected 1, actual 0
# 2. Plain stream in a with-block, stop after the first event
exporter.clear()
with client.messages.create(stream=True, **KW) as s:
next(iter(s))
del s
print("with-block early exit: ", spans()) # expected 1, actual 0
# 3. Async with-block
async def main():
aclient = anthropic.AsyncAnthropic(api_key="x", base_url="http://mock",
http_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)))
exporter.clear()
async with await aclient.messages.create(stream=True, **KW) as s:
await s.__aiter__().__anext__()
del s
print("async with-block early exit:", spans()) # expected 1, actual 0
asyncio.run(main())👍 Expected behavior
Exactly one finished span per call when the stream is stopped early by leaving a with block, by the messages.stream() helper, or by close(). Paths with no explicit end signal (break, drop, never iterated) should at least end the span when the object is released.
👎 Actual Behavior with Screenshots
messages.stream() early exit: 0
with-block early exit: 0
async with-block early exit: 0
Spans ended (still referenced / after release / after gc.collect()), anthropic 1.11.0:
| Path | sync | async |
|---|---|---|
| read to end | 1 / 1 / 1 | 1 / 1 / 1 |
messages.stream() read to end |
1 / 1 / 1 | — |
messages.stream() early exit |
0 / 0 / 0 | 0 / 0 / 0 |
with block |
0 / 0 / 0 | 0 / 0 / 0 |
close() |
0 / 0 / 0 | 0 / 0 / 0 |
break / drop / never iterated |
0 / 0 / 0 | 0 / 0 / 0 |
For comparison, opentelemetry-instrumentation-genai-anthropic 1.2b0 ends the span on the with block, the helper's early exit and close().
🤖 Python Version
3.12
📃 Provide any additional context for the Bug.
- Reproduced on 0.62.4 and on
mainas of 2026-10-01; anthropic 1.11.0, opentelemetry-sdk 1.45.0, Ubuntu 24.04. - Related: 🐛 Bug Report: OpenAI instrumentation: ChatStream does not end the span on close() (sync or async) #4532 (the OpenAI instrumentation's
ChatStreamdoes not end the span onclose()); 🐛 Bug Report: (OpenAI instrumentation) Streaming response traces and metrics lost when stream not fully consumed #3151 / fix(openai): support for openai non-consumed streams #3155 (cleanup added toChatStreamfor non-consumed streams; the Anthropic wrappers have no equivalent).
Suggested fix: route every exit through _complete_instrumentation() (made idempotent): __exit__ / __aexit__, close, and a __del__ fallback, plus the same in WrappedMessageStreamManager.__exit__ / WrappedAsyncMessageStreamManager.__aexit__, so the messages.stream() helper ends the span when its block exits.
👀 Have you spent some time to check if this bug has been raised before?
- I checked and didn't find similar issue
Are you willing to submit PR?
Yes I am willing to submit a PR!
Activity
rupak-eng commentedon Oct 2, 2026
Working on this — the Anthropic wrappers are indeed inconsistent with the OpenAI side (fixed for
ChatStreamin #4534). Putting up a PR shortly that addsclose()/__exit__(+__del__fallback) toAnthropicStream,close()/__aexit__toAnthropicAsyncStream, and cleanup in theWrappedMessageStreamManagerexits.