From d3446417467a6f272b2e56d8bd199a2031763aaa Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 14 Sep 2026 10:10:07 +0200 Subject: [PATCH 1/2] test(node): Cover Vercel AI v7 cache tokens from the SDK usage object Add a node integration test that runs the real `ai@^7` package through the channel subscriber with `providerMetadata` keyed `gateway` (the AI Gateway shape), asserting cache read/write counts are still derived from the SDK's normalized usage. Skipped on v6, whose OTel processor only reads `providerMetadata`. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../vercelai/v6_v7/scenario-cache-tokens.mjs | 31 ++++++++++++++ .../suites/tracing/vercelai/v6_v7/test.ts | 42 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-cache-tokens.mjs diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-cache-tokens.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-cache-tokens.mjs new file mode 100644 index 000000000000..6e8d12a81fac --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/scenario-cache-tokens.mjs @@ -0,0 +1,31 @@ +import * as Sentry from '@sentry/node'; +import { generateText } from 'ai'; +import { MockLanguageModelV3 } from 'ai/test'; + +// Calls routed through the Vercel AI Gateway carry a `gateway` key in `providerMetadata` instead of a +// provider name, so cache counts can only be derived from the SDK's normalized usage object. +async function run() { + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + await generateText({ + experimental_telemetry: { isEnabled: true, recordInputs: true, recordOutputs: true }, + model: new MockLanguageModelV3({ + doGenerate: async () => ({ + finishReason: { unified: 'stop', raw: 'stop' }, + usage: { + inputTokens: { total: 120, noCache: 20, cacheRead: 80, cacheWrite: 20 }, + outputTokens: { total: 10, noCache: 10, cached: 0 }, + totalTokens: { total: 130, noCache: 30, cached: 100 }, + }, + content: [{ type: 'text', text: 'Cache token span!' }], + warnings: [], + providerMetadata: { + gateway: { routing: {} }, + }, + }), + }), + messages: [{ role: 'user', content: 'Tell me something' }], + }); + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts index 5c538267e935..7f9186cc3abd 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts @@ -15,6 +15,7 @@ import { GEN_AI_TOOL_DEFINITIONS, GEN_AI_TOOL_DESCRIPTION, GEN_AI_TOOL_NAME, + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, GEN_AI_USAGE_INPUT_TOKENS, GEN_AI_USAGE_OUTPUT_TOKENS, @@ -811,6 +812,47 @@ describe.each(matrix)('Vercel AI integration (version %s)', (version, vercelAiVe }, ); + createEsmTests( + __dirname, + 'scenario-cache-tokens.mjs', + 'instrument.mjs', + (createRunner, test) => { + // Through the Vercel AI Gateway, `providerMetadata` is keyed `gateway` rather than by provider, + // so cache counts can only come from the SDK's normalized usage object. Only the channel + // subscriber (v7) reads them from there; the v6 OTel processor derives cache counts from + // `providerMetadata` alone and can't see the gateway case. + test.skipIf(version === '6')('reads cache token counts from the SDK usage object', async () => { + await createRunner() + .expect({ transaction: { transaction: 'main' } }) + .expect({ + span: container => { + const generateContent = container.items.find( + span => span.attributes['sentry.op']?.value === 'gen_ai.generate_content', + )!; + expect(generateContent).toBeDefined(); + expect(generateContent.attributes[GEN_AI_USAGE_INPUT_TOKENS]?.value).toBe(120); + expect(generateContent.attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]?.value).toBe(80); + expect(generateContent.attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]?.value).toBe(20); + + const invokeAgent = container.items.find( + span => span.attributes['sentry.op']?.value === 'gen_ai.invoke_agent', + )!; + expect(invokeAgent).toBeDefined(); + expect(invokeAgent.attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]?.value).toBe(80); + expect(invokeAgent.attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]?.value).toBe(20); + }, + }) + .start() + .completed(); + }); + }, + { + additionalDependencies: { + ai: vercelAiVersion, + }, + }, + ); + createEsmTests( __dirname, 'scenario-embeddings.mjs', From 27897d2b968f9fd5c1ba62b29717f72f339008dd Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 14 Sep 2026 10:12:30 +0200 Subject: [PATCH 2/2] cleanup --- .../suites/tracing/vercelai/v6_v7/test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts index 7f9186cc3abd..d014e9308862 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6_v7/test.ts @@ -817,11 +817,7 @@ describe.each(matrix)('Vercel AI integration (version %s)', (version, vercelAiVe 'scenario-cache-tokens.mjs', 'instrument.mjs', (createRunner, test) => { - // Through the Vercel AI Gateway, `providerMetadata` is keyed `gateway` rather than by provider, - // so cache counts can only come from the SDK's normalized usage object. Only the channel - // subscriber (v7) reads them from there; the v6 OTel processor derives cache counts from - // `providerMetadata` alone and can't see the gateway case. - test.skipIf(version === '6')('reads cache token counts from the SDK usage object', async () => { + test('reads cache token counts from the SDK usage object', async () => { await createRunner() .expect({ transaction: { transaction: 'main' } }) .expect({