Summary
otelprovider's span carries the agent's identity but not its cost. There is currently no way to see token usage or the model on a trace, even though both are already available at the point where the span is written.
The result is a stopwatch with good labels: you can see that an agent ran and for how long, but not what it spent.
What the span emits today
Running an agent with otelprovider.NewMiddleware(...) attached:
span: BrainAgent 18.14s
gen_ai.operation.name = invoke_agent
gen_ai.provider.name = anthropic
gen_ai.agent.id = e7f04186-...
gen_ai.agent.name = BrainAgent
gen_ai.agent.description =
That is the whole attribute set.
What is missing, and why it is easy
The OpenTelemetry GenAI semantic conventions define all of these, and the constants are already in the semconv package this repo imports:
gen_ai.usage.input_tokens (semconv.GenAIUsageInputTokens)
gen_ai.usage.output_tokens (semconv.GenAIUsageOutputTokens)
gen_ai.request.model (semconv.GenAIRequestModel)
And the data is already flowing past the middleware. anthropicprovider emits usage into the response stream (provider/anthropicprovider/agent.go:109):
contents = append(contents, &message.UsageContent{
Details: toUsageDetails(resp.Usage),
})
message.UsageDetails already carries InputTokenCount, OutputTokenCount, TotalTokenCount, CachedInputTokenCount, ReasoningTokenCount.
So the middleware, which already iterates the stream, can accumulate *message.UsageContent and set the attributes on the span it already opened. I grepped the repo: input_tokens currently appears only inside serialization helpers, never as a span attribute.
Why this is the attribute set people actually want
Token count is cost, and cost is the main reason to trace an agent at all. Concretely, from a real run of a 5-agent workflow once I added these attributes locally:
workflow:ticket-to-brief 303s
researcher 66s turns=6 tool_calls=14 in=197,057 out=3,195
grapher 45s turns=2 tool_calls=5 in=111,940 out=2,512
author 27s turns=2 tool_calls=5 in= 10,493 out=1,957
critic 84s turns=4 tool_calls=12 in= 75,790 out=5,515
publisher 35s turns=2 tool_calls=2 in= 15,692 out=3,414
────────────────────────────────────────────────────────────
TOTAL 410,972 in / 16,593 out
The researcher costing ~2x the author is not visible without this, and it is the single most useful thing the trace can tell you.
Suggested addition
In the middleware's stream loop, accumulate message.UsageDetails and set on span end:
gen_ai.request.model (from AgentConfig.Model; the middleware would need it passed via MiddlewareConfig, or the provider could set it)
gen_ai.usage.input_tokens / gen_ai.usage.output_tokens / gen_ai.usage.total_tokens
- optionally
gen_ai.usage.cached_input_tokens and gen_ai.usage.reasoning_tokens when non-zero
Deliberately NOT suggesting cost. Prices go stale and are deployment-specific; a framework asserting a stale price is worse than one asserting none. Exposing usage is the right primitive — let people price it themselves.
I have this working locally and am happy to open a PR.
Summary
otelprovider's span carries the agent's identity but not its cost. There is currently no way to see token usage or the model on a trace, even though both are already available at the point where the span is written.The result is a stopwatch with good labels: you can see that an agent ran and for how long, but not what it spent.
What the span emits today
Running an agent with
otelprovider.NewMiddleware(...)attached:That is the whole attribute set.
What is missing, and why it is easy
The OpenTelemetry GenAI semantic conventions define all of these, and the constants are already in the
semconvpackage this repo imports:gen_ai.usage.input_tokens(semconv.GenAIUsageInputTokens)gen_ai.usage.output_tokens(semconv.GenAIUsageOutputTokens)gen_ai.request.model(semconv.GenAIRequestModel)And the data is already flowing past the middleware.
anthropicprovideremits usage into the response stream (provider/anthropicprovider/agent.go:109):message.UsageDetailsalready carriesInputTokenCount,OutputTokenCount,TotalTokenCount,CachedInputTokenCount,ReasoningTokenCount.So the middleware, which already iterates the stream, can accumulate
*message.UsageContentand set the attributes on the span it already opened. I grepped the repo:input_tokenscurrently appears only inside serialization helpers, never as a span attribute.Why this is the attribute set people actually want
Token count is cost, and cost is the main reason to trace an agent at all. Concretely, from a real run of a 5-agent workflow once I added these attributes locally:
The researcher costing ~2x the author is not visible without this, and it is the single most useful thing the trace can tell you.
Suggested addition
In the middleware's stream loop, accumulate
message.UsageDetailsand set on span end:gen_ai.request.model(fromAgentConfig.Model; the middleware would need it passed viaMiddlewareConfig, or the provider could set it)gen_ai.usage.input_tokens/gen_ai.usage.output_tokens/gen_ai.usage.total_tokensgen_ai.usage.cached_input_tokensandgen_ai.usage.reasoning_tokenswhen non-zeroDeliberately NOT suggesting cost. Prices go stale and are deployment-specific; a framework asserting a stale price is worse than one asserting none. Exposing usage is the right primitive — let people price it themselves.
I have this working locally and am happy to open a PR.