Skip to content

fix(server-utils): Read AI SDK cache token counts in Vercel AI channel subscriber - #24350

Open
sergical wants to merge 4 commits into
developfrom
fix/vercel-ai-channel-cache-tokens
Open

fix(server-utils): Read AI SDK cache token counts in Vercel AI channel subscriber#24350
sergical wants to merge 4 commits into
developfrom
fix/vercel-ai-channel-cache-tokens

Conversation

@sergical

@sergical sergical commented Sep 11, 2026

Copy link
Copy Markdown
Member

The v11 channel subscriber only maps cache token counts out of providerMetadata, keyed by provider name. Calls through the Vercel AI Gateway carry a gateway key instead, so gen_ai.usage.cache_read.input_tokens and gen_ai.usage.cache_creation.input_tokens are never set for them. The OTel processor read the SDK's normalized counts (ai.usage.cachedInputTokens) and was removed in #23384 together with its test, so this went out untested.

The subscriber now reads the SDK's normalized usage in all three shapes:

  • v5: usage.cachedInputTokens
  • v6: usage.inputTokenDetails.{cacheReadTokens,cacheWriteTokens}
  • v7: usage.inputTokens.{cacheRead,cacheWrite}

A provider-reported count from providerMetadata still wins on model-call spans. On root operations the cache keys are now last-step-only, so the aggregated SDK counts are kept.

Created with Claude Code

…l subscriber

The `ai:telemetry` channel subscriber only derived
`gen_ai.usage.cache_read.input_tokens` and
`gen_ai.usage.cache_creation.input_tokens` from provider-keyed
`providerMetadata`. Through the Vercel AI Gateway that object is keyed
`gateway`, so the cache counts the AI SDK already normalizes into its
usage object were dropped. The v10 OTel-based integration mapped them.

Read them from the usage object as well: `cachedInputTokens` (v5),
`inputTokenDetails.cacheReadTokens` / `cacheWriteTokens` (v6), and the
`inputTokens.cacheRead` / `cacheWrite` objects of v7 model-call usage.
Provider-derived values are applied afterwards and still win.

Co-authored-by: Claude <claude@anthropic.com>
@sergical
sergical requested a review from a team as a code owner September 11, 2026 21:30
@sergical
sergical requested review from logaretm and stephanie-anderson and removed request for a team September 11, 2026 21:30
Comment thread packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts Outdated
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 28.96 kB - -
@sentry/browser - with treeshaking flags 27.26 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.15 kB - -
@sentry/browser (incl. Tracing) 50.51 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 50.52 kB - -
@sentry/browser (incl. Tracing, Profiling) 53.5 kB - -
@sentry/browser (incl. Tracing, Replay) 90.07 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 79.16 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 94.77 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 107.73 kB - -
@sentry/browser (incl. Feedback) 46.46 kB - -
@sentry/browser (incl. sendFeedback) 34.01 kB - -
@sentry/browser (incl. FeedbackAsync) 39.12 kB - -
@sentry/browser (incl. Metrics) 29.98 kB - -
@sentry/browser (incl. Logs) 30.24 kB - -
@sentry/browser (incl. Metrics & Logs) 30.91 kB - -
@sentry/react 30.72 kB - -
@sentry/react (incl. Tracing) 52.81 kB - -
@sentry/vue 36.2 kB - -
@sentry/vue (incl. Tracing) 52.76 kB - -
@sentry/svelte 28.98 kB - -
CDN Bundle 30.7 kB - -
CDN Bundle (incl. Tracing) 51.01 kB - -
CDN Bundle (incl. Logs, Metrics) 32.98 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 52.99 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 73.67 kB - -
CDN Bundle (incl. Tracing, Replay) 88.56 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 90.53 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 94.64 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 96.63 kB - -
CDN Bundle - uncompressed 90.83 kB - -
CDN Bundle (incl. Tracing) - uncompressed 152.33 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 97.41 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 158.29 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 226.82 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 271.9 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 277.85 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 285.6 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 291.54 kB - -
@sentry/nextjs (client) 55.13 kB - -
@sentry/sveltekit (client) 50.93 kB - -
@sentry/core/server 37.13 kB - -
@sentry/core/browser 13.66 kB - -
@sentry/node 129.93 kB +0.11% +131 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.83 kB - -
@sentry/node - without tracing 89.62 kB +0.04% +32 B 🔺
@sentry/node - without channel injection 108.82 kB +0.13% +140 B 🔺
@sentry/aws-serverless 97.83 kB +0.03% +21 B 🔺
@sentry/cloudflare (withSentry) - minified 203.36 kB - -
@sentry/cloudflare (withSentry) 506.44 kB - -

View base workflow run

Co-authored-by: Claude <claude@anthropic.com>
@sergical
sergical requested a review from RulaKhaled September 11, 2026 22:09

@logaretm logaretm 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.

Minor change

Comment thread packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts Outdated
Co-authored-by: Abdelrahman Awad <abdelrahman.awad@sentry.io>

@logaretm logaretm 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.

clanker flagged some stuff, plus got a question on undefined possibly now wiping attributes but doubt it could happen.

Could that be tested?

GEN_AI_USAGE_OUTPUT_TOKENS,
GEN_AI_USAGE_TOTAL_TOKENS,
GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS,
GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS,

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.

Adding the two cache keys here drops them from providerAttributes on every root operation, but I think v6/v7 have replacements for them so it's not an issue there.

On v4/v5 cache_creation would get dropped from the root span, is that desirable?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think so. On a root span the other last-step keys (output_tokens, total_tokens) are already dropped for the same reason, and a last-step cache write count on a span whose input/read counts are aggregated across steps reads as the whole call's writes. On v4/v5 the SDK reports no cache writes at all, so the root span ends up with reads only, which matches what the SDK itself exposes. Model-call spans keep the providerMetadata value in every version.

if (totalTokens !== undefined) {
span.setAttribute(GEN_AI_USAGE_TOTAL_TOKENS, totalTokens);
}
span.setAttributes(cacheTokenAttributes(usage));

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.

Q: Now that undefined could be passed in here, is it possible that it wipes previously set attributes? I don't think that case could happen but maybe something to check as well.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch, it did: setAttributes with an undefined value removed an attribute that was already on the span, so a v5 usage object (no cache write count) wiped a cache_creation set earlier. Fixed in fa57569 by only setting the counts that are reported, with a test for the existing-attribute case.

* Cache token counts as the AI SDK normalizes them: v5 `cachedInputTokens`, v6 `inputTokenDetails`,
* v7 `inputTokens.{cacheRead,cacheWrite}`.
*/
function cacheTokenAttributes(usage: Record<string, unknown>): Record<string, number> {

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.

Since it could be number | undefined this might fail now in the tests.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Resolved by the same change: the helper now returns { cacheRead?, cacheWrite? } and the callers skip undefined, so nothing typed number receives it.

Co-authored-by: Claude <claude@anthropic.com>
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