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..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 @@ -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,43 @@ describe.each(matrix)('Vercel AI integration (version %s)', (version, vercelAiVe }, ); + createEsmTests( + __dirname, + 'scenario-cache-tokens.mjs', + 'instrument.mjs', + (createRunner, test) => { + test('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',