[BUG] Built-in OTel claude_code.llm_request spans omit request_id/gen_ai.response.id on errored or aborted streaming requests

Status Open
Reported on v2.1.281
Maintainer reply None cached
Activity 0 comments · opened Sep 29, 2026

Summary

With built-in telemetry enabled (CLAUDE_CODE_ENABLE_TELEMETRY=1, OTLP trace export), the claude_code.llm_request span records the correlation IDs request_id, client_request_id, and gen_ai.response.id only when the request completes successfully (or returns a complete non-streaming error response). When a streaming request errors mid-response, times out, is aborted, or hits a connection error, the span is exported with error status but without any of these IDs — even though the request-id response header and the message_start message id had already arrived at the client. This makes failed/aborted requests impossible to correlate with server-side/provider records — precisely the requests one most needs to investigate.

Related: #79854 (closed, stale) reported the sibling symptom — spans exported UNSET on a mid-response server error. Span status now appears to be ERROR, but the correlation-ID attributes are still missing on error, which #79854 didn't cover.

Evidence

From OTLP traces over a 3-day window (span attributes only; no payloads):

  • Among errored claude_code.llm_request spans, request_id/gen_ai.response.id are present on <0.1% (single digits out of ~17k).
  • The only error classes that retain the IDs return a complete non-streaming response — rate_limit (429), output_content_filtered — and they carry the ID but no ttft_ms, which decouples "has the ID" from "streamed content" and pins ID capture to "obtained a complete response object."
  • Transport failures ran a long time before failing (so response headers had long since arrived), yet none carry the IDs:
  • aborted: p50 ~50 s (max ~154 s)
  • api_timeout: p50 ~30 min
  • connection_error: p50 ~3 min
  • server_error (503): p50 ~7.6 min
  • On error spans, only duration_ms is recorded (plus error/error_class/status_code); ttft_ms, first_content_ms, output_tokens, request_id, gen_ai.response.id are all absent — indicating these attributes are committed in a single completion step that the error/abort path skips.

Expected behavior

request_id (and gen_ai.response.id once message_start arrives) should be set on the span as soon as they're known — from response headers / the first stream event — and re-affirmed in the error/abort path before span.end(), so failed and aborted streaming requests remain correlatable.

Repro (high level)

  1. Enable built-in telemetry (CLAUDE_CODE_ENABLE_TELEMETRY=1, OTEL_TRACES_EXPORTER=otlp, OTLP endpoint set).
  2. Issue a streaming request that fails after the stream begins (mid-response server error, client abort after headers, or read timeout).
  3. Inspect the exported claude_code.llm_request span: status is ERROR but request_id/gen_ai.response.id are absent.

Environment

  • Claude Code built-in OTel telemetry (CLAUDE_CODE_ENABLE_TELEMETRY=1, OTEL_TRACES_EXPORTER=otlp).
  • Reproduces across Claude Code 2.1.x, including current latest builds — highest volume on 2.1.281 / 2.1.283 / 2.1.284 (observed 2.1.141 → 2.1.284). Still present ~70 releases after #79854 (2.1.211).

View original on GitHub ↗