Skip to content

feat(core): Accumulate tokens for gen_ai.invoke_agent spans from child LLM calls #17281

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,9 @@ describe('Vercel AI integration', () => {
'vercel.ai.settings.maxSteps': 1,
'vercel.ai.streaming': false,
'gen_ai.response.model': 'mock-model-id',
'gen_ai.usage.input_tokens': 15,
'gen_ai.usage.output_tokens': 25,
'gen_ai.usage.total_tokens': 40,
'operation.name': 'ai.generateText',
'sentry.op': 'gen_ai.invoke_agent',
'sentry.origin': 'auto.vercelai.otel',
Expand Down Expand Up @@ -550,6 +553,9 @@ describe('Vercel AI integration', () => {
'vercel.ai.settings.maxSteps': 1,
'vercel.ai.streaming': false,
'gen_ai.response.model': 'mock-model-id',
'gen_ai.usage.input_tokens': 15,
'gen_ai.usage.output_tokens': 25,
'gen_ai.usage.total_tokens': 40,
'operation.name': 'ai.generateText',
'sentry.op': 'gen_ai.invoke_agent',
'sentry.origin': 'auto.vercelai.otel',
Expand Down
48 changes: 48 additions & 0 deletions packages/core/src/utils/vercel-ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,17 @@ function onVercelAiSpanStart(span: Span): void {

function vercelAiEventProcessor(event: Event): Event {
if (event.type === 'transaction' && event.spans) {
// First pass: process all spans normally
for (const span of event.spans) {
// this mutates spans in-place
processEndedVercelAiSpan(span);
}

// Second pass: accumulate tokens for gen_ai.invoke_agent spans
// TODO: Determine how to handle token aggregation for tool call spans.
for (const span of event.spans) {
accumulateTokensFromChildSpans(span, event.spans);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Token Accumulation Error in Nested Spans

Token accumulation for nested gen_ai.invoke_agent spans is incorrect. The accumulateTokensFromChildSpans function processes spans in arbitrary array order. This means a parent gen_ai.invoke_agent span may aggregate tokens from its children before those children have fully accumulated tokens from their own descendants, resulting in incomplete token totals on the parent.

Locations (1)
Fix in Cursor Fix in Web

}
return event;
}
Expand Down Expand Up @@ -241,6 +248,47 @@ export function addVercelAiProcessors(client: Client): void {
client.addEventProcessor(Object.assign(vercelAiEventProcessor, { id: 'VercelAiEventProcessor' }));
}

/**
* For the gen_ai.invoke_agent span, iterate over child spans and aggregate tokens:
* - Input tokens from client LLM child spans that include `gen_ai.usage.input_tokens` attribute.
* - Output tokens from client LLM child spans that include `gen_ai.usage.output_tokens` attribute.
* - Total tokens from client LLM child spans that include `gen_ai.usage.total_tokens` attribute.
*
* Only immediate children of the `gen_ai.invoke_agent` span need to be considered,
* since aggregation will automatically occur for each parent span.
*/
function accumulateTokensFromChildSpans(spanJSON: SpanJSON, allSpans: SpanJSON[]): void {
if (spanJSON.op !== 'gen_ai.invoke_agent') {
return;
}
const childSpans = allSpans.filter(childSpan => childSpan.parent_span_id === spanJSON.span_id);

let totalInputTokens = 0;
let totalOutputTokens = 0;

for (const childSpan of childSpans) {
const inputTokens = childSpan.data[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE];
const outputTokens = childSpan.data[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE];

if (typeof inputTokens === 'number') {
totalInputTokens += inputTokens;
}
if (typeof outputTokens === 'number') {
totalOutputTokens += outputTokens;
}
}

if (totalInputTokens > 0) {
spanJSON.data[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE] = totalInputTokens;
}
if (totalOutputTokens > 0) {
spanJSON.data[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE] = totalOutputTokens;
}
if (totalInputTokens > 0 || totalOutputTokens > 0) {
spanJSON.data['gen_ai.usage.total_tokens'] = totalInputTokens + totalOutputTokens;
}
}

function addProviderMetadataToAttributes(attributes: SpanAttributes): void {
const providerMetadata = attributes[AI_RESPONSE_PROVIDER_METADATA_ATTRIBUTE] as string | undefined;
if (providerMetadata) {
Expand Down