Commit e00cd1d
committed
fix(llmobs/ai): surface prompt cache tokens for Vercel AI SDK integration across all supported providers (#8530)
* fix(llmobs/ai): surface prompt cache tokens for AI SDK Bedrock integration
Reads `ai.usage.cachedInputTokens` (AI SDK v6+ standardized attribute) and
`ai.response.providerMetadata.bedrock.usage.cache{Read,Write}InputTokens` and
tags them as `cache_read_input_tokens` / `cache_write_input_tokens` on the
LLMObs span. Without this, prompt-cached Bedrock requests via the Vercel AI
SDK appeared as 100% non-cached input in LLMObs, breaking cost and
cache-hit-rate dashboards.
Tests use the Vertex AI fetch-stub pattern (passing a mock fetch directly
to createAmazonBedrock) because @ai-sdk/amazon-bedrock uses globalThis.fetch
which nock cannot intercept. Two real-shape Bedrock Converse API fixtures
(cache read + cache write) exercise the extraction across ai 5.x / 6.x and
the compatible @ai-sdk/amazon-bedrock 3.x / 4.x versions.
Bumps the `ai` cap in test versions/package.json from 6.0.39 to 6.0.185 so
tests run against the SDK version that emits cache attributes on the
doGenerate span (older v6 versions only emit them on the parent generateText
span). Supported versions table regenerated.
Closes MLOB-7519
Refs MLOS-633
* fix(llmobs/ai): skip zero cache values and polyfill crypto for Node 18
The AI SDK sets `ai.usage.cachedInputTokens=0` on every language model
span regardless of provider, which caused our plugin to tag
`cache_read_input_tokens: 0` on spans that don't actually use prompt
caching (e.g., OpenAI calls). This broke existing OpenAI tests that
assert exact metric key counts. Skip emitting zero values so non-caching
spans stay clean — the cost estimator math is unchanged because
subtracting 0 is a no-op.
Additionally, `@ai-sdk/amazon-bedrock` signs requests with `aws4fetch`
which uses `globalThis.crypto` for SHA256/HMAC. Node 19+ exposes that as
a global, but Node 18 (still supported and used in CI) does not. Polyfill
from `node:crypto.webcrypto` in the Bedrock test setup.
Bedrock cache test assertions updated to expect `undefined` (not `0`)
for the absent cache field in each scenario.
* test(llmobs/ai): add Anthropic prompt cache tests and parameterize for additional providers
Extracts the Bedrock prompt-cache integration tests into a generic
`describeProviderCacheTests()` helper that takes a provider config
object (display name, package mapper, model builder, optional env
vars). Adds Anthropic as the second consumer, exercising the
`providerMetadata.anthropic.cache{Read,Creation}InputTokens` branch
of `getProviderCacheTokens()` that was previously uncovered by tests
(Codecov diff coverage gap).
Anthropic SDK has the same multi-version normalization quirk as
Bedrock (verified in source): `@ai-sdk/anthropic@3.x` normalizes
`inputTokens.total = inputTokens + cacheCreation + cacheRead` via
`convertToLanguageModelV3Usage`, while `@2.x` and `@1.x` pass the
raw fresh count through. The generic test helper exercises all
three majors via `withVersions`.
Additional providers (OpenAI, Google, etc.) can be added by passing
one more config object to `describeProviderCacheTests()` — no
duplicated test orchestration code.
* test(llmobs/ai): add OpenAI prompt cache tests across Chat Completions and Responses APIs
Proves the cache_read capture in this plugin is provider-agnostic and
not specific to Bedrock or Anthropic. OpenAI exercises the
`ai.usage.cachedInputTokens` standardized-attribute path — by virtue of
covering OpenAI, this also transitively validates Google Gemini, xAI,
and any other provider whose `@ai-sdk/<name>` package populates the
standardized attribute.
Two consumers added because `@ai-sdk/openai` v1/v2/v3 route the default
`openai(modelId)` factory to the Responses API, while `.chat(modelId)`
routes to Chat Completions. The two endpoints have different response
shapes and the SDK has separate parsing for each:
- Chat Completions: usage.prompt_tokens_details.cached_tokens
- Responses API: usage.input_tokens_details.cached_tokens
Both endpoints surface cache_read via `ai.usage.cachedInputTokens` on
the doGenerate span, but covering both paths future-proofs the plugin
against any SDK regression that might affect only one endpoint —
relevant because OpenAI is actively migrating customers from Chat
Completions to Responses.
Extends `describeProviderCacheTests()` helper with two new config
options to support providers whose semantics differ from
Bedrock/Anthropic:
- `scenarios`: which scenarios to test (OpenAI lacks per-request
cache_write, so it passes ['cache-read'] only)
- `getExpectedMetrics`: version-aware expectations (OpenAI's
`prompt_tokens` / `input_tokens` is always the sum at the API
level, so `ai.usage.inputTokens` is 4448 across all ai versions —
unlike Bedrock/Anthropic where the v5-paired SDK passes raw
fresh through and the v6-paired SDK normalizes)
18 tests now exercise the full provider matrix:
- Bedrock (cache_read + cache_write) x 3 ai versions = 6
- Anthropic (cache_read + cache_write) x 3 ai versions = 6
- OpenAI Chat Completions (cache_read) x 3 ai versions = 3
- OpenAI Responses API (cache_read) x 3 ai versions = 3
* test(llmobs/ai): add Google Gemini prompt cache tests
Google Gemini exercises a third upstream API shape distinct from both
the Bedrock/Anthropic family (provider-specific cache fields in
providerMetadata) and the OpenAI-compatible family (Chat Completions /
Responses with shared field names).
Google's response uses `usageMetadata.cachedContentTokenCount` at the
top level of the response (not nested under a provider namespace), and
the `@ai-sdk/google` provider package maps it to
`ai.usage.cachedInputTokens` via the standardized attribute path.
Like OpenAI, Google's `promptTokenCount` already includes cached
tokens at the API level, so `ai.usage.inputTokens` is the sum across
all ai versions.
Per-request cache write metric is not exposed by Google (context
caching requires a separate API call to create the cache), so only
cache-read is tested.
By covering three distinct API shapes (Bedrock/Anthropic native, OpenAI
shared, Google native), we establish that the plugin's cache token
capture is genuinely provider-agnostic via the AI SDK v5+ standardized
attribute, not just inferentially supported. Transitively this also
covers xAI (which uses @ai-sdk/openai-compatible under the hood — same
code paths as OpenAI).
21 tests now cover the full provider matrix.
* fix(llmobs/ai): normalize inputTokens to the sum convention used by bedrockruntime
Addresses Codex review feedback: some provider/SDK combinations leave
`ai.usage.inputTokens` as the raw fresh count while still surfacing
cache_write tokens in `providerMetadata`. A downstream consumer doing
`nonCached = input - cacheRead - cacheWrite` would compute a negative
number, and the upcoming backend partial-tokens UI warning would fire
even though the cost math is otherwise correct via the token_normalizer
clamp.
Specifically affected stacks:
- ai@5 + @ai-sdk/amazon-bedrock@3.x (bedrock@3 doesn't normalize)
- ai@4-5 + @ai-sdk/anthropic@1-2.x (older anthropic SDK doesn't normalize)
Matches the convention in bedrockruntime.js:157 — input_tokens is always
the sum of fresh + cache_read + cache_write.
Detection uses a math check: if `inputTokens < cacheRead + cacheWrite`,
the value cannot already be a sum (would be impossible for fresh to be
less than cached). This is the only signal that reliably differentiates
"raw fresh" from "already normalized" without false positives on stacks
where the upstream SDK has already done the work (ai@6 + bedrock@4 /
anthropic@3, OpenAI, Google).
Tests updated for ai@5 cache-write scenarios where `input_tokens` is now
the normalized sum (4448) instead of raw fresh (23).
* chore: regenerate supported versions after rebase
* chore: sync versions/package.json with master and add provider sdk caps1 parent acf2647 commit e00cd1d
13 files changed
Lines changed: 587 additions & 46 deletions
File tree
- packages
- datadog-plugin-ai/test/resources
- dd-trace
- src/llmobs/plugins/ai
- test
- llmobs/plugins/ai
- plugins
- versions
Lines changed: 15 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
Lines changed: 15 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
Lines changed: 17 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
Lines changed: 17 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
Lines changed: 19 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
Lines changed: 33 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
Lines changed: 33 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
66 | 66 | | |
67 | 67 | | |
68 | 68 | | |
69 | | - | |
70 | | - | |
71 | | - | |
72 | | - | |
73 | | - | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
74 | 85 | | |
75 | 86 | | |
76 | 87 | | |
| |||
87 | 98 | | |
88 | 99 | | |
89 | 100 | | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
90 | 134 | | |
91 | 135 | | |
92 | 136 | | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
93 | 179 | | |
94 | 180 | | |
95 | 181 | | |
| |||
0 commit comments