Version
@voltagent/core: 2.8.0
Describe the bug
Trace-level usage semantics appear to differ by provider/model during tool-call runs.
For OpenAI/GPT tool-call traces, the root/agent span appears to record cumulative totalUsage across the full streamText run. For Anthropic/Claude tool-call traces, the root/agent span appears to record usage / last-step-or-provider usage instead.
This is a separate issue from trace summary totalTokens double-counting cached tokens. The problem here is that the same trace/root usage fields can mean different things depending on provider.
Why this is a problem
We listen to the same VoltAgent observability hooks and read the same fields:
usage.prompt_tokens
usage.completion_tokens
usage.total_tokens
usage.cached_tokens
But when only the model/provider changes, the semantics appear to change:
- OpenAI/GPT: trace/root usage looks cumulative across multiple model requests inside a tool-calling agent run.
- Anthropic/Claude: trace/root usage can look like last-step/provider usage even when the trace contains multiple tool calls and multiple conversation steps.
That makes provider comparisons and billing/usage dashboards very hard to reason about. A field named trace/root usage.* should have one consistent meaning across providers.
Evidence from traces
OpenAI/GPT trace with tool calls
The trace contains tool calls and multiple conversation step persists:
tool calls: currentDateTime, getBrandDetails
persisted step groups: 2, 2, 1
Usage values:
root usage.prompt_tokens = 161251
root usage.completion_tokens = 349
root usage.total_tokens = 161600
root usage.cached_tokens = 160768
llm.usage.prompt_tokens = 53909
llm.usage.completion_tokens = 122
llm.usage.total_tokens = 54031
llm.usage.cached_tokens = 53760
The root prompt usage is roughly 3x the LLM span prompt usage, which is consistent with cumulative usage across multiple requests/steps:
Anthropic/Claude trace with tool calls
The trace also contains tool calls and multiple conversation step persists:
tool calls: currentDateTime, getBrandDetails, getDataSourceConnectionStatus, querySocialMediaPostsAndComments x2
persisted step groups: 4, 4, 2, 1
Usage values:
root usage.prompt_tokens = 74865
root usage.completion_tokens = 274
root usage.total_tokens = 75139
root usage.cached_tokens = 74610
llm.usage.prompt_tokens = 74865
llm.usage.completion_tokens = 274
llm.usage.total_tokens = 75139
llm.usage.cached_tokens = 74610
Here root usage equals LLM span usage, despite the run containing multiple tool calls/steps. This suggests the trace/root usage is not cumulative in the same way as the OpenAI/GPT trace.
Source-level cause
In @voltagent/core@2.8.0, resolveFinishUsage() normally prefers totalUsage, but switches to usage for Anthropic/cache cases.
Relevant source files:
packages/core/src/utils/usage-normalizer.ts
packages/core/src/agent/agent.ts
The relevant behavior is:
providerMetadata has anthropic -> shouldUseLastStepUsage = true
resolveFinishUsage(...) -> returns usage instead of totalUsage
recordRootSpanUsageAndProviderCost(...) -> writes that value to the root span
This makes Anthropic traces use a different root usage semantics than non-Anthropic traces.
Expected behavior
Trace/root usage fields should have consistent semantics across providers.
If usage.prompt_tokens on the root span means cumulative usage for the full agent run, then it should mean that for Anthropic as well.
If Anthropic needs special cache-aware handling, VoltAgent should expose separate fields instead of changing the meaning of root usage.*, for example:
usage.prompt_tokens // cumulative trace/run usage
usage.completion_tokens // cumulative trace/run usage
usage.total_tokens // cumulative trace/run usage
usage.last_step.prompt_tokens // final/provider step usage
usage.cache_read_tokens
usage.cache_creation_tokens
Actual behavior
The same root usage fields appear to represent cumulative usage for GPT/OpenAI traces but last-step/provider usage for Claude/Anthropic traces.
Related
Related but separate from #1359, which is about exported trace summary totalTokens double-counting cached/reasoning token breakdowns.
Impact on onEnd hook consumers
This also affects application code that consumes usage from the agent onEnd hook.
For example, we currently track token usage from:
onEnd: async ({ agent, output, context, error }) => {
const usage = {
inputTokens: output?.usage?.promptTokens || 0,
outputTokens: output?.usage?.completionTokens || 0,
totalTokens: output?.usage?.totalTokens || 0,
reasoningTokens: output?.usage?.reasoningTokens || 0,
cachedInputTokens: output?.usage?.cachedInputTokens || 0,
};
}
Because output.usage appears to use different semantics depending on provider/model, this makes our own token accounting incorrect when switching models while listening to the same hook.
The expectation is not necessarily that output.usage must be cumulative or non-cumulative; the important requirement is that it must be consistent across providers:
- If
output.usage represents cumulative usage for the full agent run, it should do so for all providers.
- If
output.usage represents last-step/provider usage, it should do so for all providers.
- If both are useful, they should be exposed as separate fields with explicit names.
Right now the same hook field can mean different things for OpenAI vs Anthropic tool-call runs, which is very hard for downstream applications to handle reliably.
Code evidence permalinks
The following permalinks are from the @voltagent/core@2.8.0 tag commit 0c1a1a4a3b4ad01b5a62ae91b4370f8e685cbba3.
- Anthropic/cache cases are explicitly routed to
usage instead of totalUsage:
streamText passes both finalResult.usage and finalResult.totalUsage into resolveFinishUsage():
- The resolved value is what gets written to the root trace usage and exposed to
onEnd:
setTraceContextUsage() maps the resolved usage into root span attributes:
These links show why the same onEnd hook field and root span usage.* attributes can resolve from totalUsage for non-Anthropic providers but from usage for Anthropic/cache cases.
Version
@voltagent/core:2.8.0Describe the bug
Trace-level usage semantics appear to differ by provider/model during tool-call runs.
For OpenAI/GPT tool-call traces, the root/agent span appears to record cumulative
totalUsageacross the fullstreamTextrun. For Anthropic/Claude tool-call traces, the root/agent span appears to recordusage/ last-step-or-provider usage instead.This is a separate issue from trace summary
totalTokensdouble-counting cached tokens. The problem here is that the same trace/root usage fields can mean different things depending on provider.Why this is a problem
We listen to the same VoltAgent observability hooks and read the same fields:
But when only the model/provider changes, the semantics appear to change:
That makes provider comparisons and billing/usage dashboards very hard to reason about. A field named trace/root
usage.*should have one consistent meaning across providers.Evidence from traces
OpenAI/GPT trace with tool calls
The trace contains tool calls and multiple conversation step persists:
Usage values:
The root prompt usage is roughly 3x the LLM span prompt usage, which is consistent with cumulative usage across multiple requests/steps:
Anthropic/Claude trace with tool calls
The trace also contains tool calls and multiple conversation step persists:
Usage values:
Here root usage equals LLM span usage, despite the run containing multiple tool calls/steps. This suggests the trace/root usage is not cumulative in the same way as the OpenAI/GPT trace.
Source-level cause
In
@voltagent/core@2.8.0,resolveFinishUsage()normally preferstotalUsage, but switches tousagefor Anthropic/cache cases.Relevant source files:
The relevant behavior is:
This makes Anthropic traces use a different root usage semantics than non-Anthropic traces.
Expected behavior
Trace/root usage fields should have consistent semantics across providers.
If
usage.prompt_tokenson the root span means cumulative usage for the full agent run, then it should mean that for Anthropic as well.If Anthropic needs special cache-aware handling, VoltAgent should expose separate fields instead of changing the meaning of root
usage.*, for example:Actual behavior
The same root usage fields appear to represent cumulative usage for GPT/OpenAI traces but last-step/provider usage for Claude/Anthropic traces.
Related
Related but separate from #1359, which is about exported trace summary
totalTokensdouble-counting cached/reasoning token breakdowns.Impact on
onEndhook consumersThis also affects application code that consumes usage from the agent
onEndhook.For example, we currently track token usage from:
Because
output.usageappears to use different semantics depending on provider/model, this makes our own token accounting incorrect when switching models while listening to the same hook.The expectation is not necessarily that
output.usagemust be cumulative or non-cumulative; the important requirement is that it must be consistent across providers:output.usagerepresents cumulative usage for the full agent run, it should do so for all providers.output.usagerepresents last-step/provider usage, it should do so for all providers.Right now the same hook field can mean different things for OpenAI vs Anthropic tool-call runs, which is very hard for downstream applications to handle reliably.
Code evidence permalinks
The following permalinks are from the
@voltagent/core@2.8.0tag commit0c1a1a4a3b4ad01b5a62ae91b4370f8e685cbba3.usageinstead oftotalUsage:shouldUseLastStepUsage()returnstruewhenproviderMetadatahasanthropicresolveFinishUsage()returnsusage ?? totalUsagewhenshouldUseLastStepUsage(...)is true; otherwise it returnstotalUsage ?? usagestreamTextpasses bothfinalResult.usageandfinalResult.totalUsageintoresolveFinishUsage():providerUsageis read fromfinalResult.usage, thenresolveFinishUsage({ usage: providerUsage, totalUsage: finalResult.totalUsage })is calledonEnd:recordRootSpanUsageAndProviderCost(..., usageForFinish, ...)writes the resolved usage to the root trace contextconst usage = convertUsage(usageForFinish)onEndreceivesoutput: { text, usage, ... }, so hook consumers see this provider-dependent resolved usagesetTraceContextUsage()maps the resolved usage into root span attributes:traceContext.setUsage({ promptTokens, completionTokens, totalTokens, cachedTokens, reasoningTokens })These links show why the same
onEndhook field and root spanusage.*attributes can resolve fromtotalUsagefor non-Anthropic providers but fromusagefor Anthropic/cache cases.