Skip to content

fix(server-utils): Include Gemini reasoning tokens in Vercel AI token usage - #24066

Merged
RulaKhaled merged 2 commits into
developfrom
fix/v11-vercel-ai-gemini-reasoning-tokens
Sep 4, 2026
Merged

fix(server-utils): Include Gemini reasoning tokens in Vercel AI token usage#24066
RulaKhaled merged 2 commits into
developfrom
fix/v11-vercel-ai-gemini-reasoning-tokens

Conversation

@RulaKhaled

@RulaKhaled RulaKhaled commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Forward port of #23433, which landed on v10 because the Vercel AI OTel span processing it originally targeted was removed here in #23384. Without this, upgrading v10 → v11 loses the fix.

Gemini reports its reasoning ("thoughts") tokens separately from the candidate output count, so on ai v4/v5 the SDK's outputTokens covers only the visible answer and the reasoning count reaches us solely through providerMetadata.google.usageMetadata. getProviderMetadataAttributes() handled OpenAI, Anthropic, Bedrock and DeepSeek but never looked at the Google/Vertex block. The conventions define gen_ai.usage.output_tokens as reasoning-inclusive, so these spans were under-reporting rather than merely missing a breakdown — a real Gemini response of {promptTokenCount: 14, candidatesTokenCount: 1, thoughtsTokenCount: 100, totalTokenCount: 115} emitted output 1 / total 15 instead of output 101 / total 115.

Known limitations, tracked in #23993: invoke_agent spans carry no reasoning count at all, and nothing sums it from their children the way applyAccumulatedTokens does for input and output. Separately, enrichSpanOnEnd never reads usage.outputTokenDetails.reasoningTokens, which ai v6+ supplies directly — reading it would populate the reasoning breakdown for Gemini, OpenAI and Anthropic at once, and is the more valuable change for anyone on a current SDK version. This PR only helps v4/v5 users.

… usage

Forward port of #23433, which landed on `v10` because the Vercel AI OTel span
processing it originally targeted was removed here in #23384.

Gemini reports its reasoning ("thoughts") tokens separately from the candidate
output count, so on `ai` v4/v5 the SDK's `outputTokens` covers only the visible
answer and the reasoning count reaches us solely through
`providerMetadata.google.usageMetadata`. The conventions define
`gen_ai.usage.output_tokens` as reasoning-inclusive, so those spans were
under-reporting rather than merely missing a breakdown.

Read the `google`/`vertex` `usageMetadata` and derive output from
`candidatesTokenCount + thoughtsTokenCount`, the total from `totalTokenCount`,
and the reasoning breakdown alongside them. An absent `candidatesTokenCount`
counts as zero: Gemini omits it when the response is truncated during thinking,
which means no candidate tokens were produced, and skipping the recompute there
would report zero output for a call that spent its whole budget reasoning.

None of the three land on `gen_ai.invoke_agent` spans. Those carry usage summed
across every step while `providerMetadata` describes the last step alone, so
writing from it would replace an aggregate with one step's figures. Reasoning is
gated with them because it is a subset of an output that span never recomputes,
and nothing sums it across steps.

Unlike `v10` this branch has a single caller, so the rule lives directly in
`enrichSpanOnEnd` and there is no `addProviderMetadataToAttributes` equivalent.

Ref #23993

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018TR1cvQA7t6T2saCrHwwUh
@RulaKhaled
RulaKhaled marked this pull request as ready for review September 4, 2026 08:52
@RulaKhaled
RulaKhaled requested a review from a team as a code owner September 4, 2026 08:52
@RulaKhaled
RulaKhaled requested review from stephanie-anderson and removed request for a team September 4, 2026 08:52
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 28.69 kB - -
@sentry/browser - with treeshaking flags 26.99 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 26.89 kB - -
@sentry/browser (incl. Tracing) 49.07 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 49.07 kB - -
@sentry/browser (incl. Tracing, Profiling) 51.98 kB - -
@sentry/browser (incl. Tracing, Replay) 88.62 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 77.85 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 93.3 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 106.24 kB - -
@sentry/browser (incl. Feedback) 46.17 kB - -
@sentry/browser (incl. sendFeedback) 33.75 kB - -
@sentry/browser (incl. FeedbackAsync) 38.85 kB - -
@sentry/browser (incl. Metrics) 29.66 kB - -
@sentry/browser (incl. Logs) 29.94 kB - -
@sentry/browser (incl. Metrics & Logs) 30.59 kB - -
@sentry/react 30.45 kB - -
@sentry/react (incl. Tracing) 51.28 kB - -
@sentry/vue 35.91 kB - -
@sentry/vue (incl. Tracing) 51.34 kB - -
@sentry/svelte 28.71 kB - -
CDN Bundle 30.43 kB - -
CDN Bundle (incl. Tracing) 49.6 kB - -
CDN Bundle (incl. Logs, Metrics) 32.66 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 51.53 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 73.31 kB - -
CDN Bundle (incl. Tracing, Replay) 87.15 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 89.02 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 93.08 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 95.03 kB - -
CDN Bundle - uncompressed 90.13 kB - -
CDN Bundle (incl. Tracing) - uncompressed 147.81 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 96.52 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 153.59 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 225.82 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 267.44 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 273.21 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 281.15 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 286.9 kB - -
@sentry/nextjs (client) 53.89 kB - -
@sentry/sveltekit (client) 49.51 kB - -
@sentry/core/server 40.96 kB - -
@sentry/core/browser 13.53 kB - -
@sentry/node 124.76 kB +0.1% +122 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.51 kB - -
@sentry/node - without tracing 88.5 kB +0.03% +23 B 🔺
@sentry/node - without channel injection 104.08 kB +0.15% +154 B 🔺
@sentry/aws-serverless 96.87 kB +0.03% +23 B 🔺
@sentry/cloudflare (withSentry) - minified 201.56 kB - -
@sentry/cloudflare (withSentry) 501.37 kB - -

View base workflow run

Comment thread packages/server-utils/src/ai/vercel-ai/index.ts Outdated
Gating `gen_ai.usage.reasoning.output_tokens` unconditionally dropped it from
`invoke_agent` spans for every provider, not just Google. OpenAI writes the same
attribute from `providerMetadata.openai.reasoningTokens`, so a `generateText`
call against a reasoning model silently lost a count it used to report.

The reason to drop it only applies where a recomputed output is dropped with it:
that leaves the span's own output reasoning-exclusive, so the documented subset
relationship would not hold. A provider reporting reasoning against an
already-inclusive output keeps it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018TR1cvQA7t6T2saCrHwwUh

@andreiborza andreiborza left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for taking care of this!

@RulaKhaled
RulaKhaled merged commit 0238c64 into develop Sep 4, 2026
433 of 435 checks passed
@RulaKhaled
RulaKhaled deleted the fix/v11-vercel-ai-gemini-reasoning-tokens branch September 4, 2026 09:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants